# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---

import ldap
from ldap import filter as ldap_filter
from lib389._mapped_object import DSLdapObject, DSLdapObjects, _normalise_attrs
from lib389.backend import Backends
from lib389.config import Config
from lib389.idm.account import Account
from lib389.idm.nscontainer import nsContainers, nsContainer
from lib389.idm.user import (
    BasicUserAccounts,
    nsUserAccounts,
    TraditionalUserAccounts,
)
from lib389.idm.services import ServiceAccounts
from lib389.cos import CosPointerDefinitions, CosPointerDefinition, CosTemplates


POLICY_TYPE_GLOBAL = 'Global Policy'
POLICY_TYPE_USER = 'User Policy'
POLICY_TYPE_SUBTREE = 'Subtree Policy'

SYNTAX_POLICY_ATTRS = [
    'passwordchecksyntax',
    'passwordminlength',
    'passwordmindigits',
    'passwordminalphas',
    'passwordminuppers',
    'passwordminlowers',
    'passwordminspecials',
    'passwordmin8bit',
    'passwordmaxrepeats',
    'passwordpalindrome',
    'passwordmaxsequence',
    'passwordmaxseqsets',
    'passwordmaxclasschars',
    'passwordmincategories',
    'passwordmintokenlength',
    'passwordbadwords',
    'passworduserattributes',
    'passworddictcheck',
    'passworddictpath',
]

GLOBAL_ONLY_POLICY_ATTRS = [
    'passwordisglobalpolicy',
    'nsslapd-pwpolicy-local',
    'nsslapd-allow-hashed-passwords',
    'nsslapd-pwpolicy-inherit-global',
]

USER_TYPE_ACCOUNTS = {
    'posix': nsUserAccounts,
    'basic': BasicUserAccounts,
    'service': ServiceAccounts,
    'traditional': TraditionalUserAccounts,
}

# Defaults from pwpolicy_init_defaults() in ldap/servers/slapd/libglobs.c
LOCAL_PW_POLICY_DEFAULTS = {
    'passwordchange': 'on',
    'passwordmustchange': 'off',
    'passwordchecksyntax': 'off',
    'passwordexp': 'off',
    'passwordsendexpiringtime': 'off',
    'passwordminlength': '8',
    'passwordmindigits': '0',
    'passwordminalphas': '0',
    'passwordminuppers': '0',
    'passwordminlowers': '0',
    'passwordminspecials': '0',
    'passwordmin8bit': '0',
    'passwordmaxrepeats': '0',
    'passwordmincategories': '3',
    'passwordmintokenlength': '3',
    'passwordmaxage': '8640000',
    'passwordminage': '0',
    'passwordwarning': '86400',
    'passwordhistory': 'off',
    'passwordinhistory': '6',
    'passwordlockout': 'off',
    'passwordmaxfailure': '3',
    'passwordunlock': 'on',
    'passwordlockoutduration': '3600',
    'passwordresetfailurecount': '600',
    'passwordTPRMaxUse': '-1',
    'passwordTPRDelayExpireAt': '-1',
    'passwordTPRDelayValidFrom': '-1',
    'passwordgracelimit': '0',
    'passwordadmindn': '',
    'passwordadminskipinfoupdate': 'off',
    'passwordtrackupdatetime': 'off',
    'passwordpalindrome': 'off',
    'passwordmaxsequence': '0',
    'passwordmaxseqsets': '0',
    'passwordmaxclasschars': '0',
    'passwordbadwords': '',
    'passworduserattributes': '',
    'passworddictcheck': 'off',
    'passworddictpath': '',
    'passwordstoragescheme': '',
}


def _decode_cn_dn_fragment(fragment):
    """Decode LDAP escaped characters in a cn-stored DN fragment."""
    return (fragment.replace('\\3D', '=').replace('\\3d', '=')
            .replace('\\2C', ',').replace('\\2c', ','))


