#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This is a port of the original in testprogs/ejs/ldap.js

import optparse
import sys
import os
import time

sys.path.insert(0, "bin/python")
import samba
from samba.hresult import HRES_SEC_E_INVALID_TOKEN, HRES_SEC_E_LOGON_DENIED
from samba.tests.subunitrun import SubunitOptions, TestProgram

import samba.getopt as options

from samba.credentials import Credentials, DONT_USE_KERBEROS
from samba.auth import system_session
from samba.common import get_string

from ldb import SCOPE_BASE, LdbError
from ldb import ERR_NO_SUCH_OBJECT, ERR_ATTRIBUTE_OR_VALUE_EXISTS
from ldb import ERR_ENTRY_ALREADY_EXISTS, ERR_UNWILLING_TO_PERFORM
from ldb import ERR_OTHER, ERR_NO_SUCH_ATTRIBUTE
from ldb import ERR_OBJECT_CLASS_VIOLATION
from ldb import ERR_CONSTRAINT_VIOLATION
from ldb import ERR_UNDEFINED_ATTRIBUTE_TYPE
from ldb import ERR_INSUFFICIENT_ACCESS_RIGHTS
from ldb import ERR_INVALID_CREDENTIALS
from ldb import ERR_STRONG_AUTH_REQUIRED
from ldb import Message, MessageElement, Dn
from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
from samba.samdb import SamDB
from samba.dsdb import (UF_NORMAL_ACCOUNT, UF_ACCOUNTDISABLE,
                        UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
                        UF_PARTIAL_SECRETS_ACCOUNT, UF_TEMP_DUPLICATE_ACCOUNT,
                        UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SMARTCARD_REQUIRED,
                        UF_PASSWD_NOTREQD, UF_LOCKOUT, UF_PASSWORD_EXPIRED, ATYPE_NORMAL_ACCOUNT,
                        GTYPE_SECURITY_BUILTIN_LOCAL_GROUP, GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
                        GTYPE_SECURITY_GLOBAL_GROUP, GTYPE_SECURITY_UNIVERSAL_GROUP,
                        GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP, GTYPE_DISTRIBUTION_GLOBAL_GROUP,
                        GTYPE_DISTRIBUTION_UNIVERSAL_GROUP,
                        ATYPE_SECURITY_GLOBAL_GROUP, ATYPE_SECURITY_UNIVERSAL_GROUP,
                        ATYPE_SECURITY_LOCAL_GROUP, ATYPE_DISTRIBUTION_GLOBAL_GROUP,
                        ATYPE_DISTRIBUTION_UNIVERSAL_GROUP, ATYPE_DISTRIBUTION_LOCAL_GROUP,
                        ATYPE_WORKSTATION_TRUST)
from samba.dcerpc.security import (DOMAIN_RID_USERS, DOMAIN_RID_ADMINS,
                                   DOMAIN_RID_DOMAIN_MEMBERS, DOMAIN_RID_DCS, DOMAIN_RID_READONLY_DCS)

from samba.ndr import ndr_unpack
from samba.dcerpc import drsblobs
from samba.dcerpc import drsuapi
from samba.dcerpc import security
from samba.tests import delete_force
from samba import gensec
from samba import werror

parser = optparse.OptionParser("sam.py [options] <host>")
sambaopts = options.SambaOptions(parser)
parser.add_option_group(sambaopts)
parser.add_option_group(options.VersionOptions(parser))
# use command line creds if available
credopts = options.CredentialsOptions(parser)
parser.add_option_group(credopts)
subunitopts = SubunitOptions(parser)
parser.add_option_group(subunitopts)
opts, args = parser.parse_args()

if len(args) < 1:
    parser.print_usage()
    sys.exit(1)

host = args[0]

lp = sambaopts.get_loadparm()
creds = credopts.get_credentials(lp)
creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)