def _parse_policy_target_from_cn(cn_value):
    """Return the policy target DN encoded in a policy entry cn value."""
    if not cn_value:
        return None
    cn_value = cn_value.strip("'").strip('"')
    cn_lower = cn_value.lower()
    prefixes = (
        'cn=nspwpolicyentry_user,',
        'cn=nspwpolicyentry_subtree,',
    )
    if not any(cn_lower.startswith(p) for p in prefixes):
        return None
    try:
        dn_comps = ldap.dn.explode_dn(cn_value)
        dn_comps.pop(0)
        if dn_comps:
            return ','.join(dn_comps)
    except ldap.DECODING_ERROR:
        # Decoding errors are ok, just proceed
        pass

    remainder = _decode_cn_dn_fragment(cn_value[cn_value.index(',') + 1:])
    try:
        return ldap.dn.dn2str(ldap.dn.str2dn(remainder))
    except ldap.DECODING_ERROR:
        # It's ok, just return the remainder
        return remainder


def _classify_policy_from_pwdpolicysubentry(pwp_dn, pwp_entry):
    """Return policy type, source DN, and target DN for a local policy entry."""
    cn = pwp_entry.get_attr_val_utf8('cn') or ''
    cn_l = cn.lower()
    if 'nspwpolicyentry_user' in cn_l:
        policy_type = POLICY_TYPE_USER
    elif 'nspwpolicyentry_subtree' in cn_l:
        policy_type = POLICY_TYPE_SUBTREE
    else:
        raise ValueError(
            'Unable to determine password policy type for entry "{}"'.format(pwp_dn))
    target = _parse_policy_target_from_cn(cn)
    if not target:
        raise ValueError(
            'Unable to parse password policy target from policy entry "{}"'.format(pwp_dn))
    return policy_type, pwp_dn, target


def _attr_is_on(value):
    if value is None:
        return False
    return str(value).lower() in ('on', 'true', '1', 'yes')


def _should_show_inherited_value(value):
    """Return True when an inherited setting should appear in local policy output."""
    if value is None:
        return False
    text = str(value).strip()
    if not text or text == '0':
        return False
    if text.lower() == 'off':
        return False
    return True


def _first_attr_value(attrs, attr_name, default=''):
    if not attrs:
        return default
    values = attrs.get(attr_name, [])
    if values:
        return values[0]
    attr_lower = attr_name.lower()
    for key, vals in attrs.items():
        if key.lower() == attr_lower and vals:
            return vals[0]
    return default


def _make_attr_entry(value, inherited=False):
    return {'values': ['' if value is None else str(value)], 'inherited': inherited}


class PwPolicyManager(object):
    """Manages user, subtree and global password policies

    :param instance: An instance
    :type instance: lib389.DirSrv
    """

    def __init__(self, instance):
        self._instance = instance
        self.log = instance.log
        self.arg_to_attr = {
            'pwdlocal': 'nsslapd-pwpolicy-local',
            'pwdscheme': 'passwordstoragescheme',
            'pwdchange': 'passwordchange',
            'pwdmustchange': 'passwordmustchange',
            'pwdhistory': 'passwordhistory',
            'pwdhistorycount': 'passwordinhistory',
            'pwdadmin': 'passwordadmindn',
            'pwdadminskipupdates': 'passwordadminskipinfoupdate',
            'pwdtrack': 'passwordtrackupdatetime',
            'pwdwarning': 'passwordwarning',
            'pwdisglobal': 'passwordisglobalpolicy',
            'pwdexpire': 'passwordexp',
            'pwdmaxage': 'passwordmaxage',
            'pwdminage': 'passwordminage',
            'pwdgracelimit': 'passwordgracelimit',
            'pwdsendexpiring': 'passwordsendexpiringtime',
            'pwdlockout': 'passwordlockout',
            'pwdunlock': 'passwordunlock',
            'pwdlockoutduration': 'passwordlockoutduration',
            'pwdmaxfailures': 'passwordmaxfailure',
            'pwdresetfailcount': 'passwordresetfailurecount',
            'pwdchecksyntax': 'passwordchecksyntax',
            'pwdminlen': 'passwordminlength',
            'pwdmindigits': 'passwordmindigits',
            'pwdminalphas': 'passwordminalphas',
            'pwdminuppers': 'passwordminuppers',
            'pwdminlowers': 'passwordminlowers',
            'pwdminspecials': 'passwordminspecials',
            'pwdmin8bits': 'passwordmin8bit',
            'pwdmaxrepeats': 'passwordmaxrepeats',
            'pwdpalindrome': 'passwordpalindrome',
            'pwdmaxseq': 'passwordmaxsequence',
            'pwdmaxseqsets': 'passwordmaxseqsets',
            'pwdmaxclasschars': 'passwordmaxclasschars',
            'pwdmincatagories': 'passwordmincategories',
            'pwdmintokenlen': 'passwordmintokenlength',
            'pwdbadwords': 'passwordbadwords',
            'pwduserattrs': 'passworduserattributes',
            'pwddictcheck': 'passworddictcheck',
            'pwddictpath': 'passworddictpath',
            'pwdallowhash': 'nsslapd-allow-hashed-passwords',
            'pwpinheritglobal': 'nsslapd-pwpolicy-inherit-global',
            'pwptprmaxuse': 'passwordTPRMaxUse',
            'pwptprdelayexpireat': 'passwordTPRDelayExpireAt',
            'pwptprdelayvalidfrom': 'passwordTPRDelayValidFrom'
        }

    def policy_attribute_names(self, include_global_only=False):
        """Return sorted password policy attribute names for CLI output."""
        names = sorted(set(self.arg_to_attr.values()))
        if include_global_only:
            for attr in GLOBAL_ONLY_POLICY_ATTRS:
                if attr not in names:
                    names.append(attr)
            names.sort()
        return names

    def _local_policy_attribute_names(self):
        """Return password policy attribute names stored on local policy entries."""
        return sorted(set(self.arg_to_attr.values()) | {'passwordstoragescheme'})

    def _report_attribute_names(self, report):
        """Return attribute names to emit for a policy report."""
        if report['policy_type'] == POLICY_TYPE_GLOBAL:
            return self.policy_attribute_names(include_global_only=True)
        local = []
        inherited = []
        for attr in sorted(report['attrs'].keys()):
            if report['attrs'][attr].get('inherited'):
                inherited.append(attr)
            else:
                local.append(attr)
        return local + inherited

    def resolve_user_dn(self, basedn, user_type, selector, parent_basedn=None):
        """Resolve a dsidm user selector to an entry DN."""
        if not selector:
            raise ValueError('A user identifier is required')
        accounts_class = USER_TYPE_ACCOUNTS.get(user_type or 'posix', nsUserAccounts)
        if parent_basedn is not None:
            accounts = accounts_class(self._instance, parent_basedn, rdn=None)
        else:
            accounts = accounts_class(self._instance, basedn)
        if not accounts.exists(selector):
            raise ValueError('User "{}" was not found'.format(selector))
        return accounts.get(selector).dn

    def get_effective_policy(self, basedn, user_type, selector, parent_basedn=None):
        """Resolve the effective password policy for a user account."""
        user_dn = self.resolve_user_dn(basedn, user_type, selector, parent_basedn)
        user_entry = Account(self._instance, user_dn)
        user_attrs = user_entry.get_all_attrs_utf8()
        if not user_attrs:
            raise ValueError('User entry "{}" was not found'.format(user_dn))

        if self._instance.config.get_attr_val_utf8_l('nsslapd-pwpolicy-local') == 'on':
            pwp_subentry_vals = user_attrs.get('pwdpolicysubentry', [])
            pwp_dn = pwp_subentry_vals[0] if pwp_subentry_vals else None
        else:
            pwp_dn = None

        global_attrs_raw = self._get_global_policy_attrs()

        if pwp_dn is None:
            return {
                'dn': user_dn,
                'policy_type': POLICY_TYPE_GLOBAL,
                'policy_dn': Config(self._instance).dn,
                'policy_target': 'global',
                'attrs': self._build_global_effective_attrs(global_attrs_raw),
            }

        pwp_entry = PwPolicyEntry(self._instance, pwp_dn)
        if not pwp_entry.exists():
            raise ValueError(
                'Assigned password policy entry "{}" could not be found'.format(pwp_dn))
        if not pwp_entry.present('objectClass', 'passwordpolicy'):
            raise ValueError(
                'Assigned password policy entry "{}" is not a valid password policy'.format(pwp_dn))

        policy_type, policy_dn, policy_target = _classify_policy_from_pwdpolicysubentry(
            pwp_dn, pwp_entry)
        policy_attrs_raw = _normalise_attrs(pwp_entry.get_all_attrs_utf8())
        effective = self._build_local_effective_attrs(policy_attrs_raw, global_attrs_raw)

        return {
            'dn': user_dn,
            'policy_type': policy_type,
            'policy_dn': policy_dn,
            'policy_target': policy_target,
            'attrs': effective,
        }

    def _get_global_policy_attrs(self):
        config = Config(self._instance)
        attr_names = self.policy_attribute_names(include_global_only=True)
        attrs = _normalise_attrs(config.get_attrs_vals_utf8(attr_names))
        # Some config attributes may be returned only under schema canonical names.
        all_attrs = _normalise_attrs(config.get_all_attrs_utf8())
        for name in attr_names:
            if name not in attrs or not attrs[name]:
                if name in all_attrs and all_attrs[name]:
                    attrs[name] = all_attrs[name]
        return attrs

    def _build_global_effective_attrs(self, global_attrs_raw):
        effective = {}
        for attr in self.policy_attribute_names(include_global_only=True):
            value = _first_attr_value(global_attrs_raw, attr, LOCAL_PW_POLICY_DEFAULTS.get(attr, ''))
            effective[attr] = _make_attr_entry(value, inherited=False)
        return effective

    def _build_local_effective_attrs(self, policy_attrs_raw, global_attrs_raw):
        """Build attrs for a local policy: explicit entry values plus inherited syntax."""
        policy_attrs_raw = _normalise_attrs(policy_attrs_raw)
        global_attrs_raw = _normalise_attrs(global_attrs_raw)
        effective = {}
        local_attrs = set()

        for attr in self._local_policy_attribute_names():
            if attr in policy_attrs_raw and policy_attrs_raw[attr]:
                effective[attr] = _make_attr_entry(
                    _first_attr_value(policy_attrs_raw, attr), inherited=False)
                local_attrs.add(attr)

        if 'passwordchecksyntax' in local_attrs:
            local_syntax_on = _attr_is_on(effective['passwordchecksyntax']['values'][0])
        else:
            local_syntax_on = False

        inherit_global = _attr_is_on(
            _first_attr_value(global_attrs_raw, 'nsslapd-pwpolicy-inherit-global', 'off'))
        global_syntax_on = _attr_is_on(
            _first_attr_value(global_attrs_raw, 'passwordchecksyntax', 'off'))

        if not local_syntax_on and inherit_global and global_syntax_on:
            for attr in SYNTAX_POLICY_ATTRS:
                if attr == 'passwordchecksyntax' or attr in local_attrs:
                    continue
                value = _first_attr_value(global_attrs_raw, attr, '')
                if not _should_show_inherited_value(value):
                    continue
                effective[attr] = _make_attr_entry(value, inherited=True)

        return effective

    def format_report_text(self, report):
        """Format an effective policy report for human-readable CLI output."""
        lines = [
            'dn: {}'.format(report['dn']),
            'passwordPolicy: {}'.format(report['policy_type']),
            'passwordPolicyDN: {}'.format(report['policy_dn']),
            'passwordPolicyTarget: {}'.format(report['policy_target']),
            '------------------- Policy Settings -------------------',
        ]
        for attr in self._report_attribute_names(report):
            item = report['attrs'][attr]
            value = item['values'][0] if item['values'] else ''
            suffix = ' (inherited)' if item.get('inherited') else ''
            lines.append('{}: {}{}'.format(attr, value, suffix))
        return '\n'.join(lines) + '\n'

    def format_report_json(self, report):
        """Format an effective policy report as a JSON-serializable dict."""
        attrs_out = {}
        for attr in self._report_attribute_names(report):
            item = report['attrs'][attr]
            values = list(item['values'])
            if item.get('inherited'):
                values.append('inherited')
            attrs_out[attr] = values
        return {
            'dn': report['dn'],
            'policy_type': report['policy_type'],
            'policy_dn': report['policy_dn'],
            'policy_target': report['policy_target'],
            'attrs': attrs_out,
        }

    def is_subtree_policy(self, dn):
        """Check if a subtree password policy exists for a given entry DN.

        A subtree policy is indicated by the presence of any CoS template
        (under `cn=nsPwPolicyContainer,<dn>`) that has a `pwdpolicysubentry`
        attribute pointing to an existing entry with objectClass `passwordpolicy`.

        :param dn: Entry DN to check for subtree policy
        :type dn: str

        :returns: True if a subtree policy exists, False otherwise
        :rtype: bool
        """
        try:
            container_basedn = 'cn=nsPwPolicyContainer,{}'.format(dn)
            templates = CosTemplates(self._instance, container_basedn).list()
            for tmpl in templates:
                pwp_dn = tmpl.get_attr_val_utf8('pwdpolicysubentry')
                if not pwp_dn:
                    continue
                # Validate that the referenced entry exists and is a passwordpolicy
                pwp_entry = PwPolicyEntry(self._instance, pwp_dn)
                if pwp_entry.exists() and pwp_entry.present('objectClass', 'passwordpolicy'):
                    return True
        except ldap.LDAPError:
            pass
        return False

    def is_user_policy(self, dn):
        """Check if the entry has a user password policy.

        A user policy is indicated by the target entry having a
        `pwdpolicysubentry` attribute that points to an existing
        entry with objectClass `passwordpolicy`.

        :param dn: Entry DN to check
        :type dn: str

        :returns: True if the entry has a user policy, False otherwise
        :rtype: bool
        """
        try:
            entry = Account(self._instance, dn)
            if not entry.exists():
                return False
            pwp_dn = entry.get_attr_val_utf8('pwdpolicysubentry')
            if not pwp_dn:
                return False
            pwp_entry = PwPolicyEntry(self._instance, pwp_dn)
            return pwp_entry.exists() and pwp_entry.present('objectClass', 'passwordpolicy')
        except ldap.LDAPError:
            return False

    def create_user_policy(self, dn, properties):
        """Creates all entries which are needed for the user
        password policy

        :param dn: Entry DN for the subtree pwpolicy
        :type dn: str
        :param properties: A dict with password policy settings
        :type properties: dict

        :returns: PwPolicyEntry instance
        """

        # Verify target dn exists before getting started
        user_entry = Account(self._instance, dn)
        if not user_entry.exists():
            raise ValueError('Can not create user password policy because the target dn does not exist')

        dn_comps = ldap.dn.explode_dn(user_entry.dn)
        dn_comps.pop(0)
        parentdn = ",".join(dn_comps)

        # Create the pwp container if needed
        pwp_containers = nsContainers(self._instance, basedn=parentdn)
        pwp_container = pwp_containers.ensure_state(properties={'cn': 'nsPwPolicyContainer'}, strict=True)

        # Create or update the policy entry
        properties['cn'] = 'cn=nsPwPolicyEntry_user,%s' % dn
        pwp_entries = PwPolicyEntries(self._instance, pwp_container.dn)
        pwp_entry = pwp_entries.ensure_state(properties=properties, strict=True)
        try:
            # Add policy to the entry if needed
            user_pwp_dn = user_entry.get_attr_val_utf8('pwdpolicysubentry')
            if user_pwp_dn != pwp_entry.dn:
                user_entry.replace('pwdpolicysubentry', pwp_entry.dn)
        except ldap.LDAPError as e:
            # failure, undo what we have done
            pwp_entry.delete()
            raise e

        # make sure that local policies are enabled
        self.set_global_policy({'nsslapd-pwpolicy-local': 'on'})

        return pwp_entry

    def create_subtree_policy(self, dn, properties):
        """Creates all entries which are needed for the subtree
        password policy

        :param dn: Entry DN for the subtree pwpolicy
        :type dn: str
        :param properties: A dict with password policy settings
        :type properties: dict

        :returns: PwPolicyEntry instance
        """

        # Verify target dn exists before getting started
        subtree_entry = Account(self._instance, dn)
        if not subtree_entry.exists():
            raise ValueError('Can not create subtree password policy because the target dn does not exist')

        # Create the pwp container if needed
        pwp_containers = nsContainers(self._instance, basedn=dn)
        pwp_container = pwp_containers.ensure_state(properties={'cn': 'nsPwPolicyContainer'}, strict=True)

        # Create or update the policy entry
        properties['cn'] = 'cn=nsPwPolicyEntry_subtree,%s' % dn
        pwp_entries = PwPolicyEntries(self._instance, pwp_container.dn)
        pwp_entry = pwp_entries.ensure_state(properties=properties, strict=True)

        # Ensure the CoS template entry (nsPwTemplateEntry) that points to the
        # password policy entry
        cos_templates = CosTemplates(self._instance, pwp_container.dn)
        cos_template = cos_templates.ensure_state(properties={
            'cosPriority': '1',
            'pwdpolicysubentry': pwp_entry.dn,
            'cn': 'cn=nsPwTemplateEntry,%s' % dn
        }, strict=True)

        # Ensure the CoS specification entry at the subtree level
        cos_pointer_defs = CosPointerDefinitions(self._instance, dn)
        cos_pointer_def = cos_pointer_defs.ensure_state(properties={
            'cosAttribute': 'pwdpolicysubentry default operational-default',
            'cosTemplateDn': cos_template.dn,
            'cn': 'nsPwPolicy_CoS'
        }, strict=True)

        # make sure that local policies are enabled
        self.set_global_policy({'nsslapd-pwpolicy-local': 'on'})

        # Capture the outcome of each ensure_state call
        ensure_status = [
            pwp_container.ensure_status,
            pwp_entry.ensure_status,
            cos_template.ensure_status,
            cos_pointer_def.ensure_status,
        ]

        # Determine overall status
        if all(s == DSLdapObject.ENSURE_UNCHANGED for s in ensure_status):
            pwp_entry._ensure_status = DSLdapObject.ENSURE_UNCHANGED
        elif all(s == DSLdapObject.ENSURE_ADDED for s in ensure_status):
            pwp_entry._ensure_status = DSLdapObject.ENSURE_ADDED
        else:
            # If any component was added/updated, consider it UPDATED (repair or modification)
            pwp_entry._ensure_status = DSLdapObject.ENSURE_UPDATED

        return pwp_entry

    def get_pwpolicy_entry(self, dn):
        """Get a local password policy entry

        :param dn: Entry DN for the local pwpolicy
        :type dn: str

        :returns: PwPolicyEntry instance
        """

        entry = Account(self._instance, dn)
        if not entry.exists():
            raise ValueError('Can not get the password policy entry because the target dn does not exist')

        try:
            Backends(self._instance).get(dn)
            # The DN is a base suffix, it has no parent
            parentdn = dn
        except:
            # Ok, this is a not a suffix, get the parent DN
            dn_comps = ldap.dn.explode_dn(entry.dn)
            dn_comps.pop(0)
            parentdn = ",".join(dn_comps)

        # Get the parent's policies
        pwp_entries = PwPolicyEntries(self._instance, parentdn)
        policies = pwp_entries.list()

        for policy in policies:
            # Sometimes, the cn value includes quotes (for example, after migration from pre-CLI version).
            # We need to strip them as python-ldap doesn't expect them
            dn_comps_str = policy.get_attr_val_utf8_l('cn').strip("\'").strip("\"")
            try:
                dn_comps = ldap.dn.explode_dn(dn_comps_str)
                dn_comps.pop(0)
                pwp_dn = ",".join(dn_comps)
                if pwp_dn == dn.lower():
                    # This DN does have a policy associated with it
                    return policy
            except ldap.DECODING_ERROR:
                # This is some kind of custom password policy
                continue

        # Did not find a policy for this entry
        raise ValueError("No password policy was found for this entry")

    def delete_local_policy(self, dn):
        """Delete a local password policy entry

        :param dn: Entry DN for the local pwpolicy
        :type dn: str
        """

        subtree = False

        # Verify target dn exists, and has a policy
        entry = Account(self._instance, dn)
        if not entry.exists():
            raise ValueError('The target entry dn does not exist')
        pwp_entry = self.get_pwpolicy_entry(entry.dn)

        # Subtree or user policy?
        if self.is_subtree_policy(entry.dn):
            parentdn = dn
            subtree = True
        elif self.is_user_policy(entry.dn):
            dn_comps = ldap.dn.explode_dn(dn)
            dn_comps.pop(0)
            parentdn = ",".join(dn_comps)
        else:
            raise ValueError('The target entry dn does not have a password policy')

        # Starting deleting the policy, ignore the parts that might already have been removed
        pwp_container = nsContainer(self._instance, 'cn=nsPwPolicyContainer,%s' % parentdn)
        if subtree:
            try:
                # Delete template
                cos_templates = CosTemplates(self._instance, pwp_container.dn)
                cos_template = cos_templates.get('cn=nsPwTemplateEntry,%s' % dn)
                cos_template.delete()
            except ldap.NO_SUCH_OBJECT:
                # Already deleted
                pass
            try:
                # Delete definition
                cos_pointer_def = CosPointerDefinition(self._instance, 'cn=nsPwPolicy_CoS,%s' % dn)
                cos_pointer_def.delete()
            except ldap.NO_SUCH_OBJECT:
                # Already deleted
                pass
        else:
            try:
                # Cleanup user entry
                entry.remove("pwdpolicysubentry", pwp_entry.dn)
            except ldap.NO_SUCH_ATTRIBUTE:
                # Policy already removed from user
                pass

        # Remove the local policy
        try:
            pwp_entry.delete()
        except ldap.NO_SUCH_OBJECT:
            # Already deleted
            pass

        try:
            pwp_container.delete()
        except (ldap.NOT_ALLOWED_ON_NONLEAF, ldap.NO_SUCH_OBJECT):
            # There are other policies still using this container, no problem
            pass

    def set_global_policy(self, properties):
        """Configure global password policy

        :param properties: A dictionary with password policy attributes
        :type properties: dict
        """

        modlist = []
        for attr, value in properties.items():
            modlist.append((attr, value))

        if len(modlist) > 0:
            config = Config(self._instance)
            config.replace_many(*modlist)
        else:
            raise ValueError("There are no password policies to set")