class SamTests(samba.tests.TestCase):

    def setUp(self):
        super(SamTests, self).setUp()
        self.ldb = ldb
        self.base_dn = ldb.domain_dn()

        print("baseDN: %s\n" % self.base_dn)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
        delete_force(self.ldb, r"cn=ldaptest\,specialuser,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestcomputer2,cn=computers," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)

    def test_users_groups(self):
        """This tests the SAM users and groups behaviour"""
        print("Testing users and groups behaviour\n")

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group"})

        ldb.add({
            "dn": "cn=ldaptestgroup2,cn=users," + self.base_dn,
            "objectclass": "group"})

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["objectSID"])
        self.assertTrue(len(res1) == 1)
        obj_sid = get_string(ldb.schema_format_value("objectSID",
                                                     res1[0]["objectSID"][0]))
        group_rid_1 = security.dom_sid(obj_sid).split()[1]

        res1 = ldb.search("cn=ldaptestgroup2,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["objectSID"])
        self.assertTrue(len(res1) == 1)
        obj_sid = get_string(ldb.schema_format_value("objectSID",
                                                     res1[0]["objectSID"][0]))
        group_rid_2 = security.dom_sid(obj_sid).split()[1]

        # Try to create a user with an invalid account name
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "sAMAccountName": "administrator"})
            self.fail()
        except LdbError as e9:
            (num, _) = e9.args
            self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # Try to create a user with an invalid account name
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "sAMAccountName": []})
            self.fail()
        except LdbError as e10:
            (num, _) = e10.args
            self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # Try to create a user with an invalid primary group
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "primaryGroupID": "0"})
            self.fail()
        except LdbError as e11:
            (num, _) = e11.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # Try to Create a user with a valid primary group
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "primaryGroupID": str(group_rid_1)})
            self.fail()
        except LdbError as e12:
            (num, _) = e12.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # Test to see how we should behave when the user account doesn't
        # exist
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement("0", FLAG_MOD_REPLACE,
                                             "primaryGroupID")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e13:
            (num, _) = e13.args
            self.assertEqual(num, ERR_NO_SUCH_OBJECT)

        # Test to see how we should behave when the account isn't a user
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement("0", FLAG_MOD_REPLACE,
                                             "primaryGroupID")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e14:
            (num, _) = e14.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)

        # Test default primary groups on add operations

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD)})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # unfortunately the INTERDOMAIN_TRUST_ACCOUNT case cannot be tested
        # since such accounts aren't directly creatable (ACCESS_DENIED)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT |
                                      UF_PASSWD_NOTREQD)})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]),
                          DOMAIN_RID_DOMAIN_MEMBERS)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT |
                                      UF_PASSWD_NOTREQD)})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_DCS)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # Read-only DC accounts are only creatable by
        # UF_WORKSTATION_TRUST_ACCOUNT and work only on DCs >= 2008 (therefore
        # we have a fallback in the assertion)
        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_PARTIAL_SECRETS_ACCOUNT |
                                      UF_WORKSTATION_TRUST_ACCOUNT |
                                      UF_PASSWD_NOTREQD)})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertTrue(int(res1[0]["primaryGroupID"][0]) == DOMAIN_RID_READONLY_DCS or
                        int(res1[0]["primaryGroupID"][0]) == DOMAIN_RID_DOMAIN_MEMBERS)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # Test default primary groups on modify operations

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_NORMAL_ACCOUNT |
                                                     UF_PASSWD_NOTREQD),
                                                 FLAG_MOD_REPLACE,
                                                 "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)

        # unfortunately the INTERDOMAIN_TRUST_ACCOUNT case cannot be tested
        # since such accounts aren't directly creatable (ACCESS_DENIED)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_NORMAL_ACCOUNT |
                                      UF_PASSWD_NOTREQD)})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT |
                                                     UF_PASSWD_NOTREQD),
                                                 FLAG_MOD_REPLACE,
                                                 "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_DOMAIN_MEMBERS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_SERVER_TRUST_ACCOUNT |
                                                     UF_PASSWD_NOTREQD),
                                                 FLAG_MOD_REPLACE,
                                                 "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_DCS)

        # Read-only DC accounts are only creatable by
        # UF_WORKSTATION_TRUST_ACCOUNT and work only on DCs >= 2008 (therefore
        # we have a fallback in the assertion)
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_PARTIAL_SECRETS_ACCOUNT |
                                                     UF_WORKSTATION_TRUST_ACCOUNT |
                                                     UF_PASSWD_NOTREQD),
                                                 FLAG_MOD_REPLACE,
                                                 "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertTrue(int(res1[0]["primaryGroupID"][0]) == DOMAIN_RID_READONLY_DCS or
                        int(res1[0]["primaryGroupID"][0]) == DOMAIN_RID_DOMAIN_MEMBERS)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # Recreate account for further tests

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        # Try to set an invalid account name
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["sAMAccountName"] = MessageElement("administrator", FLAG_MOD_REPLACE,
                                             "sAMAccountName")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e15:
            (num, _) = e15.args
            self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)

        # But to reset the actual "sAMAccountName" should still be possible
        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountName"])
        self.assertTrue(len(res1) == 1)
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["sAMAccountName"] = MessageElement(res1[0]["sAMAccountName"][0], FLAG_MOD_REPLACE,
                                             "sAMAccountName")
        ldb.modify(m)

        # And another (free) name should be possible as well
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["sAMAccountName"] = MessageElement("xxx_ldaptestuser_xxx", FLAG_MOD_REPLACE,
                                             "sAMAccountName")
        ldb.modify(m)

        # We should be able to reset our actual primary group
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement(str(DOMAIN_RID_USERS), FLAG_MOD_REPLACE,
                                             "primaryGroupID")
        ldb.modify(m)

        # Try to add invalid primary group
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement("0", FLAG_MOD_REPLACE,
                                             "primaryGroupID")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e16:
            (num, _) = e16.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Try to make group 1 primary - should be denied since it is not yet
        # secondary
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement(str(group_rid_1),
                                             FLAG_MOD_REPLACE, "primaryGroupID")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e17:
            (num, _) = e17.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Make group 1 secondary
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
                                     FLAG_MOD_REPLACE, "member")
        ldb.modify(m)

        # Make group 1 primary
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement(str(group_rid_1),
                                             FLAG_MOD_REPLACE, "primaryGroupID")
        ldb.modify(m)

        # Try to delete group 1 - should be denied
        try:
            ldb.delete("cn=ldaptestgroup,cn=users," + self.base_dn)
            self.fail()
        except LdbError as e18:
            (num, _) = e18.args
            self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)

        # Try to add group 1 also as secondary - should be denied
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
                                     FLAG_MOD_ADD, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e19:
            (num, _) = e19.args
            self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)

        # Try to add invalid member to group 1 - should be denied
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["member"] = MessageElement(
            "cn=ldaptestuser3,cn=users," + self.base_dn,
            FLAG_MOD_ADD, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e20:
            (num, _) = e20.args
            self.assertEqual(num, ERR_NO_SUCH_OBJECT)

        # Make group 2 secondary
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
                                     FLAG_MOD_ADD, "member")
        ldb.modify(m)

        # Swap the groups
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement(str(group_rid_2),
                                             FLAG_MOD_REPLACE, "primaryGroupID")
        ldb.modify(m)

        # Swap the groups (does not really make sense but does the same)
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement(str(group_rid_1),
                                             FLAG_MOD_REPLACE, "primaryGroupID")
        m["primaryGroupID"] = MessageElement(str(group_rid_2),
                                             FLAG_MOD_REPLACE, "primaryGroupID")
        ldb.modify(m)

        # Old primary group should contain a "member" attribute for the user,
        # the new shouldn't contain anymore one
        res1 = ldb.search("cn=ldaptestgroup, cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["member"])
        self.assertTrue(len(res1) == 1)
        self.assertTrue(len(res1[0]["member"]) == 1)
        self.assertEqual(str(res1[0]["member"][0]).lower(),
                          ("cn=ldaptestuser,cn=users," + self.base_dn).lower())

        res1 = ldb.search("cn=ldaptestgroup2, cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["member"])
        self.assertTrue(len(res1) == 1)
        self.assertFalse("member" in res1[0])

        # Primary group member
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
                                     FLAG_MOD_DELETE, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e21:
            (num, _) = e21.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Delete invalid group member
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser1,cn=users," + self.base_dn,
                                     FLAG_MOD_DELETE, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e22:
            (num, _) = e22.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Also this should be denied
        try:
            ldb.add({
                "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
                "objectclass": "user",
                "primaryGroupID": "0"})
            self.fail()
        except LdbError as e23:
            (num, _) = e23.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Recreate user accounts

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        ldb.add({
            "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
            "objectclass": "user"})

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
                                     FLAG_MOD_ADD, "member")
        ldb.modify(m)

        # Already added
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
                                     FLAG_MOD_ADD, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e24:
            (num, _) = e24.args
            self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)

        # Already added, but as <SID=...>
        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["objectSid"])
        self.assertTrue(len(res1) == 1)
        sid_bin = res1[0]["objectSid"][0]
        sid_str = ("<SID=" + get_string(ldb.schema_format_value("objectSid", sid_bin)) + ">").upper()

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement(sid_str, FLAG_MOD_ADD, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e25:
            (num, _) = e25.args
            self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)

        # Invalid member
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser1,cn=users," + self.base_dn,
                                     FLAG_MOD_REPLACE, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e26:
            (num, _) = e26.args
            self.assertEqual(num, ERR_NO_SUCH_OBJECT)

        # Invalid member
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement(["cn=ldaptestuser,cn=users," + self.base_dn,
                                      "cn=ldaptestuser1,cn=users," + self.base_dn],
                                     FLAG_MOD_REPLACE, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e27:
            (num, _) = e27.args
            self.assertEqual(num, ERR_NO_SUCH_OBJECT)

        # Invalid member
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
                                     FLAG_MOD_REPLACE, "member")
        m["member"] = MessageElement("cn=ldaptestuser1,cn=users," + self.base_dn,
                                     FLAG_MOD_ADD, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e28:
            (num, _) = e28.args
            self.assertEqual(num, ERR_NO_SUCH_OBJECT)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
        m["member"] = MessageElement(["cn=ldaptestuser,cn=users," + self.base_dn,
                                      "cn=ldaptestuser2,cn=users," + self.base_dn],
                                     FLAG_MOD_REPLACE, "member")
        ldb.modify(m)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)

        # Make also a small test for accounts with special DNs ("," in this case)
        ldb.add({
            "dn": r"cn=ldaptest\,specialuser,cn=users," + self.base_dn,
            "objectclass": "user"})
        delete_force(self.ldb, r"cn=ldaptest\,specialuser,cn=users," + self.base_dn)

    def test_sam_attributes(self):
        """Test the behaviour of special attributes of SAM objects"""
        print("Testing the behaviour of special attributes of SAM objects\n")

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})
        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group"})

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(str(GTYPE_SECURITY_GLOBAL_GROUP), FLAG_MOD_ADD,
                                        "groupType")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e29:
            (num, _) = e29.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        # Delete protection tests

        for attr in ["nTSecurityDescriptor", "objectSid", "sAMAccountType",
                     "sAMAccountName", "groupType"]:

            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m[attr] = MessageElement([], FLAG_MOD_REPLACE, attr)
            try:
                ldb.modify(m)
                self.fail()
            except LdbError as e:
                (num, _) = e.args
                self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m[attr] = MessageElement([], FLAG_MOD_DELETE, attr)
            try:
                ldb.modify(m)
                self.fail()
            except LdbError as e1:
                (num, _) = e1.args
                self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement("513", FLAG_MOD_ADD,
                                             "primaryGroupID")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e30:
            (num, _) = e30.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_NORMAL_ACCOUNT |
                                                     UF_PASSWD_NOTREQD),
                                                 FLAG_MOD_ADD,
                                                 "userAccountControl")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e31:
            (num, _) = e31.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["objectSid"] = MessageElement("xxxxxxxxxxxxxxxx", FLAG_MOD_ADD,
                                        "objectSid")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e32:
            (num, _) = e32.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["sAMAccountType"] = MessageElement("0", FLAG_MOD_ADD,
                                             "sAMAccountType")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e33:
            (num, _) = e33.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["sAMAccountName"] = MessageElement("test", FLAG_MOD_ADD,
                                             "sAMAccountName")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e34:
            (num, _) = e34.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        # Delete protection tests

        for attr in ["nTSecurityDescriptor", "objectSid", "sAMAccountType",
                     "sAMAccountName", "primaryGroupID", "userAccountControl",
                     "accountExpires", "badPasswordTime", "badPwdCount",
                     "codePage", "countryCode", "lastLogoff", "lastLogon",
                     "logonCount", "pwdLastSet"]:

            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m[attr] = MessageElement([], FLAG_MOD_REPLACE, attr)
            try:
                ldb.modify(m)
                self.fail()
            except LdbError as e2:
                (num, _) = e2.args
                self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m[attr] = MessageElement([], FLAG_MOD_DELETE, attr)
            try:
                ldb.modify(m)
                self.fail()
            except LdbError as e3:
                (num, _) = e3.args
                self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

    def test_primary_group_token_constructed(self):
        """Test the primary group token behaviour (hidden-generated-readonly attribute on groups) and some other constructed attributes"""
        print("Testing primary group token behaviour and other constructed attributes\n")

        try:
            ldb.add({
                "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
                "objectclass": "group",
                "primaryGroupToken": "100"})
            self.fail()
        except LdbError as e35:
            (num, _) = e35.args
            self.assertEqual(num, ERR_UNDEFINED_ATTRIBUTE_TYPE)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group"})

        # Testing for one invalid, and one valid operational attribute, but also the things they are built from
        res1 = ldb.search(self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupToken", "canonicalName", "objectClass", "objectSid"])
        self.assertTrue(len(res1) == 1)
        self.assertFalse("primaryGroupToken" in res1[0])
        self.assertTrue("canonicalName" in res1[0])
        self.assertTrue("objectClass" in res1[0])
        self.assertTrue("objectSid" in res1[0])

        res1 = ldb.search(self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupToken", "canonicalName"])
        self.assertTrue(len(res1) == 1)
        self.assertFalse("primaryGroupToken" in res1[0])
        self.assertFalse("objectSid" in res1[0])
        self.assertFalse("objectClass" in res1[0])
        self.assertTrue("canonicalName" in res1[0])

        res1 = ldb.search("cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupToken"])
        self.assertTrue(len(res1) == 1)
        self.assertFalse("primaryGroupToken" in res1[0])

        res1 = ldb.search("cn=ldaptestuser, cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupToken"])
        self.assertTrue(len(res1) == 1)
        self.assertFalse("primaryGroupToken" in res1[0])

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE)
        self.assertTrue(len(res1) == 1)
        self.assertFalse("primaryGroupToken" in res1[0])

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["primaryGroupToken", "objectSID"])
        self.assertTrue(len(res1) == 1)
        primary_group_token = int(res1[0]["primaryGroupToken"][0])

        obj_sid = get_string(ldb.schema_format_value("objectSID", res1[0]["objectSID"][0]))
        rid = security.dom_sid(obj_sid).split()[1]
        self.assertEqual(primary_group_token, rid)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["primaryGroupToken"] = "100"
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e36:
            (num, _) = e36.args
            self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

    def test_tokenGroups(self):
        """Test the tokenGroups behaviour (hidden-generated-readonly attribute on SAM objects)"""
        print("Testing tokenGroups behaviour\n")

        # The domain object shouldn't contain any "tokenGroups" entry
        res = ldb.search(self.base_dn, scope=SCOPE_BASE, attrs=["tokenGroups"])
        self.assertTrue(len(res) == 1)
        self.assertFalse("tokenGroups" in res[0])

        # The domain administrator should contain "tokenGroups" entries
        # (the exact number depends on the domain/forest function level and the
        # DC software versions)
        res = ldb.search("cn=Administrator,cn=Users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["tokenGroups"])
        self.assertTrue(len(res) == 1)
        self.assertTrue("tokenGroups" in res[0])

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        # This testuser should contain at least two "tokenGroups" entries
        # (exactly two on an unmodified "Domain Users" and "Users" group)
        res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["tokenGroups"])
        self.assertTrue(len(res) == 1)
        self.assertTrue(len(res[0]["tokenGroups"]) >= 2)

        # one entry which we need to find should point to domains "Domain Users"
        # group and another entry should point to the builtin "Users"group
        domain_users_group_found = False
        users_group_found = False
        for sid in res[0]["tokenGroups"]:
            obj_sid = get_string(ldb.schema_format_value("objectSID", sid))
            rid = security.dom_sid(obj_sid).split()[1]
            if rid == 513:
                domain_users_group_found = True
            if rid == 545:
                users_group_found = True

        self.assertTrue(domain_users_group_found)
        self.assertTrue(users_group_found)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_groupType(self):
        """Test the groupType behaviour"""
        print("Testing groupType behaviour\n")

        # You can never create or change to a
        # "GTYPE_SECURITY_BUILTIN_LOCAL_GROUP"

        # Add operation

        # Invalid attribute
        try:
            ldb.add({
                "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
                "objectclass": "group",
                "groupType": "0"})
            self.fail()
        except LdbError as e37:
            (num, _) = e37.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        try:
            ldb.add({
                "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
                "objectclass": "group",
                "groupType": str(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP)})
            self.fail()
        except LdbError as e38:
            (num, _) = e38.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "groupType": str(GTYPE_SECURITY_GLOBAL_GROUP)})

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_GLOBAL_GROUP)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "groupType": str(GTYPE_SECURITY_UNIVERSAL_GROUP)})

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_UNIVERSAL_GROUP)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "groupType": str(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)})

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_LOCAL_GROUP)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "groupType": str(GTYPE_DISTRIBUTION_GLOBAL_GROUP)})

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_GLOBAL_GROUP)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "groupType": str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP)})

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "groupType": str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)})

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_LOCAL_GROUP)
        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        # Modify operation

        ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group"})

        # We can change in this direction: global <-> universal <-> local
        # On each step also the group type itself (security/distribution) is
        # variable.

        # After creation we should have a "security global group"
        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_GLOBAL_GROUP)

        # Invalid attribute
        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement("0",
                                            FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e39:
            (num, _) = e39.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Security groups

        # Default is "global group"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_GLOBAL_GROUP)

        # Change to "local" (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e40:
            (num, _) = e40.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Change to "universal"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_UNIVERSAL_GROUP)

        # Change back to "global"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_GLOBAL_GROUP)

        # Change back to "universal"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_UNIVERSAL_GROUP)

        # Change to "local"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_LOCAL_GROUP)

        # Change to "global" (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_SECURITY_GLOBAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e41:
            (num, _) = e41.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Change to "builtin local" (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e42:
            (num, _) = e42.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        # Change back to "universal"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_UNIVERSAL_GROUP)

        # Change to "builtin local" (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e43:
            (num, _) = e43.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Change back to "global"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_GLOBAL_GROUP)

        # Change to "builtin local" (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e44:
            (num, _) = e44.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Distribution groups

        # Default is "global group"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_GLOBAL_GROUP)

        # Change to local (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e45:
            (num, _) = e45.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Change to "universal"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)

        # Change back to "global"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_GLOBAL_GROUP)

        # Change back to "universal"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)

        # Change to "local"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_LOCAL_GROUP)

        # Change to "global" (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e46:
            (num, _) = e46.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Change back to "universal"

        # Try to add invalid member to group 1 - should be denied
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["member"] = MessageElement(
            "cn=ldaptestuser3,cn=users," + self.base_dn,
            FLAG_MOD_ADD, "member")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e47:
            (num, _) = e47.args
            self.assertEqual(num, ERR_NO_SUCH_OBJECT)

        # Make group 2 secondary
        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)

        # Change back to "global"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_GLOBAL_GROUP)

        # Both group types: this performs only random checks - all possibilities
        # would require too much code.

        # Default is "global group"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_GLOBAL_GROUP)

        # Change to "local" (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e48:
            (num, _) = e48.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Change to "universal"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)

        # Change back to "global"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_GLOBAL_GROUP)

        # Change back to "universal"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_UNIVERSAL_GROUP)

        # Change to "local"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_DISTRIBUTION_LOCAL_GROUP)

        # Change to "global" (shouldn't work)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
            m["groupType"] = MessageElement(
                str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
                FLAG_MOD_REPLACE, "groupType")
            ldb.modify(m)
            self.fail()
        except LdbError as e49:
            (num, _) = e49.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # Change back to "universal"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_UNIVERSAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_UNIVERSAL_GROUP)

        # Change back to "global"

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["groupType"] = MessageElement(
            str(GTYPE_SECURITY_GLOBAL_GROUP),
            FLAG_MOD_REPLACE, "groupType")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_SECURITY_GLOBAL_GROUP)

        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

    def test_pwdLastSet(self):
        """Test the pwdLastSet behaviour"""
        print("Testing pwdLastSet behaviour\n")

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "pwdLastSet": "0"})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res1[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
        self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "pwdLastSet": "-1"})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res1[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
        self.assertNotEqual(int(res1[0]["pwdLastSet"][0]), 0)
        lastset = int(res1[0]["pwdLastSet"][0])
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "pwdLastSet": str(1)})
            self.fail()
        except LdbError as e50:
            (num, msg) = e50.args
            self.assertEqual(num, ERR_OTHER)
            self.assertTrue('00000057' in msg)

        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "pwdLastSet": str(lastset)})
            self.fail()
        except LdbError as e51:
            (num, msg) = e51.args
            self.assertEqual(num, ERR_OTHER)
            self.assertTrue('00000057' in msg)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res1[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
        self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["pls1"] = MessageElement(str(0),
                                   FLAG_MOD_REPLACE,
                                   "pwdLastSet")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["pls1"] = MessageElement(str(0),
                                   FLAG_MOD_DELETE,
                                   "pwdLastSet")
        m["pls2"] = MessageElement(str(0),
                                   FLAG_MOD_ADD,
                                   "pwdLastSet")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["pls1"] = MessageElement(str(-1),
                                   FLAG_MOD_REPLACE,
                                   "pwdLastSet")
        ldb.modify(m)
        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res1[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
        self.assertGreater(int(res1[0]["pwdLastSet"][0]), lastset)
        lastset = int(res1[0]["pwdLastSet"][0])

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["pls1"] = MessageElement(str(0),
                                       FLAG_MOD_DELETE,
                                       "pwdLastSet")
            m["pls2"] = MessageElement(str(0),
                                       FLAG_MOD_ADD,
                                       "pwdLastSet")
            ldb.modify(m)
            self.fail()
        except LdbError as e52:
            (num, msg) = e52.args
            self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
            self.assertTrue('00002085' in msg)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["pls1"] = MessageElement(str(-1),
                                       FLAG_MOD_DELETE,
                                       "pwdLastSet")
            m["pls2"] = MessageElement(str(0),
                                       FLAG_MOD_ADD,
                                       "pwdLastSet")
            ldb.modify(m)
            self.fail()
        except LdbError as e53:
            (num, msg) = e53.args
            self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
            self.assertTrue('00002085' in msg)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["pls1"] = MessageElement(str(lastset),
                                   FLAG_MOD_DELETE,
                                   "pwdLastSet")
        m["pls2"] = MessageElement(str(-1),
                                   FLAG_MOD_ADD,
                                   "pwdLastSet")
        time.sleep(0.2)
        ldb.modify(m)
        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res1[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
        self.assertEqual(int(res1[0]["pwdLastSet"][0]), lastset)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["pls1"] = MessageElement(str(lastset),
                                       FLAG_MOD_DELETE,
                                       "pwdLastSet")
            m["pls2"] = MessageElement(str(lastset),
                                       FLAG_MOD_ADD,
                                       "pwdLastSet")
            ldb.modify(m)
            self.fail()
        except LdbError as e54:
            (num, msg) = e54.args
            self.assertEqual(num, ERR_OTHER)
            self.assertTrue('00000057' in msg)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["pls1"] = MessageElement(str(lastset),
                                   FLAG_MOD_DELETE,
                                   "pwdLastSet")
        m["pls2"] = MessageElement(str(0),
                                   FLAG_MOD_ADD,
                                   "pwdLastSet")
        ldb.modify(m)
        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res1[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
        uac = int(res1[0]["userAccountControl"][0])
        self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["uac1"] = MessageElement(str(uac |UF_PASSWORD_EXPIRED),
                                   FLAG_MOD_REPLACE,
                                   "userAccountControl")
        ldb.modify(m)
        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res1[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
        self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_ldap_bind_must_change_pwd(self):
        """Test the error messages for failing LDAP binds"""
        print("Test the error messages for failing LDAP binds\n")

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        def format_error_msg(hresult_v, dsid_v, werror_v):
            #
            # There are 4 lower case hex digits following 'v' at the end,
            # but different Windows Versions return different values:
            #
            # Windows 2008R2 uses 'v1db1'
            # Windows 2012R2 uses 'v2580'
            #
            return "%08X: LdapErr: DSID-%08X, comment: AcceptSecurityContext error, data %x, v" % (
                    hresult_v, dsid_v, werror_v)

        sasl_bind_dsid = 0x0C0904DC
        simple_bind_dsid = 0x0C0903A9

        error_msg_sasl_wrong_pw = format_error_msg(
                                HRES_SEC_E_LOGON_DENIED,
                                sasl_bind_dsid,
                                werror.WERR_LOGON_FAILURE)
        error_msg_sasl_must_change = format_error_msg(
                                HRES_SEC_E_LOGON_DENIED,
                                sasl_bind_dsid,
                                werror.WERR_PASSWORD_MUST_CHANGE)
        error_msg_simple_wrong_pw = format_error_msg(
                                HRES_SEC_E_INVALID_TOKEN,
                                simple_bind_dsid,
                                werror.WERR_LOGON_FAILURE)
        error_msg_simple_must_change = format_error_msg(
                                HRES_SEC_E_INVALID_TOKEN,
                                simple_bind_dsid,
                                werror.WERR_PASSWORD_MUST_CHANGE)

        username = "ldaptestuser"
        password = "thatsAcomplPASS2"
        utf16pw = ('"' + password + '"').encode('utf-16-le')

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "sAMAccountName": username,
            "userAccountControl": str(UF_NORMAL_ACCOUNT),
            "unicodePwd": utf16pw,
        })

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountName", "sAMAccountType", "userAccountControl", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["sAMAccountName"][0]), username)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]), ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res1[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT)
        self.assertNotEqual(int(res1[0]["pwdLastSet"][0]), 0)

        # Open a second LDB connection with the user credentials. Use the
        # command line credentials for information like the domain, the realm
        # and the workstation.
        sasl_creds = Credentials()
        sasl_creds.set_username(username)
        sasl_creds.set_password(password)
        sasl_creds.set_domain(creds.get_domain())
        sasl_creds.set_workstation(creds.get_workstation())
        sasl_creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
        sasl_creds.set_kerberos_state(DONT_USE_KERBEROS)

        sasl_wrong_creds = Credentials()
        sasl_wrong_creds.set_username(username)
        sasl_wrong_creds.set_password("wrong")
        sasl_wrong_creds.set_domain(creds.get_domain())
        sasl_wrong_creds.set_workstation(creds.get_workstation())
        sasl_wrong_creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
        sasl_wrong_creds.set_kerberos_state(DONT_USE_KERBEROS)

        simple_creds = Credentials()
        simple_creds.set_bind_dn("cn=ldaptestuser,cn=users," + self.base_dn)
        simple_creds.set_username(username)
        simple_creds.set_password(password)
        simple_creds.set_domain(creds.get_domain())
        simple_creds.set_workstation(creds.get_workstation())
        simple_creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
        simple_creds.set_kerberos_state(DONT_USE_KERBEROS)

        simple_wrong_creds = Credentials()
        simple_wrong_creds.set_bind_dn("cn=ldaptestuser,cn=users," + self.base_dn)
        simple_wrong_creds.set_username(username)
        simple_wrong_creds.set_password("wrong")
        simple_wrong_creds.set_domain(creds.get_domain())
        simple_wrong_creds.set_workstation(creds.get_workstation())
        simple_wrong_creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
        simple_wrong_creds.set_kerberos_state(DONT_USE_KERBEROS)

        sasl_ldb = SamDB(url=host, credentials=sasl_creds, lp=lp)
        self.assertIsNotNone(sasl_ldb)
        del sasl_ldb

        requires_strong_auth = False
        try:
            simple_ldb = SamDB(url=host, credentials=simple_creds, lp=lp)
            self.assertIsNotNone(simple_ldb)
            del simple_ldb
        except LdbError as e55:
            (num, msg) = e55.args
            if num != ERR_STRONG_AUTH_REQUIRED:
                raise
            requires_strong_auth = True

        def assertLDAPErrorMsg(msg, expected_msg):
            self.assertTrue(expected_msg in msg,
                            "msg[%s] does not contain expected[%s]" % (
                                msg, expected_msg))

        try:
            SamDB(url=host, credentials=sasl_wrong_creds, lp=lp)
            self.fail()
        except LdbError as e56:
            (num, msg) = e56.args
            self.assertEqual(num, ERR_INVALID_CREDENTIALS)
            self.assertTrue(error_msg_sasl_wrong_pw in msg)

        if not requires_strong_auth:
            try:
                SamDB(url=host, credentials=simple_wrong_creds, lp=lp)
                self.fail()
            except LdbError as e4:
                (num, msg) = e4.args
                self.assertEqual(num, ERR_INVALID_CREDENTIALS)
                assertLDAPErrorMsg(msg, error_msg_simple_wrong_pw)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["pls1"] = MessageElement(str(0),
                                   FLAG_MOD_REPLACE,
                                   "pwdLastSet")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["pwdLastSet"])
        self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)

        try:
            SamDB(url=host, credentials=sasl_wrong_creds, lp=lp)
            self.fail()
        except LdbError as e57:
            (num, msg) = e57.args
            self.assertEqual(num, ERR_INVALID_CREDENTIALS)
            assertLDAPErrorMsg(msg, error_msg_sasl_wrong_pw)

        try:
            SamDB(url=host, credentials=sasl_creds, lp=lp)
            self.fail()
        except LdbError as e58:
            (num, msg) = e58.args
            self.assertEqual(num, ERR_INVALID_CREDENTIALS)
            assertLDAPErrorMsg(msg, error_msg_sasl_must_change)

        if not requires_strong_auth:
            try:
                SamDB(url=host, credentials=simple_wrong_creds, lp=lp)
                self.fail()
            except LdbError as e5:
                (num, msg) = e5.args
                self.assertEqual(num, ERR_INVALID_CREDENTIALS)
                assertLDAPErrorMsg(msg, error_msg_simple_wrong_pw)

            try:
                SamDB(url=host, credentials=simple_creds, lp=lp)
                self.fail()
            except LdbError as e6:
                (num, msg) = e6.args
                self.assertEqual(num, ERR_INVALID_CREDENTIALS)
                assertLDAPErrorMsg(msg, error_msg_simple_must_change)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_0_uac(self):
        """Test the userAccountControl behaviour"""
        print("Testing userAccountControl behaviour\n")

        # With a user object

        # Add operation

        # As user you can only set a normal account.
        # The UF_PASSWD_NOTREQD flag is needed since we haven't requested a
        # password yet.
        # With SYSTEM rights you can set a interdomain trust account.

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": "0"})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_PASSWD_NOTREQD == 0)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_normal(self):
        """Test the userAccountControl behaviour"""
        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": str(UF_NORMAL_ACCOUNT)})
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_normal_pwnotreq(self):
        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD)})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_normal_pwnotreq_lockout_expired(self):
        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": str(UF_NORMAL_ACCOUNT |
                                      UF_PASSWD_NOTREQD |
                                      UF_LOCKOUT |
                                      UF_PASSWORD_EXPIRED)})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "lockoutTime", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & (UF_LOCKOUT | UF_PASSWORD_EXPIRED) == 0)
        self.assertFalse("lockoutTime" in res1[0])
        self.assertTrue(int(res1[0]["pwdLastSet"][0]) == 0)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_temp_dup(self):
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "userAccountControl": str(UF_TEMP_DUPLICATE_ACCOUNT)})
            self.fail()
        except LdbError as e59:
            (num, _) = e59.args
            self.assertEqual(num, ERR_OTHER)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_server(self):
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT)})
            self.fail()
        except LdbError as e60:
            (num, _) = e60.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_workstation(self):
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT)})
        except LdbError as e61:
            (num, _) = e61.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_rodc(self):
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)})
        except LdbError as e62:
            (num, _) = e62.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_userAccountControl_user_add_trust(self):
        try:
            ldb.add({
                "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
                "objectclass": "user",
                "userAccountControl": str(UF_INTERDOMAIN_TRUST_ACCOUNT)})
            self.fail()
        except LdbError as e63:
            (num, _) = e63.args
            self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        # Modify operation

    def test_userAccountControl_user_modify(self):
        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        # After creation we should have a normal account
        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE != 0)

        # As user you can only switch from a normal account to a workstation
        # trust account and back.
        # The UF_PASSWD_NOTREQD flag is needed since we haven't requested a
        # password yet.
        # With SYSTEM rights you can switch to a interdomain trust account.

        # Invalid attribute
        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["userAccountControl"] = MessageElement("0",
                                                     FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
        except LdbError as e64:
            (num, _) = e64.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["userAccountControl"] = MessageElement(
                str(UF_NORMAL_ACCOUNT),
                FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
        except LdbError as e65:
            (num, _) = e65.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_ACCOUNTDISABLE),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_NORMAL_ACCOUNT != 0)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE != 0)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["lockoutTime"] = MessageElement(str(samba.unix2nttime(0)), FLAG_MOD_REPLACE, "lockoutTime")
        m["pwdLastSet"] = MessageElement(str(samba.unix2nttime(0)), FLAG_MOD_REPLACE, "pwdLastSet")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_LOCKOUT | UF_PASSWORD_EXPIRED),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "lockoutTime", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_NORMAL_ACCOUNT != 0)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & (UF_LOCKOUT | UF_PASSWORD_EXPIRED) == 0)
        self.assertTrue(int(res1[0]["lockoutTime"][0]) == 0)
        self.assertTrue(int(res1[0]["pwdLastSet"][0]) == 0)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["userAccountControl"] = MessageElement(
                str(UF_TEMP_DUPLICATE_ACCOUNT),
                FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
            self.fail()
        except LdbError as e66:
            (num, _) = e66.args
            self.assertEqual(num, ERR_OTHER)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["userAccountControl"] = MessageElement(
                str(UF_SERVER_TRUST_ACCOUNT),
                FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
            self.fail()
        except LdbError as e67:
            (num, _) = e67.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_WORKSTATION_TRUST_ACCOUNT),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["userAccountControl"] = MessageElement(
                str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT),
                FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
            self.fail()
        except LdbError as e68:
            (num, _) = e68.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_WORKSTATION_TRUST)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
            m["userAccountControl"] = MessageElement(
                str(UF_INTERDOMAIN_TRUST_ACCOUNT),
                FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
            self.fail()
        except LdbError as e69:
            (num, _) = e69.args
            self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)

    def test_userAccountControl_computer_add_0_uac(self):
        # With a computer object

        # Add operation

        # As computer you can set a normal account and a server trust account.
        # The UF_PASSWD_NOTREQD flag is needed since we haven't requested a
        # password yet.
        # With SYSTEM rights you can set a interdomain trust account.

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": "0"})

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_WORKSTATION_TRUST)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_PASSWD_NOTREQD == 0)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_userAccountControl_computer_add_normal(self):
        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_NORMAL_ACCOUNT)})
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_userAccountControl_computer_add_normal_pwnotreqd(self):
        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD)})

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_userAccountControl_computer_add_normal_pwnotreqd_lockout_expired(self):
        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_NORMAL_ACCOUNT |
                                      UF_PASSWD_NOTREQD |
                                      UF_LOCKOUT |
                                      UF_PASSWORD_EXPIRED)})

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "lockoutTime", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & (UF_LOCKOUT | UF_PASSWORD_EXPIRED) == 0)
        self.assertFalse("lockoutTime" in res1[0])
        self.assertTrue(int(res1[0]["pwdLastSet"][0]) == 0)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_userAccountControl_computer_add_temp_dup(self):
        try:
            ldb.add({
                "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
                "objectclass": "computer",
                "userAccountControl": str(UF_TEMP_DUPLICATE_ACCOUNT)})
            self.fail()
        except LdbError as e70:
            (num, _) = e70.args
            self.assertEqual(num, ERR_OTHER)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_userAccountControl_computer_add_server(self):
        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT)})

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_WORKSTATION_TRUST)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_userAccountControl_computer_add_workstation(self):
        try:
            ldb.add({
                "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
                "objectclass": "computer",
                "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT)})
        except LdbError as e71:
            (num, _) = e71.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_userAccountControl_computer_add_trust(self):
        try:
            ldb.add({
                "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
                "objectclass": "computer",
                "userAccountControl": str(UF_INTERDOMAIN_TRUST_ACCOUNT)})
            self.fail()
        except LdbError as e72:
            (num, _) = e72.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_userAccountControl_computer_modify(self):
        # Modify operation

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer"})

        # After creation we should have a normal account
        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_WORKSTATION_TRUST)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE != 0)

        # As computer you can switch from a normal account to a workstation
        # or server trust account and back (also swapping between trust
        # accounts is allowed).
        # The UF_PASSWD_NOTREQD flag is needed since we haven't requested a
        # password yet.
        # With SYSTEM rights you can switch to a interdomain trust account.

        # Invalid attribute
        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
            m["userAccountControl"] = MessageElement("0",
                                                     FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
        except LdbError as e73:
            (num, _) = e73.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
            m["userAccountControl"] = MessageElement(
                str(UF_NORMAL_ACCOUNT),
                FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
        except LdbError as e74:
            (num, _) = e74.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_ACCOUNTDISABLE),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_NORMAL_ACCOUNT != 0)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE != 0)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["lockoutTime"] = MessageElement(str(samba.unix2nttime(0)), FLAG_MOD_REPLACE, "lockoutTime")
        m["pwdLastSet"] = MessageElement(str(samba.unix2nttime(0)), FLAG_MOD_REPLACE, "pwdLastSet")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_LOCKOUT | UF_PASSWORD_EXPIRED),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["sAMAccountType", "userAccountControl", "lockoutTime", "pwdLastSet"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_NORMAL_ACCOUNT != 0)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & (UF_LOCKOUT | UF_PASSWORD_EXPIRED) == 0)
        self.assertTrue(int(res1[0]["lockoutTime"][0]) == 0)
        self.assertTrue(int(res1[0]["pwdLastSet"][0]) == 0)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
            m["userAccountControl"] = MessageElement(
                str(UF_TEMP_DUPLICATE_ACCOUNT),
                FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
            self.fail()
        except LdbError as e75:
            (num, _) = e75.args
            self.assertEqual(num, ERR_OTHER)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_SERVER_TRUST_ACCOUNT),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_WORKSTATION_TRUST)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_WORKSTATION_TRUST_ACCOUNT),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_WORKSTATION_TRUST)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_NORMAL_ACCOUNT)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_SERVER_TRUST_ACCOUNT),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_WORKSTATION_TRUST)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_WORKSTATION_TRUST_ACCOUNT),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE, attrs=["sAMAccountType"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["sAMAccountType"][0]),
                          ATYPE_WORKSTATION_TRUST)

        try:
            m = Message()
            m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
            m["userAccountControl"] = MessageElement(
                str(UF_INTERDOMAIN_TRUST_ACCOUNT),
                FLAG_MOD_REPLACE, "userAccountControl")
            ldb.modify(m)
            self.fail()
        except LdbError as e76:
            (num, _) = e76.args
            self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)

        # "primaryGroupID" does not change if account type remains the same

        # For a user account

        ldb.add({
            "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": str(UF_NORMAL_ACCOUNT |
                                      UF_PASSWD_NOTREQD |
                                      UF_ACCOUNTDISABLE)})

        res1 = ldb.search("cn=ldaptestuser2,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["userAccountControl"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["userAccountControl"][0]),
                          UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE)

        m = Message()
        m.dn = Dn(ldb, "<SID=" + ldb.get_domain_sid() + "-" + str(DOMAIN_RID_ADMINS) + ">")
        m["member"] = MessageElement(
            "cn=ldaptestuser2,cn=users," + self.base_dn, FLAG_MOD_ADD, "member")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
        m["primaryGroupID"] = MessageElement(str(DOMAIN_RID_ADMINS),
                                             FLAG_MOD_REPLACE, "primaryGroupID")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser2,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["userAccountControl", "primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_ADMINS)

        # For a workstation account

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_DOMAIN_MEMBERS)

        m = Message()
        m.dn = Dn(ldb, "<SID=" + ldb.get_domain_sid() + "-" + str(DOMAIN_RID_USERS) + ">")
        m["member"] = MessageElement(
            "cn=ldaptestcomputer,cn=computers," + self.base_dn, FLAG_MOD_ADD, "member")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["primaryGroupID"] = MessageElement(str(DOMAIN_RID_USERS),
                                             FLAG_MOD_REPLACE, "primaryGroupID")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_WORKSTATION_TRUST_ACCOUNT),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["primaryGroupID"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def find_repl_meta_data(self, rpmd, attid):
        for i in range(0, rpmd.ctr.count):
            m = rpmd.ctr.array[i]
            if m.attid == attid:
                return m
        return None

    def test_smartcard_required1(self):
        """Test the UF_SMARTCARD_REQUIRED behaviour"""
        print("Testing UF_SMARTCARD_REQUIRED behaviour\n")

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": str(UF_NORMAL_ACCOUNT),
            "unicodePwd": "\"thatsAcomplPASS2\"".encode('utf-16-le')
        })

        res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                         scope=SCOPE_BASE,
                         attrs=["sAMAccountType", "userAccountControl",
                                "pwdLastSet", "msDS-KeyVersionNumber",
                                "replPropertyMetaData"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(int(res[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT)
        self.assertNotEqual(int(res[0]["pwdLastSet"][0]), 0)
        lastset = int(res[0]["pwdLastSet"][0])
        self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 1)
        self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
        rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
                          res[0]["replPropertyMetaData"][0])
        lastsetmd = self.find_repl_meta_data(rpmd,
                                             drsuapi.DRSUAPI_ATTID_pwdLastSet)
        self.assertIsNotNone(lastsetmd)
        self.assertEqual(lastsetmd.version, 1)
        nthashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_unicodePwd)
        self.assertIsNotNone(nthashmd)
        self.assertEqual(nthashmd.version, 1)
        nthistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_ntPwdHistory)
        self.assertIsNotNone(nthistmd)
        self.assertEqual(nthistmd.version, 1)
        lmhashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_dBCSPwd)
        self.assertIsNotNone(lmhashmd)
        self.assertEqual(lmhashmd.version, 1)
        lmhistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_lmPwdHistory)
        self.assertIsNotNone(lmhistmd)
        self.assertEqual(lmhistmd.version, 1)
        spcbmd = self.find_repl_meta_data(rpmd,
                                          drsuapi.DRSUAPI_ATTID_supplementalCredentials)
        self.assertIsNotNone(spcbmd)
        self.assertEqual(spcbmd.version, 1)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                         scope=SCOPE_BASE,
                         attrs=["sAMAccountType", "userAccountControl",
                                "pwdLastSet", "msDS-KeyVersionNumber",
                                "replPropertyMetaData"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(int(res[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED)
        self.assertEqual(int(res[0]["pwdLastSet"][0]), lastset)
        self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 2)
        self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
        rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
                          res[0]["replPropertyMetaData"][0])
        lastsetmd = self.find_repl_meta_data(rpmd,
                                             drsuapi.DRSUAPI_ATTID_pwdLastSet)
        self.assertIsNotNone(lastsetmd)
        self.assertEqual(lastsetmd.version, 1)
        nthashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_unicodePwd)
        self.assertIsNotNone(nthashmd)
        self.assertEqual(nthashmd.version, 2)
        nthistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_ntPwdHistory)
        self.assertIsNotNone(nthistmd)
        self.assertEqual(nthistmd.version, 2)
        lmhashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_dBCSPwd)
        self.assertIsNotNone(lmhashmd)
        self.assertEqual(lmhashmd.version, 2)
        lmhistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_lmPwdHistory)
        self.assertIsNotNone(lmhistmd)
        self.assertEqual(lmhistmd.version, 2)
        spcbmd = self.find_repl_meta_data(rpmd,
                                          drsuapi.DRSUAPI_ATTID_supplementalCredentials)
        self.assertIsNotNone(spcbmd)
        self.assertEqual(spcbmd.version, 2)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_smartcard_required2(self):
        """Test the UF_SMARTCARD_REQUIRED behaviour"""
        print("Testing UF_SMARTCARD_REQUIRED behaviour\n")

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": str(UF_NORMAL_ACCOUNT |UF_ACCOUNTDISABLE),
        })

        res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                         scope=SCOPE_BASE,
                         attrs=["sAMAccountType", "userAccountControl",
                                "pwdLastSet", "msDS-KeyVersionNumber",
                                "replPropertyMetaData"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(int(res[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT |UF_ACCOUNTDISABLE)
        self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
        self.assertTrue("msDS-KeyVersionNumber" in res[0])
        self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 1)
        self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
        rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
                          res[0]["replPropertyMetaData"][0])
        lastsetmd = self.find_repl_meta_data(rpmd,
                                             drsuapi.DRSUAPI_ATTID_pwdLastSet)
        self.assertIsNotNone(lastsetmd)
        self.assertEqual(lastsetmd.version, 1)
        nthashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_unicodePwd)
        self.assertIsNotNone(nthashmd)
        self.assertEqual(nthashmd.version, 1)
        nthistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_ntPwdHistory)
        self.assertIsNotNone(nthistmd)
        self.assertEqual(nthistmd.version, 1)
        lmhashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_dBCSPwd)
        self.assertIsNotNone(lmhashmd)
        self.assertEqual(lmhashmd.version, 1)
        lmhistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_lmPwdHistory)
        self.assertIsNotNone(lmhistmd)
        self.assertEqual(lmhistmd.version, 1)
        spcbmd = self.find_repl_meta_data(rpmd,
                                          drsuapi.DRSUAPI_ATTID_supplementalCredentials)
        self.assertIsNone(spcbmd)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT |UF_ACCOUNTDISABLE |UF_SMARTCARD_REQUIRED),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                         scope=SCOPE_BASE,
                         attrs=["sAMAccountType", "userAccountControl",
                                "pwdLastSet", "msDS-KeyVersionNumber",
                                "replPropertyMetaData"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(int(res[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT |UF_ACCOUNTDISABLE |UF_SMARTCARD_REQUIRED)
        self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
        self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 2)
        self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
        rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
                          res[0]["replPropertyMetaData"][0])
        lastsetmd = self.find_repl_meta_data(rpmd,
                                             drsuapi.DRSUAPI_ATTID_pwdLastSet)
        self.assertIsNotNone(lastsetmd)
        self.assertEqual(lastsetmd.version, 1)
        nthashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_unicodePwd)
        self.assertIsNotNone(nthashmd)
        self.assertEqual(nthashmd.version, 2)
        nthistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_ntPwdHistory)
        self.assertIsNotNone(nthistmd)
        self.assertEqual(nthistmd.version, 2)
        lmhashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_dBCSPwd)
        self.assertIsNotNone(lmhashmd)
        self.assertEqual(lmhashmd.version, 2)
        lmhistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_lmPwdHistory)
        self.assertIsNotNone(lmhistmd)
        self.assertEqual(lmhistmd.version, 2)
        spcbmd = self.find_repl_meta_data(rpmd,
                                          drsuapi.DRSUAPI_ATTID_supplementalCredentials)
        self.assertIsNotNone(spcbmd)
        self.assertEqual(spcbmd.version, 1)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                         scope=SCOPE_BASE,
                         attrs=["sAMAccountType", "userAccountControl",
                                "pwdLastSet", "msDS-KeyVersionNumber",
                                "replPropertyMetaData"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(int(res[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED)
        self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
        self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 2)
        self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
        rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
                          res[0]["replPropertyMetaData"][0])
        lastsetmd = self.find_repl_meta_data(rpmd,
                                             drsuapi.DRSUAPI_ATTID_pwdLastSet)
        self.assertIsNotNone(lastsetmd)
        self.assertEqual(lastsetmd.version, 1)
        nthashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_unicodePwd)
        self.assertIsNotNone(nthashmd)
        self.assertEqual(nthashmd.version, 2)
        nthistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_ntPwdHistory)
        self.assertIsNotNone(nthistmd)
        self.assertEqual(nthistmd.version, 2)
        lmhashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_dBCSPwd)
        self.assertIsNotNone(lmhashmd)
        self.assertEqual(lmhashmd.version, 2)
        lmhistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_lmPwdHistory)
        self.assertIsNotNone(lmhistmd)
        self.assertEqual(lmhistmd.version, 2)
        spcbmd = self.find_repl_meta_data(rpmd,
                                          drsuapi.DRSUAPI_ATTID_supplementalCredentials)
        self.assertIsNotNone(spcbmd)
        self.assertEqual(spcbmd.version, 1)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_smartcard_required3(self):
        """Test the UF_SMARTCARD_REQUIRED behaviour"""
        print("Testing UF_SMARTCARD_REQUIRED behaviour\n")

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user",
            "userAccountControl": str(UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED |UF_ACCOUNTDISABLE),
        })

        res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                         scope=SCOPE_BASE,
                         attrs=["sAMAccountType", "userAccountControl",
                                "pwdLastSet", "msDS-KeyVersionNumber",
                                "replPropertyMetaData"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(int(res[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED |UF_ACCOUNTDISABLE)
        self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
        self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 1)
        self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
        rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
                          res[0]["replPropertyMetaData"][0])
        lastsetmd = self.find_repl_meta_data(rpmd,
                                             drsuapi.DRSUAPI_ATTID_pwdLastSet)
        self.assertIsNotNone(lastsetmd)
        self.assertEqual(lastsetmd.version, 1)
        nthashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_unicodePwd)
        self.assertIsNotNone(nthashmd)
        self.assertEqual(nthashmd.version, 1)
        nthistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_ntPwdHistory)
        self.assertIsNotNone(nthistmd)
        self.assertEqual(nthistmd.version, 1)
        lmhashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_dBCSPwd)
        self.assertIsNotNone(lmhashmd)
        self.assertEqual(lmhashmd.version, 1)
        lmhistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_lmPwdHistory)
        self.assertIsNotNone(lmhistmd)
        self.assertEqual(lmhistmd.version, 1)
        spcbmd = self.find_repl_meta_data(rpmd,
                                          drsuapi.DRSUAPI_ATTID_supplementalCredentials)
        self.assertIsNotNone(spcbmd)
        self.assertEqual(spcbmd.version, 1)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                         scope=SCOPE_BASE,
                         attrs=["sAMAccountType", "userAccountControl",
                                "pwdLastSet", "msDS-KeyVersionNumber",
                                "replPropertyMetaData"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(int(res[0]["sAMAccountType"][0]),
                         ATYPE_NORMAL_ACCOUNT)
        self.assertEqual(int(res[0]["userAccountControl"][0]),
                         UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED)
        self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
        self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 1)
        self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
        rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
                          res[0]["replPropertyMetaData"][0])
        lastsetmd = self.find_repl_meta_data(rpmd,
                                             drsuapi.DRSUAPI_ATTID_pwdLastSet)
        self.assertIsNotNone(lastsetmd)
        self.assertEqual(lastsetmd.version, 1)
        nthashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_unicodePwd)
        self.assertIsNotNone(nthashmd)
        self.assertEqual(nthashmd.version, 1)
        nthistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_ntPwdHistory)
        self.assertIsNotNone(nthistmd)
        self.assertEqual(nthistmd.version, 1)
        lmhashmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_dBCSPwd)
        self.assertIsNotNone(lmhashmd)
        self.assertEqual(lmhashmd.version, 1)
        lmhistmd = self.find_repl_meta_data(rpmd,
                                            drsuapi.DRSUAPI_ATTID_lmPwdHistory)
        self.assertIsNotNone(lmhistmd)
        self.assertEqual(lmhistmd.version, 1)
        spcbmd = self.find_repl_meta_data(rpmd,
                                          drsuapi.DRSUAPI_ATTID_supplementalCredentials)
        self.assertIsNotNone(spcbmd)
        self.assertEqual(spcbmd.version, 1)

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_isCriticalSystemObject_user(self):
        """Test the isCriticalSystemObject behaviour"""
        print("Testing isCriticalSystemObject behaviour\n")

        # Add tests (of a user)

        ldb.add({
            "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
            "objectclass": "user"})

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertTrue("isCriticalSystemObject" not in res1[0])

        # Modification tests
        m = Message()

        m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT),
                                                 FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertTrue("isCriticalSystemObject" in res1[0])
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")

        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)

    def test_isCriticalSystemObject(self):
        """Test the isCriticalSystemObject behaviour"""
        print("Testing isCriticalSystemObject behaviour\n")

        # Add tests

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer"})

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertTrue("isCriticalSystemObject" in res1[0])
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT)})

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)})

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT)})

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")

        # Modification tests

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
                                                 FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT),
                                                 FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(
            str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT),
            FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
                                                 FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_SERVER_TRUST_ACCOUNT),
                                                 FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["userAccountControl"] = MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT),
                                                 FLAG_MOD_REPLACE, "userAccountControl")
        ldb.modify(m)

        res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                          scope=SCOPE_BASE,
                          attrs=["isCriticalSystemObject"])
        self.assertTrue(len(res1) == 1)
        self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_service_principal_name_updates(self):
        """Test the servicePrincipalNames update behaviour"""
        print("Testing servicePrincipalNames update behaviour\n")

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "dNSHostName": "testname.testdom"})

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertFalse("servicePrincipalName" in res[0])

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "servicePrincipalName": "HOST/testname.testdom"})

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["dNSHostName"])
        self.assertTrue(len(res) == 1)
        self.assertFalse("dNSHostName" in res[0])

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "dNSHostName": "testname2.testdom",
            "servicePrincipalName": "HOST/testname.testdom"})

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["dNSHostName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["dNSHostName"][0]), "testname2.testdom")

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname.testdom")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement("testname.testdoM",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname.testdom")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement("testname2.testdom2",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname2.testdom2")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement([],
                                          FLAG_MOD_DELETE, "dNSHostName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname2.testdom2")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement("testname.testdom3",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname2.testdom2")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement("testname2.testdom2",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement("testname3.testdom3",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        m["servicePrincipalName"] = MessageElement("HOST/testname2.testdom2",
                                                   FLAG_MOD_REPLACE,
                                                   "servicePrincipalName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname3.testdom3")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["servicePrincipalName"] = MessageElement("HOST/testname2.testdom2",
                                                   FLAG_MOD_REPLACE,
                                                   "servicePrincipalName")
        m["dNSHostName"] = MessageElement("testname4.testdom4",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname2.testdom2")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["servicePrincipalName"] = MessageElement([],
                                                   FLAG_MOD_DELETE,
                                                   "servicePrincipalName")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement("testname2.testdom2",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertFalse("servicePrincipalName" in res[0])

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "sAMAccountName": "testname$"})

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertFalse("servicePrincipalName" in res[0])

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "servicePrincipalName": "HOST/testname"})

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["sAMAccountName"])
        self.assertTrue(len(res) == 1)
        self.assertTrue("sAMAccountName" in res[0])

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "sAMAccountName": "testname$",
            "servicePrincipalName": "HOST/testname"})

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["sAMAccountName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname$")

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["sAMAccountName"] = MessageElement("testnamE$",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["sAMAccountName"] = MessageElement("testname",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["sAMAccountName"] = MessageElement("test$name$",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/test$name")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["sAMAccountName"] = MessageElement("testname2",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname2")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["sAMAccountName"] = MessageElement("testname3",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        m["servicePrincipalName"] = MessageElement("HOST/testname2",
                                                   FLAG_MOD_REPLACE,
                                                   "servicePrincipalName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname3")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["servicePrincipalName"] = MessageElement("HOST/testname2",
                                                   FLAG_MOD_REPLACE,
                                                   "servicePrincipalName")
        m["sAMAccountName"] = MessageElement("testname4",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["servicePrincipalName"][0]),
                          "HOST/testname2")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["servicePrincipalName"] = MessageElement([],
                                                   FLAG_MOD_DELETE,
                                                   "servicePrincipalName")
        ldb.modify(m)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["sAMAccountName"] = MessageElement("testname2",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertFalse("servicePrincipalName" in res[0])

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "dNSHostName": "testname.testdom",
            "sAMAccountName": "testname$",
            "servicePrincipalName": ["HOST/testname.testdom", "HOST/testname"]
        })

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement("testname2.testdom",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        m["sAMAccountName"] = MessageElement("testname2$",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["dNSHostName", "sAMAccountName", "servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["dNSHostName"][0]), "testname2.testdom")
        self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname2$")
        self.assertTrue(str(res[0]["servicePrincipalName"][0]) == "HOST/testname2" or
                        str(res[0]["servicePrincipalName"][1]) == "HOST/testname2")
        self.assertTrue(str(res[0]["servicePrincipalName"][0]) == "HOST/testname2.testdom" or
                        str(res[0]["servicePrincipalName"][1]) == "HOST/testname2.testdom")

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "dNSHostName": "testname.testdom",
            "sAMAccountName": "testname$",
            "servicePrincipalName": ["HOST/testname.testdom", "HOST/testname"]
        })

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["sAMAccountName"] = MessageElement("testname2$",
                                             FLAG_MOD_REPLACE, "sAMAccountName")
        m["dNSHostName"] = MessageElement("testname2.testdom",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["dNSHostName", "sAMAccountName", "servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["dNSHostName"][0]), "testname2.testdom")
        self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname2$")
        self.assertTrue(len(res[0]["servicePrincipalName"]) == 2)
        self.assertTrue("HOST/testname2" in [str(x) for x in res[0]["servicePrincipalName"]])
        self.assertTrue("HOST/testname2.testdom" in [str(x) for x in res[0]["servicePrincipalName"]])

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["servicePrincipalName"] = MessageElement("HOST/testname2.testdom",
                                                   FLAG_MOD_ADD, "servicePrincipalName")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e77:
            (num, _) = e77.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["servicePrincipalName"] = MessageElement("HOST/testname3",
                                                   FLAG_MOD_ADD, "servicePrincipalName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["dNSHostName", "sAMAccountName", "servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["dNSHostName"][0]), "testname2.testdom")
        self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname2$")
        self.assertTrue(len(res[0]["servicePrincipalName"]) == 3)
        self.assertTrue("HOST/testname2" in [str(x) for x in res[0]["servicePrincipalName"]])
        self.assertTrue("HOST/testname3" in [str(x) for x in res[0]["servicePrincipalName"]])
        self.assertTrue("HOST/testname2.testdom" in [str(x) for x in res[0]["servicePrincipalName"]])

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
        m["dNSHostName"] = MessageElement("testname3.testdom",
                                          FLAG_MOD_REPLACE, "dNSHostName")
        m["servicePrincipalName"] = MessageElement("HOST/testname3.testdom",
                                                   FLAG_MOD_ADD, "servicePrincipalName")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["dNSHostName", "sAMAccountName", "servicePrincipalName"])
        self.assertTrue(len(res) == 1)
        self.assertEqual(str(res[0]["dNSHostName"][0]), "testname3.testdom")
        self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname2$")
        self.assertTrue(len(res[0]["servicePrincipalName"]) == 3)
        self.assertTrue("HOST/testname2" in [str(x) for x in res[0]["servicePrincipalName"]])
        self.assertTrue("HOST/testname3" in [str(x) for x in res[0]["servicePrincipalName"]])
        self.assertTrue("HOST/testname3.testdom" in [str(x) for x in res[0]["servicePrincipalName"]])

        delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)

    def test_service_principal_name_uniqueness(self):
        """Test the servicePrincipalName uniqueness behaviour"""
        print("Testing servicePrincipalName uniqueness behaviour")

        ldb.add({
            "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
            "objectclass": "computer",
            "servicePrincipalName": "HOST/testname.testdom"})

        try:
            ldb.add({
                "dn": "cn=ldaptestcomputer2,cn=computers," + self.base_dn,
                "objectclass": "computer",
                "servicePrincipalName": "HOST/testname.testdom"})
        except LdbError as e:
            num, _ = e.args
            self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
        else:
            self.fail()

    def test_sam_description_attribute(self):
        """Test SAM description attribute"""
        print("Test SAM description attribute")

        self.ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "description": "desc1"
        })

        res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["description"])
        self.assertTrue(len(res) == 1)
        self.assertTrue("description" in res[0])
        self.assertTrue(len(res[0]["description"]) == 1)
        self.assertEqual(str(res[0]["description"][0]), "desc1")

        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        self.ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "description": ["desc1", "desc2"]})

        res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["description"])
        self.assertTrue(len(res) == 1)
        self.assertTrue("description" in res[0])
        self.assertTrue(len(res[0]["description"]) == 2)
        self.assertTrue(str(res[0]["description"][0]) == "desc1" or
                        str(res[0]["description"][1]) == "desc1")
        self.assertTrue(str(res[0]["description"][0]) == "desc2" or
                        str(res[0]["description"][1]) == "desc2")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement(["desc1", "desc2"], FLAG_MOD_REPLACE,
                                          "description")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e78:
            (num, _) = e78.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement(["desc1", "desc2"], FLAG_MOD_DELETE,
                                          "description")
        ldb.modify(m)

        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        self.ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group"})

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement("desc1", FLAG_MOD_REPLACE,
                                          "description")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["description"])
        self.assertTrue(len(res) == 1)
        self.assertTrue("description" in res[0])
        self.assertTrue(len(res[0]["description"]) == 1)
        self.assertEqual(str(res[0]["description"][0]), "desc1")

        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        self.ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "description": ["desc1", "desc2"]})

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement("desc1", FLAG_MOD_REPLACE,
                                          "description")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["description"])
        self.assertTrue(len(res) == 1)
        self.assertTrue("description" in res[0])
        self.assertTrue(len(res[0]["description"]) == 1)
        self.assertEqual(str(res[0]["description"][0]), "desc1")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement("desc3", FLAG_MOD_ADD,
                                          "description")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e79:
            (num, _) = e79.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement(["desc1", "desc2"], FLAG_MOD_DELETE,
                                          "description")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e80:
            (num, _) = e80.args
            self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement("desc1", FLAG_MOD_DELETE,
                                          "description")
        ldb.modify(m)
        res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["description"])
        self.assertTrue(len(res) == 1)
        self.assertFalse("description" in res[0])

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement(["desc1", "desc2"], FLAG_MOD_REPLACE,
                                          "description")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e81:
            (num, _) = e81.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement(["desc3", "desc4"], FLAG_MOD_ADD,
                                          "description")
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e82:
            (num, _) = e82.args
            self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m["description"] = MessageElement("desc1", FLAG_MOD_ADD,
                                          "description")
        ldb.modify(m)

        res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["description"])
        self.assertTrue(len(res) == 1)
        self.assertTrue("description" in res[0])
        self.assertTrue(len(res[0]["description"]) == 1)
        self.assertEqual(str(res[0]["description"][0]), "desc1")

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m.add(MessageElement("desc1", FLAG_MOD_DELETE, "description"))
        m.add(MessageElement("desc2", FLAG_MOD_ADD, "description"))
        ldb.modify(m)

        res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
                         scope=SCOPE_BASE, attrs=["description"])
        self.assertTrue(len(res) == 1)
        self.assertTrue("description" in res[0])
        self.assertTrue(len(res[0]["description"]) == 1)
        self.assertEqual(str(res[0]["description"][0]), "desc2")

        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

    def test_fSMORoleOwner_attribute(self):
        """Test fSMORoleOwner attribute"""
        print("Test fSMORoleOwner attribute")

        ds_service_name = self.ldb.get_dsServiceName()

        # The "fSMORoleOwner" attribute can only be set to "nTDSDSA" entries,
        # invalid DNs return ERR_UNWILLING_TO_PERFORM

        try:
            self.ldb.add({
                "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
                "objectclass": "group",
                "fSMORoleOwner": self.base_dn})
            self.fail()
        except LdbError as e83:
            (num, _) = e83.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        try:
            self.ldb.add({
                "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
                "objectclass": "group",
                "fSMORoleOwner": []})
            self.fail()
        except LdbError as e84:
            (num, _) = e84.args
            self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)

        # We are able to set it to a valid "nTDSDSA" entry if the server is
        # capable of handling the role

        self.ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group",
            "fSMORoleOwner": ds_service_name})

        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

        self.ldb.add({
            "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
            "objectclass": "group"})

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m.add(MessageElement(self.base_dn, FLAG_MOD_REPLACE, "fSMORoleOwner"))
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e85:
            (num, _) = e85.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m.add(MessageElement([], FLAG_MOD_REPLACE, "fSMORoleOwner"))
        try:
            ldb.modify(m)
            self.fail()
        except LdbError as e86:
            (num, _) = e86.args
            self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)

        # We are able to set it to a valid "nTDSDSA" entry if the server is
        # capable of handling the role

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m.add(MessageElement(ds_service_name, FLAG_MOD_REPLACE, "fSMORoleOwner"))
        ldb.modify(m)

        # A clean-out works on plain entries, not master (schema, PDC...) DNs

        m = Message()
        m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
        m.add(MessageElement([], FLAG_MOD_DELETE, "fSMORoleOwner"))
        ldb.modify(m)

        delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)

    def test_protected_sid_objects(self):
        """Test deletion of objects with RID < 1000"""
        # a list of some well-known sids
        # objects in Builtin are already covered by objectclass
        protected_list = [
            ["CN=Domain Admins", "CN=Users,"],
            ["CN=Schema Admins", "CN=Users,"],
            ["CN=Enterprise Admins", "CN=Users,"],
            ["CN=Administrator", "CN=Users,"],
            ["CN=Domain Controllers", "CN=Users,"],
            ["CN=Protected Users", "CN=Users,"],
        ]

        for pr_object in protected_list:
            try:
                self.ldb.delete(pr_object[0] + "," + pr_object[1] + self.base_dn)
            except LdbError as e7:
                (num, _) = e7.args
                self.assertEqual(num, ERR_OTHER)
            else:
                self.fail("Deleted " + pr_object[0])

            try:
                self.ldb.rename(pr_object[0] + "," + pr_object[1] + self.base_dn,
                                pr_object[0] + "2," + pr_object[1] + self.base_dn)
            except LdbError as e8:
                (num, _) = e8.args
                self.fail("Could not rename " + pr_object[0])

            self.ldb.rename(pr_object[0] + "2," + pr_object[1] + self.base_dn,
                            pr_object[0] + "," + pr_object[1] + self.base_dn)

    def test_new_user_default_attributes(self):
        """Test default attributes for new user objects"""
        print("Test default attributes for new User objects\n")

        user_name = "ldaptestuser"
        user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
        ldb.add({
            "dn": user_dn,
            "objectclass": "user",
            "sAMAccountName": user_name})

        res = ldb.search(user_dn, scope=SCOPE_BASE)
        self.assertTrue(len(res) == 1)
        user_obj = res[0]

        expected_attrs = {"primaryGroupID": MessageElement(["513"]),
                          "logonCount": MessageElement(["0"]),
                          "cn": MessageElement([user_name]),
                          "countryCode": MessageElement(["0"]),
                          "objectClass": MessageElement(["top", "person", "organizationalPerson", "user"]),
                          "instanceType": MessageElement(["4"]),
                          "distinguishedName": MessageElement([user_dn]),
                          "sAMAccountType": MessageElement(["805306368"]),
                          "objectSid": "**SKIP**",
                          "whenCreated": "**SKIP**",
                          "uSNCreated": "**SKIP**",
                          "badPasswordTime": MessageElement(["0"]),
                          "dn": Dn(ldb, user_dn),
                          "pwdLastSet": MessageElement(["0"]),
                          "sAMAccountName": MessageElement([user_name]),
                          "objectCategory": MessageElement(["CN=Person,%s" % ldb.get_schema_basedn().get_linearized()]),
                          "objectGUID": "**SKIP**",
                          "whenChanged": "**SKIP**",
                          "badPwdCount": MessageElement(["0"]),
                          "accountExpires": MessageElement(["9223372036854775807"]),
                          "name": MessageElement([user_name]),
                          "codePage": MessageElement(["0"]),
                          "userAccountControl": MessageElement(["546"]),
                          "lastLogon": MessageElement(["0"]),
                          "uSNChanged": "**SKIP**",
                          "lastLogoff": MessageElement(["0"])}
        # assert we have expected attribute names
        actual_names = set(user_obj.keys())
        # Samba does not use 'dSCorePropagationData', so skip it
        actual_names -= set(['dSCorePropagationData'])
        self.assertEqual(set(expected_attrs.keys()), actual_names, "Actual object does not have expected attributes")
        # check attribute values
        for name in expected_attrs.keys():
            actual_val = user_obj.get(name)
            self.assertFalse(actual_val is None, "No value for attribute '%s'" % name)
            expected_val = expected_attrs[name]
            if expected_val == "**SKIP**":
                # "**ANY**" values means "any"
                continue
            self.assertEqual(expected_val, actual_val,
                             "Unexpected value[%r] for '%s' expected[%r]" %
                             (actual_val, name, expected_val))
        # clean up
        delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)


if "://" not in host:
    if os.path.isfile(host):
        host = "tdb://%s" % host
    else:
        host = "ldap://%s" % host

ldb = SamDB(host, credentials=creds, session_info=system_session(lp), lp=lp)

TestProgram(module=__name__, opts=subunitopts)