class PwPolicyEntry(DSLdapObject):
    """A single instance of a task entry

    :param instance: An instance
    :type instance: lib389.DirSrv
    :param dn: Entry DN
    :type dn: str
    """

    def __init__(self, instance, dn):
        super(PwPolicyEntry, self).__init__(instance, dn)
        self._rdn_attribute = 'cn'
        self._must_attributes = ['cn']
        self._create_objectclasses = ['top', 'ldapsubentry', 'passwordpolicy']
        self._protected = False

    def is_user_policy(self):
        """Check if the policy entry is a user password policy"""

        pwp_manager = PwPolicyManager(self._instance)
        cn = self.get_attr_val_utf8_l('cn')
        entrydn = cn.replace('cn=nspwpolicyentry_user,', '')

        return pwp_manager.is_user_policy(entrydn)

    def is_subtree_policy(self):
        """Check if the policy entry is a user password policy"""

        pwp_manager = PwPolicyManager(self._instance)
        cn = self.get_attr_val_utf8_l('cn')
        entrydn = cn.replace('cn=nspwpolicyentry_subtree,', '')

        return pwp_manager.is_subtree_policy(entrydn)


class PwPolicyEntries(DSLdapObjects):
    """DSLdapObjects that represents all password policy entries in container.

    :param instance: An instance
    :type instance: lib389.DirSrv
    :param basedn: Suffix DN
    :type basedn: str
    :param rdn: The DN that will be combined wit basedn
    :type rdn: str
    """

    def __init__(self, instance, basedn):
        super(PwPolicyEntries, self).__init__(instance)
        self._objectclasses = [
            'top',
            'ldapsubentry',
            'passwordpolicy'
        ]
        self._filterattrs = ['cn']
        self._childobject = PwPolicyEntry
        self._basedn = basedn

