summaryrefslogtreecommitdiffstats
path: root/source4/dsdb/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 17:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 17:47:29 +0000
commit4f5791ebd03eaec1c7da0865a383175b05102712 (patch)
tree8ce7b00f7a76baa386372422adebbe64510812d4 /source4/dsdb/tests
parentInitial commit. (diff)
downloadsamba-upstream.tar.xz
samba-upstream.zip
Adding upstream version 2:4.17.12+dfsg.upstream/2%4.17.12+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rwxr-xr-xsource4/dsdb/tests/python/acl.py3395
-rwxr-xr-xsource4/dsdb/tests/python/acl_modify.py236
-rw-r--r--source4/dsdb/tests/python/ad_dc_medley_performance.py520
-rw-r--r--source4/dsdb/tests/python/ad_dc_multi_bind.py95
-rw-r--r--source4/dsdb/tests/python/ad_dc_performance.py339
-rw-r--r--source4/dsdb/tests/python/ad_dc_provision_performance.py130
-rw-r--r--source4/dsdb/tests/python/ad_dc_search_performance.py299
-rw-r--r--source4/dsdb/tests/python/asq.py225
-rw-r--r--source4/dsdb/tests/python/attr_from_server.py149
-rwxr-xr-xsource4/dsdb/tests/python/confidential_attr.py1137
-rwxr-xr-xsource4/dsdb/tests/python/deletetest.py565
-rwxr-xr-xsource4/dsdb/tests/python/dirsync.py1107
-rw-r--r--source4/dsdb/tests/python/dsdb_schema_info.py200
-rw-r--r--source4/dsdb/tests/python/large_ldap.py333
-rwxr-xr-xsource4/dsdb/tests/python/ldap.py3332
-rw-r--r--source4/dsdb/tests/python/ldap_modify_order.py371
-rwxr-xr-xsource4/dsdb/tests/python/ldap_schema.py1601
-rwxr-xr-xsource4/dsdb/tests/python/ldap_syntaxes.py388
-rw-r--r--source4/dsdb/tests/python/linked_attributes.py734
-rwxr-xr-xsource4/dsdb/tests/python/login_basics.py194
-rw-r--r--source4/dsdb/tests/python/ndr_pack_performance.py229
-rwxr-xr-xsource4/dsdb/tests/python/notification.py372
-rwxr-xr-xsource4/dsdb/tests/python/password_lockout.py1708
-rw-r--r--source4/dsdb/tests/python/password_lockout_base.py787
-rw-r--r--source4/dsdb/tests/python/password_settings.py876
-rwxr-xr-xsource4/dsdb/tests/python/passwords.py1454
-rw-r--r--source4/dsdb/tests/python/priv_attrs.py398
-rwxr-xr-xsource4/dsdb/tests/python/rodc.py258
-rw-r--r--source4/dsdb/tests/python/rodc_rwdc.py1325
-rwxr-xr-xsource4/dsdb/tests/python/sam.py3877
-rwxr-xr-xsource4/dsdb/tests/python/sec_descriptor.py2158
-rwxr-xr-xsource4/dsdb/tests/python/sites.py637
-rw-r--r--source4/dsdb/tests/python/sort.py379
-rw-r--r--source4/dsdb/tests/python/subtree_rename.py417
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_account_locality_device-non-admin.expected31
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_account_locality_device.expected34
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_container_flags-non-admin.expected129
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_container_flags.expected134
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue-non-admin.expected127
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue.expected138
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_inapplicable-non-admin.expected31
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_inapplicable.expected34
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_member-non-admin.expected127
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_member.expected190
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_mixed-non-admin.expected128
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_mixed.expected143
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_mixed2-non-admin.expected128
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_mixed2.expected143
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_objectclass-non-admin.expected31
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_objectclass.expected35
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_objectclass2-non-admin.expected726
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_objectclass2.expected735
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_singlevalue-non-admin.expected727
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_singlevalue.expected740
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable-non-admin.expected127
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable.expected127
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_telephone-non-admin.expected727
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_telephone.expected752
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete-non-admin.expected727
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete.expected736
-rw-r--r--source4/dsdb/tests/python/testdata/simplesort.expected8
-rw-r--r--source4/dsdb/tests/python/testdata/unicodesort.expected16
-rwxr-xr-xsource4/dsdb/tests/python/token_group.py736
-rwxr-xr-xsource4/dsdb/tests/python/tombstone_reanimation.py956
-rwxr-xr-xsource4/dsdb/tests/python/urgent_replication.py339
-rwxr-xr-xsource4/dsdb/tests/python/user_account_control.py1308
-rw-r--r--source4/dsdb/tests/python/vlv.py1765
67 files changed, 43060 insertions, 0 deletions
diff --git a/source4/dsdb/tests/python/acl.py b/source4/dsdb/tests/python/acl.py
new file mode 100755
index 0000000..eb2bafd
--- /dev/null
+++ b/source4/dsdb/tests/python/acl.py
@@ -0,0 +1,3395 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This is unit with tests for LDAP access checks
+
+import optparse
+import sys
+import base64
+import re
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests import DynamicTestCase
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+from samba.common import get_string
+
+import samba.getopt as options
+from samba.join import DCJoinContext
+
+from ldb import (
+ SCOPE_BASE, SCOPE_ONELEVEL, SCOPE_SUBTREE, LdbError, ERR_NO_SUCH_OBJECT,
+ ERR_UNWILLING_TO_PERFORM, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_OPERATIONS_ERROR
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE, FLAG_MOD_ADD, FLAG_MOD_DELETE
+from samba.dcerpc import security, drsuapi, misc
+
+from samba.auth import system_session
+from samba import gensec, sd_utils
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+import samba.tests
+from samba.tests import delete_force
+import samba.dsdb
+from samba.tests.password_test import PasswordCommon
+
+parser = optparse.OptionParser("acl.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]
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+
+
+class AclTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(AclTests, self).setUp()
+ self.ldb_admin = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.domain_sid = security.dom_sid(self.ldb_admin.get_domain_sid())
+ self.user_pass = "samba123@"
+ self.configuration_dn = self.ldb_admin.get_config_basedn().get_linearized()
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+ self.addCleanup(self.delete_admin_connection)
+ # used for anonymous login
+ self.creds_tmp = Credentials()
+ self.creds_tmp.set_username("")
+ self.creds_tmp.set_password("")
+ self.creds_tmp.set_domain(creds.get_domain())
+ self.creds_tmp.set_realm(creds.get_realm())
+ self.creds_tmp.set_workstation(creds.get_workstation())
+ print("baseDN: %s" % self.base_dn)
+
+ def get_user_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ # Test if we have any additional groups for users than default ones
+ def assert_user_no_group_member(self, username):
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % self.get_user_dn(username))
+ try:
+ self.assertEqual(res[0]["memberOf"][0], "")
+ except KeyError:
+ pass
+ else:
+ self.fail()
+
+ def delete_admin_connection(self):
+ del self.sd_utils
+ del self.ldb_admin
+
+# tests on ldap add operations
+
+
+class AclAddTests(AclTests):
+
+ def setUp(self):
+ super(AclAddTests, self).setUp()
+ # Domain admin that will be creator of OU parent-child structure
+ self.usr_admin_owner = "acl_add_user1"
+ # Second domain admin that will not be creator of OU parent-child structure
+ self.usr_admin_not_owner = "acl_add_user2"
+ # Regular user
+ self.regular_user = "acl_add_user3"
+ self.test_user1 = "test_add_user1"
+ self.test_group1 = "test_add_group1"
+ self.ou1 = "OU=test_add_ou1"
+ self.ou2 = "OU=test_add_ou2,%s" % self.ou1
+ self.ldb_admin.newuser(self.usr_admin_owner, self.user_pass)
+ self.ldb_admin.newuser(self.usr_admin_not_owner, self.user_pass)
+ self.ldb_admin.newuser(self.regular_user, self.user_pass)
+
+ # add admins to the Domain Admins group
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.usr_admin_owner],
+ add_members_operation=True)
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.usr_admin_not_owner],
+ add_members_operation=True)
+
+ self.ldb_owner = self.get_ldb_connection(self.usr_admin_owner, self.user_pass)
+ self.ldb_notowner = self.get_ldb_connection(self.usr_admin_not_owner, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.regular_user, self.user_pass)
+
+ def tearDown(self):
+ super(AclAddTests, self).tearDown()
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" %
+ (self.test_user1, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" %
+ (self.test_group1, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, self.get_user_dn(self.usr_admin_owner))
+ delete_force(self.ldb_admin, self.get_user_dn(self.usr_admin_not_owner))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+ delete_force(self.ldb_admin, self.get_user_dn("test_add_anonymous"))
+
+ del self.ldb_notowner
+ del self.ldb_owner
+ del self.ldb_user
+
+ # Make sure top OU is deleted (and so everything under it)
+ def assert_top_ou_deleted(self):
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s,%s)" % (
+ "OU=test_add_ou1", self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ def test_add_u1(self):
+ """Testing OU with the rights of Domain Admin not creator of the OU """
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ self.ldb_owner.create_ou("OU=test_add_ou2,OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.usr_admin_not_owner))
+ mod = "(D;CI;WPCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # Test user and group creation with another domain admin's credentials
+ self.ldb_notowner.newuser(self.test_user1, self.user_pass, userou=self.ou2)
+ self.ldb_notowner.newgroup("test_add_group1", groupou="OU=test_add_ou2,OU=test_add_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ # Make sure we HAVE created the two objects -- user and group
+ # !!! We should not be able to do that, but however because of ACE ordering our inherited Deny ACE
+ # !!! comes after explicit (A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA) that comes from somewhere
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_user1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertTrue(len(res) > 0)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_group1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertTrue(len(res) > 0)
+
+ def test_add_u2(self):
+ """Testing OU with the regular user that has no rights granted over the OU """
+ self.assert_top_ou_deleted()
+ # Create a parent-child OU structure with domain admin credentials
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ self.ldb_owner.create_ou("OU=test_add_ou2,OU=test_add_ou1," + self.base_dn)
+ # Test user and group creation with regular user credentials
+ try:
+ self.ldb_user.newuser(self.test_user1, self.user_pass, userou=self.ou2)
+ self.ldb_user.newgroup("test_add_group1", groupou="OU=test_add_ou2,OU=test_add_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ # Make sure we HAVEN'T created any of two objects -- user or group
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_user1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_group1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ def test_add_u3(self):
+ """Testing OU with the rights of regular user granted the right 'Create User child objects' """
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(OA;CI;CC;bf967aba-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ self.ldb_owner.create_ou("OU=test_add_ou2,OU=test_add_ou1," + self.base_dn)
+ # Test user and group creation with granted user only to one of the objects
+ self.ldb_user.newuser(self.test_user1, self.user_pass, userou=self.ou2, setpassword=False)
+ try:
+ self.ldb_user.newgroup("test_add_group1", groupou="OU=test_add_ou2,OU=test_add_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ # Make sure we HAVE created the one of two objects -- user
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s,%s)" %
+ ("CN=test_add_user1,OU=test_add_ou2,OU=test_add_ou1",
+ self.base_dn))
+ self.assertNotEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s,%s)" %
+ ("CN=test_add_group1,OU=test_add_ou2,OU=test_add_ou1",
+ self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ def test_add_u4(self):
+ """ 4 Testing OU with the rights of Domain Admin creator of the OU"""
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ self.ldb_owner.create_ou("OU=test_add_ou2,OU=test_add_ou1," + self.base_dn)
+ self.ldb_owner.newuser(self.test_user1, self.user_pass, userou=self.ou2)
+ self.ldb_owner.newgroup("test_add_group1", groupou="OU=test_add_ou2,OU=test_add_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ # Make sure we have successfully created the two objects -- user and group
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_user1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertTrue(len(res) > 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s,%s)" % ("CN=test_add_group1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertTrue(len(res) > 0)
+
+ def test_add_anonymous(self):
+ """Test add operation with anonymous user"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ try:
+ anonymous.newuser("test_add_anonymous", self.user_pass)
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+
+# tests on ldap modify operations
+
+
+class AclModifyTests(AclTests):
+
+ def setUp(self):
+ super(AclModifyTests, self).setUp()
+ self.user_with_wp = "acl_mod_user1"
+ self.user_with_sm = "acl_mod_user2"
+ self.user_with_group_sm = "acl_mod_user3"
+ self.ldb_admin.newuser(self.user_with_wp, self.user_pass)
+ self.ldb_admin.newuser(self.user_with_sm, self.user_pass)
+ self.ldb_admin.newuser(self.user_with_group_sm, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.user_with_wp, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.user_with_sm, self.user_pass)
+ self.ldb_user3 = self.get_ldb_connection(self.user_with_group_sm, self.user_pass)
+ self.user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user_with_wp))
+ self.ldb_admin.newgroup("test_modify_group2", grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ self.ldb_admin.newgroup("test_modify_group3", grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ self.ldb_admin.newuser("test_modify_user2", self.user_pass)
+
+ def tearDown(self):
+ super(AclModifyTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn("test_modify_user1"))
+ delete_force(self.ldb_admin, "CN=test_modify_group1,CN=Users," + self.base_dn)
+ delete_force(self.ldb_admin, "CN=test_modify_group2,CN=Users," + self.base_dn)
+ delete_force(self.ldb_admin, "CN=test_modify_group3,CN=Users," + self.base_dn)
+ delete_force(self.ldb_admin, "CN=test_mod_hostname,OU=test_modify_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_modify_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_wp))
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_sm))
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_group_sm))
+ delete_force(self.ldb_admin, self.get_user_dn("test_modify_user2"))
+ delete_force(self.ldb_admin, self.get_user_dn("test_anonymous"))
+
+ del self.ldb_user
+ del self.ldb_user2
+ del self.ldb_user3
+
+ def test_modify_u1(self):
+ """5 Modify one attribute if you have DS_WRITE_PROPERTY for it"""
+ mod = "(OA;;WP;bf967953-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.user_sid)
+ # First test object -- User
+ print("Testing modify on User object")
+ self.ldb_admin.newuser("test_modify_user1", self.user_pass)
+ self.sd_utils.dacl_add_ace(self.get_user_dn("test_modify_user1"), mod)
+ ldif = """
+dn: """ + self.get_user_dn("test_modify_user1") + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % self.get_user_dn("test_modify_user1"))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Second test object -- Group
+ print("Testing modify on Group object")
+ self.ldb_admin.newgroup("test_modify_group1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group1,CN=Users," + self.base_dn, mod)
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % str("CN=test_modify_group1,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Third test object -- Organizational Unit
+ print("Testing modify on OU object")
+ #delete_force(self.ldb_admin, "OU=test_modify_ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=test_modify_ou1," + self.base_dn)
+ self.sd_utils.dacl_add_ace("OU=test_modify_ou1," + self.base_dn, mod)
+ ldif = """
+dn: OU=test_modify_ou1,""" + self.base_dn + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % str("OU=test_modify_ou1," + self.base_dn))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+
+ def test_modify_u2(self):
+ """6 Modify two attributes as you have DS_WRITE_PROPERTY granted only for one of them"""
+ mod = "(OA;;WP;bf967953-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.user_sid)
+ # First test object -- User
+ print("Testing modify on User object")
+ #delete_force(self.ldb_admin, self.get_user_dn("test_modify_user1"))
+ self.ldb_admin.newuser("test_modify_user1", self.user_pass)
+ self.sd_utils.dacl_add_ace(self.get_user_dn("test_modify_user1"), mod)
+ # Modify on attribute you have rights for
+ ldif = """
+dn: """ + self.get_user_dn("test_modify_user1") + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" %
+ self.get_user_dn("test_modify_user1"))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: """ + self.get_user_dn("test_modify_user1") + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+ # Second test object -- Group
+ print("Testing modify on Group object")
+ self.ldb_admin.newgroup("test_modify_group1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group1,CN=Users," + self.base_dn, mod)
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" %
+ str("CN=test_modify_group1,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+ # Modify on attribute you do not have rights for granted while also modifying something you do have rights for
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org
+replace: displayName
+displayName: test_changed"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+ # Second test object -- Organizational Unit
+ print("Testing modify on OU object")
+ self.ldb_admin.create_ou("OU=test_modify_ou1," + self.base_dn)
+ self.sd_utils.dacl_add_ace("OU=test_modify_ou1," + self.base_dn, mod)
+ ldif = """
+dn: OU=test_modify_ou1,""" + self.base_dn + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str("OU=test_modify_ou1,"
+ + self.base_dn))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: OU=test_modify_ou1,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ def test_modify_u3(self):
+ """7 Modify one attribute as you have no what so ever rights granted"""
+ # First test object -- User
+ print("Testing modify on User object")
+ self.ldb_admin.newuser("test_modify_user1", self.user_pass)
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: """ + self.get_user_dn("test_modify_user1") + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ # Second test object -- Group
+ print("Testing modify on Group object")
+ self.ldb_admin.newgroup("test_modify_group1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ # Second test object -- Organizational Unit
+ print("Testing modify on OU object")
+ #delete_force(self.ldb_admin, "OU=test_modify_ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=test_modify_ou1," + self.base_dn)
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: OU=test_modify_ou1,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e9:
+ (num, _) = e9.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ def test_modify_u4(self):
+ """11 Grant WP to PRINCIPAL_SELF and test modify"""
+ ldif = """
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+add: adminDescription
+adminDescription: blah blah blah"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ mod = "(OA;;WP;bf967919-0de6-11d0-a285-00aa003049e2;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ # Modify on attribute you have rights for
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)"
+ % self.get_user_dn(self.user_with_wp), attrs=["adminDescription"])
+ self.assertEqual(str(res[0]["adminDescription"][0]), "blah blah blah")
+
+ def test_modify_u5(self):
+ """12 test self membership"""
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: """ + self.get_user_dn(self.user_with_sm)
+# the user has no rights granted, this should fail
+ try:
+ self.ldb_user2.modify_ldif(ldif)
+ except LdbError as e11:
+ (num, _) = e11.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+# grant self-membership, should be able to add himself
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user_with_sm))
+ mod = "(OA;;SW;bf9679c0-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group2,CN=Users," + self.base_dn, mod)
+ self.ldb_user2.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)"
+ % ("CN=test_modify_group2,CN=Users," + self.base_dn), attrs=["Member"])
+ self.assertEqual(str(res[0]["Member"][0]), self.get_user_dn(self.user_with_sm))
+# but not other users
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: CN=test_modify_user2,CN=Users,""" + self.base_dn
+ try:
+ self.ldb_user2.modify_ldif(ldif)
+ except LdbError as e12:
+ (num, _) = e12.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_modify_u6(self):
+ """13 test self membership"""
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: """ + self.get_user_dn(self.user_with_sm) + """
+Member: CN=test_modify_user2,CN=Users,""" + self.base_dn
+
+# grant self-membership, should be able to add himself but not others at the same time
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user_with_sm))
+ mod = "(OA;;SW;bf9679c0-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group2,CN=Users," + self.base_dn, mod)
+ try:
+ self.ldb_user2.modify_ldif(ldif)
+ except LdbError as e13:
+ (num, _) = e13.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_modify_u7(self):
+ """13 User with WP modifying Member"""
+# a second user is given write property permission
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user_with_wp))
+ mod = "(A;;WP;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group2,CN=Users," + self.base_dn, mod)
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: """ + self.get_user_dn(self.user_with_wp)
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)"
+ % ("CN=test_modify_group2,CN=Users," + self.base_dn), attrs=["Member"])
+ self.assertEqual(str(res[0]["Member"][0]), self.get_user_dn(self.user_with_wp))
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+delete: Member"""
+ self.ldb_user.modify_ldif(ldif)
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: CN=test_modify_user2,CN=Users,""" + self.base_dn
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)"
+ % ("CN=test_modify_group2,CN=Users," + self.base_dn), attrs=["Member"])
+ self.assertEqual(str(res[0]["Member"][0]), "CN=test_modify_user2,CN=Users," + self.base_dn)
+
+ def test_modify_anonymous(self):
+ """Test add operation with anonymous user"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ self.ldb_admin.newuser("test_anonymous", "samba123@")
+ m = Message()
+ m.dn = Dn(anonymous, self.get_user_dn("test_anonymous"))
+
+ m["description"] = MessageElement("sambauser2",
+ FLAG_MOD_ADD,
+ "description")
+ try:
+ anonymous.modify(m)
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name(self):
+ '''Test modifying dNSHostName with validated write'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_no_validated_write(self):
+ '''Test modifying dNSHostName without validated write'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_invalid(self):
+ '''Test modifying dNSHostName to an invalid value'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = 'invalid'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_invalid_wp(self):
+ '''Test modifying dNSHostName to an invalid value when we have WP'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Write Property.
+ mod = (f'(OA;CI;WP;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = 'invalid'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_invalid_non_computer(self):
+ '''Test modifying dNSHostName to an invalid value on a non-computer'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'user',
+ 'sAMAccountName': f'{account_name}',
+ })
+
+ host_name = 'invalid'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_no_value(self):
+ '''Test modifying dNSHostName with validated write with no value'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement([],
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_OPERATIONS_ERROR, num)
+ else:
+ # Windows accepts this.
+ pass
+
+ def test_modify_dns_host_name_empty_string(self):
+ '''Test modifying dNSHostName with validated write of an empty string'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement('\0',
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_dollar(self):
+ '''Test modifying dNSHostName with validated write of a value including a dollar'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}$.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_account_no_dollar(self):
+ '''Test modifying dNSHostName with validated write with no dollar in sAMAccountName'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_no_suffix(self):
+ '''Test modifying dNSHostName with validated write of a value missing the suffix'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_wrong_prefix(self):
+ '''Test modifying dNSHostName with validated write of a value with the wrong prefix'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'invalid.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_wrong_suffix(self):
+ '''Test modifying dNSHostName with validated write of a value with the wrong suffix'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.invalid.example.com'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_case(self):
+ '''Test modifying dNSHostName with validated write of a value with irregular case'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+ host_name = host_name.capitalize()
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_allowed_suffixes(self):
+ '''Test modifying dNSHostName with validated write and an allowed suffix'''
+
+ allowed_suffix = 'suffix.that.is.allowed'
+
+ # Add the allowed suffix.
+
+ res = self.ldb_admin.search(self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=['msDS-AllowedDNSSuffixes'])
+ self.assertEqual(1, len(res))
+ old_allowed_suffixes = res[0].get('msDS-AllowedDNSSuffixes')
+
+ def modify_allowed_suffixes(suffixes):
+ if suffixes is None:
+ suffixes = []
+ flag = FLAG_MOD_DELETE
+ else:
+ flag = FLAG_MOD_REPLACE
+
+ m = Message(Dn(self.ldb_admin, self.base_dn))
+ m['msDS-AllowedDNSSuffixes'] = MessageElement(
+ suffixes,
+ flag,
+ 'msDS-AllowedDNSSuffixes')
+ self.ldb_admin.modify(m)
+
+ self.addCleanup(modify_allowed_suffixes, old_allowed_suffixes)
+
+ if old_allowed_suffixes is None:
+ allowed_suffixes = []
+ else:
+ allowed_suffixes = list(old_allowed_suffixes)
+
+ if (allowed_suffix not in allowed_suffixes and
+ allowed_suffix.encode('utf-8') not in allowed_suffixes):
+ allowed_suffixes.append(allowed_suffix)
+
+ modify_allowed_suffixes(allowed_suffixes)
+
+ # Create the account and run the test.
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{allowed_suffix}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_spn(self):
+ '''Test modifying dNSHostName and SPN with validated write'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+ spn = f'host/{host_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ m['1'] = MessageElement(spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_spn_matching_dns_host_name_invalid(self):
+ '''Test modifying SPN with validated write, matching a valid dNSHostName '''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Write Property.
+ mod = (f'(OA;CI;WP;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ invalid_host_name = 'invalid'
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+ spn = f'host/{host_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(invalid_host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ m['1'] = MessageElement(spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ m['2'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_spn_matching_dns_host_name_original(self):
+ '''Test modifying SPN with validated write, matching the original dNSHostName '''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ original_host_name = 'invalid_host_name'
+ original_spn = 'host/{original_host_name}'
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'dNSHostName': original_host_name,
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(original_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ m['1'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_spn_matching_account_name_original(self):
+ '''Test modifying dNSHostName and SPN with validated write, matching the original sAMAccountName'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ sam_account_name = '3e0abfd0-126a-11d0-a060-00aa006c33ed'
+
+ # Grant Write Property.
+ mod = (f'(OA;CI;WP;{sam_account_name};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ new_account_name = 'test_mod_hostname2'
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+ spn = f'host/{host_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ m['1'] = MessageElement(spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ m['2'] = MessageElement(f'{new_account_name}$',
+ FLAG_MOD_REPLACE,
+ 'sAMAccountName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_spn_matching_account_name_new(self):
+ '''Test modifying dNSHostName and SPN with validated write, matching the new sAMAccountName'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ sam_account_name = '3e0abfd0-126a-11d0-a060-00aa006c33ed'
+
+ # Grant Write Property.
+ mod = (f'(OA;CI;WP;{sam_account_name};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ new_account_name = 'test_mod_hostname2'
+ new_host_name = f'{new_account_name}.{self.ldb_user.domain_dns_name()}'
+ new_spn = f'host/{new_host_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(new_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ m['1'] = MessageElement(new_host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ m['2'] = MessageElement(f'{new_account_name}$',
+ FLAG_MOD_REPLACE,
+ 'sAMAccountName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+# enable these when we have search implemented
+
+
+class AclSearchTests(AclTests):
+
+ def setUp(self):
+ super(AclSearchTests, self).setUp()
+
+ # permit password changes during this test
+ PasswordCommon.allow_password_changes(self, self.ldb_admin)
+
+ self.u1 = "search_u1"
+ self.u2 = "search_u2"
+ self.u3 = "search_u3"
+ self.group1 = "group1"
+ self.ldb_admin.newuser(self.u1, self.user_pass)
+ self.ldb_admin.newuser(self.u2, self.user_pass)
+ self.ldb_admin.newuser(self.u3, self.user_pass)
+ self.ldb_admin.newgroup(self.group1, grouptype=samba.dsdb.GTYPE_SECURITY_GLOBAL_GROUP)
+ self.ldb_admin.add_remove_group_members(self.group1, [self.u2],
+ add_members_operation=True)
+ self.ldb_user = self.get_ldb_connection(self.u1, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.u2, self.user_pass)
+ self.ldb_user3 = self.get_ldb_connection(self.u3, self.user_pass)
+ self.full_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou3,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou4,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)]
+ self.user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.u1))
+ self.group_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.group1))
+
+ def create_clean_ou(self, object_dn):
+ """ Base repeating setup for unittests to follow """
+ res = self.ldb_admin.search(base=self.base_dn, scope=SCOPE_SUBTREE,
+ expression="distinguishedName=%s" % object_dn)
+ # Make sure top testing OU has been deleted before starting the test
+ self.assertEqual(len(res), 0)
+ self.ldb_admin.create_ou(object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ # Make sure there are inheritable ACEs initially
+ self.assertTrue("CI" in desc_sddl or "OI" in desc_sddl)
+ # Find and remove all inherit ACEs
+ res = re.findall(r"\(.*?\)", desc_sddl)
+ res = [x for x in res if ("CI" in x) or ("OI" in x)]
+ for x in res:
+ desc_sddl = desc_sddl.replace(x, "")
+ # Add flag 'protected' in both DACL and SACL so no inherit ACEs
+ # can propagate from above
+ # remove SACL, we are not interested
+ desc_sddl = desc_sddl.replace(":AI", ":AIP")
+ self.sd_utils.modify_sd_on_dn(object_dn, desc_sddl)
+ # Verify all inheritable ACEs are gone
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ self.assertFalse("CI" in desc_sddl)
+ self.assertFalse("OI" in desc_sddl)
+
+ def tearDown(self):
+ super(AclSearchTests, self).tearDown()
+ delete_force(self.ldb_admin, "OU=test_search_ou2,OU=test_search_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_search_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou4,OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou3,OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, self.get_user_dn("search_u1"))
+ delete_force(self.ldb_admin, self.get_user_dn("search_u2"))
+ delete_force(self.ldb_admin, self.get_user_dn("search_u3"))
+ delete_force(self.ldb_admin, self.get_user_dn("group1"))
+
+ del self.ldb_user
+ del self.ldb_user2
+ del self.ldb_user3
+
+ def test_search_anonymous1(self):
+ """Verify access of rootDSE with the correct request"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ res = anonymous.search("", expression="(objectClass=*)", scope=SCOPE_BASE)
+ self.assertEqual(len(res), 1)
+ # verify some of the attributes
+ # don't care about values
+ self.assertTrue("ldapServiceName" in res[0])
+ self.assertTrue("namingContexts" in res[0])
+ self.assertTrue("isSynchronized" in res[0])
+ self.assertTrue("dsServiceName" in res[0])
+ self.assertTrue("supportedSASLMechanisms" in res[0])
+ self.assertTrue("isGlobalCatalogReady" in res[0])
+ self.assertTrue("domainControllerFunctionality" in res[0])
+ self.assertTrue("serverName" in res[0])
+
+ def test_search_anonymous2(self):
+ """Make sure we cannot access anything else"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ try:
+ res = anonymous.search("", expression="(objectClass=*)", scope=SCOPE_SUBTREE)
+ except LdbError as e15:
+ (num, _) = e15.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+ try:
+ res = anonymous.search(self.base_dn, expression="(objectClass=*)", scope=SCOPE_SUBTREE)
+ except LdbError as e16:
+ (num, _) = e16.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+ try:
+ res = anonymous.search(anonymous.get_config_basedn(), expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ except LdbError as e17:
+ (num, _) = e17.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+
+ def test_search_anonymous3(self):
+ """Set dsHeuristics and repeat"""
+ self.ldb_admin.set_dsheuristics("0000002")
+ self.ldb_admin.create_ou("OU=test_search_ou1," + self.base_dn)
+ mod = "(A;CI;LC;;;AN)"
+ self.sd_utils.dacl_add_ace("OU=test_search_ou1," + self.base_dn, mod)
+ self.ldb_admin.create_ou("OU=test_search_ou2,OU=test_search_ou1," + self.base_dn)
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ res = anonymous.search("OU=test_search_ou2,OU=test_search_ou1," + self.base_dn,
+ expression="(objectClass=*)", scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+ self.assertTrue("dn" in res[0])
+ self.assertTrue(res[0]["dn"] == Dn(self.ldb_admin,
+ "OU=test_search_ou2,OU=test_search_ou1," + self.base_dn))
+ res = anonymous.search(anonymous.get_config_basedn(), expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+ self.assertTrue("dn" in res[0])
+ self.assertTrue(res[0]["dn"] == Dn(self.ldb_admin, self.configuration_dn))
+
+ def test_search1(self):
+ """Make sure users can see us if given LC to user and group"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;;LC;;;%s)(A;;LC;;;%s)" % (str(self.user_sid), str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+
+ # regular users must see only ou1 and ou2
+ res = self.ldb_user3.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 2)
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn)]
+
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ # these users should see all ous
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 6)
+ res_list = [x["dn"] for x in res if x["dn"] in self.full_list]
+ self.assertEqual(sorted(res_list), sorted(self.full_list))
+
+ res = self.ldb_user2.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 6)
+ res_list = [x["dn"] for x in res if x["dn"] in self.full_list]
+ self.assertEqual(sorted(res_list), sorted(self.full_list))
+
+ def test_search2(self):
+ """Make sure users can't see us if access is explicitly denied"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)
+ mod = "(D;;LC;;;%s)(D;;LC;;;%s)" % (str(self.user_sid), str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user3.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ # this user should see all ous
+ res_list = [x["dn"] for x in res if x["dn"] in self.full_list]
+ self.assertEqual(sorted(res_list), sorted(self.full_list))
+
+ # these users should see ou1, 2, 5 and 6 but not 3 and 4
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)]
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ res = self.ldb_user2.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 4)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def test_search3(self):
+ """Make sure users can't see ous if access is explicitly denied - 2"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;CI;LC;;;%s)(A;CI;LC;;;%s)" % (str(self.user_sid), str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+
+ print("Testing correct behavior on nonaccessible search base")
+ try:
+ self.ldb_user3.search("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_BASE)
+ except LdbError as e18:
+ (num, _) = e18.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ else:
+ self.fail()
+
+ mod = "(D;;LC;;;%s)(D;;LC;;;%s)" % (str(self.user_sid), str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn)]
+
+ res = self.ldb_user3.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)]
+
+ # should not see ou3 and ou4, but should see ou5 and ou6
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 4)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ res = self.ldb_user2.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 4)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def test_search4(self):
+ """There is no difference in visibility if the user is also creator"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;CI;CC;;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_user.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn)]
+ res = self.ldb_user3.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 2)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 2)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def test_search5(self):
+ """Make sure users can see only attributes they are allowed to see"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;CI;LC;;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ # assert user can only see dn
+ res = self.ldb_user.search("OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ ok_list = ['dn']
+ self.assertEqual(len(res), 1)
+ res_list = list(res[0].keys())
+ self.assertEqual(res_list, ok_list)
+
+ res = self.ldb_user.search("OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_BASE, attrs=["ou"])
+
+ self.assertEqual(len(res), 1)
+ res_list = list(res[0].keys())
+ self.assertEqual(res_list, ok_list)
+
+ # give read property on ou and assert user can only see dn and ou
+ mod = "(OA;;RP;bf9679f0-0de6-11d0-a285-00aa003049e2;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user.search("OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ ok_list = ['dn', 'ou']
+ self.assertEqual(len(res), 1)
+ res_list = list(res[0].keys())
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ # give read property on Public Information and assert user can see ou and other members
+ mod = "(OA;;RP;e48d0154-bcf8-11d1-8702-00c04fb96050;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user.search("OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+
+ ok_list = ['dn', 'objectClass', 'ou', 'distinguishedName', 'name', 'objectGUID', 'objectCategory']
+ res_list = list(res[0].keys())
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def test_search6(self):
+ """If an attribute that cannot be read is used in a filter, it is as if the attribute does not exist"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;CI;LCCC;;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(ou=ou3)",
+ scope=SCOPE_SUBTREE)
+ # nothing should be returned as ou is not accessible
+ self.assertEqual(len(res), 0)
+
+ # give read property on ou and assert user can only see dn and ou
+ mod = "(OA;;RP;bf9679f0-0de6-11d0-a285-00aa003049e2;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(ou=ou3)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+ ok_list = ['dn', 'ou']
+ res_list = list(res[0].keys())
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ # give read property on Public Information and assert user can see ou and other members
+ mod = "(OA;;RP;e48d0154-bcf8-11d1-8702-00c04fb96050;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(ou=ou2)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+ ok_list = ['dn', 'objectClass', 'ou', 'distinguishedName', 'name', 'objectGUID', 'objectCategory']
+ res_list = list(res[0].keys())
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def assert_search_on_attr(self, dn, samdb, attr, expected_list):
+
+ expected_num = len(expected_list)
+ res = samdb.search(dn, expression="(%s=*)" % attr, scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), expected_num)
+
+ res_list = [ x["dn"] for x in res if x["dn"] in expected_list ]
+ self.assertEqual(sorted(res_list), sorted(expected_list))
+
+ def test_search7(self):
+ """Checks object search visibility when users don't have full rights"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;;LC;;;%s)(A;;LC;;;%s)" % (str(self.user_sid),
+ str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn,
+ sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn,
+ sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn,
+ sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn,
+ sd=tmp_desc)
+
+ ou2_dn = Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn)
+ ou1_dn = Dn(self.ldb_admin, "OU=ou1," + self.base_dn)
+
+ # even though unprivileged users can't read these attributes for OU2,
+ # the object should still be visible in searches, because they have
+ # 'List Contents' rights still. This isn't really disclosive because
+ # ALL objects have these attributes
+ visible_attrs = ["objectClass", "distinguishedName", "name",
+ "objectGUID"]
+ two_objects = [ou2_dn, ou1_dn]
+
+ for attr in visible_attrs:
+ # a regular user should just see the 2 objects
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user3, attr,
+ expected_list=two_objects)
+
+ # whereas the following users have LC rights for all the objects,
+ # so they should see them all
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user, attr,
+ expected_list=self.full_list)
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user2, attr,
+ expected_list=self.full_list)
+
+ # however when searching on the following attributes, objects will not
+ # be visible unless the user has Read Property rights
+ hidden_attrs = ["objectCategory", "instanceType", "ou", "uSNChanged",
+ "uSNCreated", "whenCreated"]
+ one_object = [ou1_dn]
+
+ for attr in hidden_attrs:
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user3, attr,
+ expected_list=one_object)
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user, attr,
+ expected_list=one_object)
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user2, attr,
+ expected_list=one_object)
+
+ # admin has RP rights so can still see all the objects
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_admin, attr,
+ expected_list=self.full_list)
+
+
+# tests on ldap delete operations
+
+
+class AclDeleteTests(AclTests):
+
+ def setUp(self):
+ super(AclDeleteTests, self).setUp()
+ self.regular_user = "acl_delete_user1"
+ # Create regular user
+ self.ldb_admin.newuser(self.regular_user, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.regular_user, self.user_pass)
+
+ def tearDown(self):
+ super(AclDeleteTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn("test_delete_user1"))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+ delete_force(self.ldb_admin, self.get_user_dn("test_anonymous"))
+
+ del self.ldb_user
+
+ def test_delete_u1(self):
+ """User is prohibited by default to delete another User object"""
+ # Create user that we try to delete
+ self.ldb_admin.newuser("test_delete_user1", self.user_pass)
+ # Here delete User object should ALWAYS through exception
+ try:
+ self.ldb_user.delete(self.get_user_dn("test_delete_user1"))
+ except LdbError as e19:
+ (num, _) = e19.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_delete_u2(self):
+ """User's group has RIGHT_DELETE to another User object"""
+ user_dn = self.get_user_dn("test_delete_user1")
+ # Create user that we try to delete
+ self.ldb_admin.newuser("test_delete_user1", self.user_pass)
+ mod = "(A;;SD;;;AU)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Try to delete User object
+ self.ldb_user.delete(user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+
+ def test_delete_u3(self):
+ """User identified by SID has RIGHT_DELETE to another User object"""
+ user_dn = self.get_user_dn("test_delete_user1")
+ # Create user that we try to delete
+ self.ldb_admin.newuser("test_delete_user1", self.user_pass)
+ mod = "(A;;SD;;;%s)" % self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Try to delete User object
+ self.ldb_user.delete(user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+
+ def test_delete_anonymous(self):
+ """Test add operation with anonymous user"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ self.ldb_admin.newuser("test_anonymous", "samba123@")
+
+ try:
+ anonymous.delete(self.get_user_dn("test_anonymous"))
+ except LdbError as e20:
+ (num, _) = e20.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+
+# tests on ldap rename operations
+
+
+class AclRenameTests(AclTests):
+
+ def setUp(self):
+ super(AclRenameTests, self).setUp()
+ self.regular_user = "acl_rename_user1"
+ self.ou1 = "OU=test_rename_ou1"
+ self.ou2 = "OU=test_rename_ou2"
+ self.ou3 = "OU=test_rename_ou3,%s" % self.ou2
+ self.testuser1 = "test_rename_user1"
+ self.testuser2 = "test_rename_user2"
+ self.testuser3 = "test_rename_user3"
+ self.testuser4 = "test_rename_user4"
+ self.testuser5 = "test_rename_user5"
+ # Create regular user
+ self.ldb_admin.newuser(self.regular_user, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.regular_user, self.user_pass)
+
+ def tearDown(self):
+ super(AclRenameTests, self).tearDown()
+ # Rename OU3
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser1, self.ou3, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser2, self.ou3, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser5, self.ou3, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou3, self.base_dn))
+ # Rename OU2
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser1, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser2, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser5, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou2, self.base_dn))
+ # Rename OU1
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser1, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser2, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser5, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "OU=test_rename_ou3,%s,%s" % (self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+
+ del self.ldb_user
+
+ def test_rename_u1(self):
+ """Regular user fails to rename 'User object' within single OU"""
+ # Create OU structure
+ self.ldb_admin.create_ou("OU=test_rename_ou1," + self.base_dn)
+ self.ldb_admin.newuser(self.testuser1, self.user_pass, userou=self.ou1)
+ try:
+ self.ldb_user.rename("CN=%s,%s,%s" % (self.testuser1, self.ou1, self.base_dn),
+ "CN=%s,%s,%s" % (self.testuser5, self.ou1, self.base_dn))
+ except LdbError as e21:
+ (num, _) = e21.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_rename_u2(self):
+ """Grant WRITE_PROPERTY to AU so regular user can rename 'User object' within single OU"""
+ ou_dn = "OU=test_rename_ou1," + self.base_dn
+ user_dn = "CN=test_rename_user1," + ou_dn
+ rename_user_dn = "CN=test_rename_user5," + ou_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou_dn)
+ self.ldb_admin.newuser(self.testuser1, self.user_pass, userou=self.ou1)
+ mod = "(A;;WP;;;AU)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Rename 'User object' having WP to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u3(self):
+ """Test rename with rights granted to 'User object' SID"""
+ ou_dn = "OU=test_rename_ou1," + self.base_dn
+ user_dn = "CN=test_rename_user1," + ou_dn
+ rename_user_dn = "CN=test_rename_user5," + ou_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou_dn)
+ self.ldb_admin.newuser(self.testuser1, self.user_pass, userou=self.ou1)
+ sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(A;;WP;;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Rename 'User object' having WP to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u4(self):
+ """Rename 'User object' cross OU with WP, SD and CC right granted on reg. user to AU"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user5," + ou2_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ mod = "(A;;WPSD;;;AU)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ # Rename 'User object' having SD and CC to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u5(self):
+ """Test rename with rights granted to 'User object' SID"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user5," + ou2_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(A;;WPSD;;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ mod = "(A;;CC;;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ # Rename 'User object' having SD and CC to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u6(self):
+ """Rename 'User object' cross OU with WP, DC and CC right granted on OU & user to AU"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user2," + ou2_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ #mod = "(A;CI;DCWP;;;AU)"
+ mod = "(A;;DC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou1_dn, mod)
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ mod = "(A;;WP;;;AU)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Rename 'User object' having SD and CC to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u7(self):
+ """Rename 'User object' cross OU (second level) with WP, DC and CC right granted on OU to AU"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ ou3_dn = "OU=test_rename_ou3," + ou2_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user5," + ou3_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ self.ldb_admin.create_ou(ou3_dn)
+ mod = "(A;CI;WPDC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou1_dn, mod)
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou3_dn, mod)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ # Rename 'User object' having SD and CC to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u8(self):
+ """Test rename on an object with and without modify access on the RDN attribute"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + ou1_dn
+ ou3_dn = "OU=test_rename_ou3," + ou1_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(OA;;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ mod = "(OD;;WP;bf9679f0-0de6-11d0-a285-00aa003049e2;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ try:
+ self.ldb_user.rename(ou2_dn, ou3_dn)
+ except LdbError as e22:
+ (num, _) = e22.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This rename operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+ sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(A;;WP;bf9679f0-0de6-11d0-a285-00aa003049e2;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ self.ldb_user.rename(ou2_dn, ou3_dn)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % ou2_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % ou3_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u9(self):
+ """Rename 'User object' cross OU, with explicit deny on sd and dc"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user5," + ou2_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ mod = "(D;;SD;;;DA)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ mod = "(D;;DC;;;DA)"
+ self.sd_utils.dacl_add_ace(ou1_dn, mod)
+ # Rename 'User object' having SD and CC to AU
+ try:
+ self.ldb_admin.rename(user_dn, rename_user_dn)
+ except LdbError as e23:
+ (num, _) = e23.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ # add an allow ace so we can delete this ou
+ mod = "(A;;DC;;;DA)"
+ self.sd_utils.dacl_add_ace(ou1_dn, mod)
+
+
+# tests on Control Access Rights
+class AclCARTests(AclTests):
+
+ def setUp(self):
+ super(AclCARTests, self).setUp()
+
+ # Get the old "dSHeuristics" if it was set
+ dsheuristics = self.ldb_admin.get_dsheuristics()
+ # Reset the "dSHeuristics" as they were before
+ self.addCleanup(self.ldb_admin.set_dsheuristics, dsheuristics)
+ # Set the "dSHeuristics" to activate the correct "userPassword" behaviour
+ self.ldb_admin.set_dsheuristics("000000001")
+ # Get the old "minPwdAge"
+ minPwdAge = self.ldb_admin.get_minPwdAge()
+ # Reset the "minPwdAge" as it was before
+ self.addCleanup(self.ldb_admin.set_minPwdAge, minPwdAge)
+ # Set it temporarily to "0"
+ self.ldb_admin.set_minPwdAge("0")
+
+ self.user_with_wp = "acl_car_user1"
+ self.user_with_pc = "acl_car_user2"
+ self.ldb_admin.newuser(self.user_with_wp, self.user_pass)
+ self.ldb_admin.newuser(self.user_with_pc, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.user_with_wp, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.user_with_pc, self.user_pass)
+
+ def tearDown(self):
+ super(AclCARTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_wp))
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_pc))
+
+ del self.ldb_user
+ del self.ldb_user2
+
+ def test_change_password1(self):
+ """Try a password change operation without any CARs given"""
+ # users have change password by default - remove for negative testing
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;WD)", "")
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;PS)", "")
+ self.sd_utils.modify_sd_on_dn(self.get_user_dn(self.user_with_wp), sddl)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e24:
+ (num, _) = e24.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ # for some reason we get constraint violation instead of insufficient access error
+ self.fail()
+
+ def test_change_password2(self):
+ """Make sure WP has no influence"""
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;WD)", "")
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;PS)", "")
+ self.sd_utils.modify_sd_on_dn(self.get_user_dn(self.user_with_wp), sddl)
+ mod = "(A;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e25:
+ (num, _) = e25.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ # for some reason we get constraint violation instead of insufficient access error
+ self.fail()
+
+ def test_change_password3(self):
+ """Make sure WP has no influence"""
+ mod = "(D;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ def test_change_password5(self):
+ """Make sure rights have no influence on dBCSPwd"""
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;WD)", "")
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;PS)", "")
+ self.sd_utils.modify_sd_on_dn(self.get_user_dn(self.user_with_wp), sddl)
+ mod = "(D;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: dBCSPwd
+dBCSPwd: XXXXXXXXXXXXXXXX
+add: dBCSPwd
+dBCSPwd: YYYYYYYYYYYYYYYY
+""")
+ except LdbError as e26:
+ (num, _) = e26.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ else:
+ self.fail()
+
+ def test_change_password6(self):
+ """Test uneven delete/adds"""
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ except LdbError as e27:
+ (num, _) = e27.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ mod = "(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ # This fails on Windows 2000 domain level with constraint violation
+ except LdbError as e28:
+ (num, _) = e28.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_UNWILLING_TO_PERFORM)
+ else:
+ self.fail()
+
+ def test_change_password7(self):
+ """Try a password change operation without any CARs given"""
+ # users have change password by default - remove for negative testing
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(self.get_user_dn(self.user_with_wp), sddl)
+ # first change our own password
+ self.ldb_user2.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_pc) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ # then someone else's
+ self.ldb_user2.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ def test_reset_password1(self):
+ """Try a user password reset operation (unicodePwd) before and after granting CAR"""
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e29:
+ (num, _) = e29.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ mod = "(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ def test_reset_password2(self):
+ """Try a user password reset operation (userPassword) before and after granting CAR"""
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ except LdbError as e30:
+ (num, _) = e30.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ mod = "(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ # This fails on Windows 2000 domain level with constraint violation
+ except LdbError as e31:
+ (num, _) = e31.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ pass # Not self.fail() as we normally want success.
+
+ def test_reset_password3(self):
+ """Grant WP and see what happens (unicodePwd)"""
+ mod = "(A;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e32:
+ (num, _) = e32.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_reset_password4(self):
+ """Grant WP and see what happens (userPassword)"""
+ mod = "(A;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ except LdbError as e33:
+ (num, _) = e33.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_reset_password5(self):
+ """Explicitly deny WP but grant CAR (unicodePwd)"""
+ mod = "(D;;WP;;;PS)(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ def test_reset_password6(self):
+ """Explicitly deny WP but grant CAR (userPassword)"""
+ mod = "(D;;WP;;;PS)(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ # This fails on Windows 2000 domain level with constraint violation
+ except LdbError as e34:
+ (num, _) = e34.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ pass # Not self.fail() as we normally want success
+
+
+class AclExtendedTests(AclTests):
+
+ def setUp(self):
+ super(AclExtendedTests, self).setUp()
+ # regular user, will be the creator
+ self.u1 = "ext_u1"
+ # regular user
+ self.u2 = "ext_u2"
+ # admin user
+ self.u3 = "ext_u3"
+ self.ldb_admin.newuser(self.u1, self.user_pass)
+ self.ldb_admin.newuser(self.u2, self.user_pass)
+ self.ldb_admin.newuser(self.u3, self.user_pass)
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.u3],
+ add_members_operation=True)
+ self.ldb_user1 = self.get_ldb_connection(self.u1, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.u2, self.user_pass)
+ self.ldb_user3 = self.get_ldb_connection(self.u3, self.user_pass)
+ self.user_sid1 = self.sd_utils.get_object_sid(self.get_user_dn(self.u1))
+ self.user_sid2 = self.sd_utils.get_object_sid(self.get_user_dn(self.u2))
+
+ def tearDown(self):
+ super(AclExtendedTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn(self.u1))
+ delete_force(self.ldb_admin, self.get_user_dn(self.u2))
+ delete_force(self.ldb_admin, self.get_user_dn(self.u3))
+ delete_force(self.ldb_admin, "CN=ext_group1,OU=ext_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "ou=ext_ou1," + self.base_dn)
+
+ del self.ldb_user1
+ del self.ldb_user2
+ del self.ldb_user3
+
+ def test_ntSecurityDescriptor(self):
+ # create empty ou
+ self.ldb_admin.create_ou("ou=ext_ou1," + self.base_dn)
+ # give u1 Create children access
+ mod = "(A;;CC;;;%s)" % str(self.user_sid1)
+ self.sd_utils.dacl_add_ace("OU=ext_ou1," + self.base_dn, mod)
+ mod = "(A;;LC;;;%s)" % str(self.user_sid2)
+ self.sd_utils.dacl_add_ace("OU=ext_ou1," + self.base_dn, mod)
+ # create a group under that, grant RP to u2
+ self.ldb_user1.newgroup("ext_group1", groupou="OU=ext_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ mod = "(A;;RP;;;%s)" % str(self.user_sid2)
+ self.sd_utils.dacl_add_ace("CN=ext_group1,OU=ext_ou1," + self.base_dn, mod)
+ # u2 must not read the descriptor
+ res = self.ldb_user2.search("CN=ext_group1,OU=ext_ou1," + self.base_dn,
+ SCOPE_BASE, None, ["nTSecurityDescriptor"])
+ self.assertNotEqual(len(res), 0)
+ self.assertFalse("nTSecurityDescriptor" in res[0].keys())
+ # grant RC to u2 - still no access
+ mod = "(A;;RC;;;%s)" % str(self.user_sid2)
+ self.sd_utils.dacl_add_ace("CN=ext_group1,OU=ext_ou1," + self.base_dn, mod)
+ res = self.ldb_user2.search("CN=ext_group1,OU=ext_ou1," + self.base_dn,
+ SCOPE_BASE, None, ["nTSecurityDescriptor"])
+ self.assertNotEqual(len(res), 0)
+ self.assertFalse("nTSecurityDescriptor" in res[0].keys())
+ # u3 is member of administrators group, should be able to read sd
+ res = self.ldb_user3.search("CN=ext_group1,OU=ext_ou1," + self.base_dn,
+ SCOPE_BASE, None, ["nTSecurityDescriptor"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("nTSecurityDescriptor" in res[0].keys())
+
+
+class AclUndeleteTests(AclTests):
+
+ def setUp(self):
+ super(AclUndeleteTests, self).setUp()
+ self.regular_user = "undeleter1"
+ self.ou1 = "OU=undeleted_ou,"
+ self.testuser1 = "to_be_undeleted1"
+ self.testuser2 = "to_be_undeleted2"
+ self.testuser3 = "to_be_undeleted3"
+ self.testuser4 = "to_be_undeleted4"
+ self.testuser5 = "to_be_undeleted5"
+ self.testuser6 = "to_be_undeleted6"
+
+ self.new_dn_ou = "CN=" + self.testuser4 + "," + self.ou1 + self.base_dn
+
+ # Create regular user
+ self.testuser1_dn = self.get_user_dn(self.testuser1)
+ self.testuser2_dn = self.get_user_dn(self.testuser2)
+ self.testuser3_dn = self.get_user_dn(self.testuser3)
+ self.testuser4_dn = self.get_user_dn(self.testuser4)
+ self.testuser5_dn = self.get_user_dn(self.testuser5)
+ self.deleted_dn1 = self.create_delete_user(self.testuser1)
+ self.deleted_dn2 = self.create_delete_user(self.testuser2)
+ self.deleted_dn3 = self.create_delete_user(self.testuser3)
+ self.deleted_dn4 = self.create_delete_user(self.testuser4)
+ self.deleted_dn5 = self.create_delete_user(self.testuser5)
+
+ self.ldb_admin.create_ou(self.ou1 + self.base_dn)
+
+ self.ldb_admin.newuser(self.regular_user, self.user_pass)
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.regular_user],
+ add_members_operation=True)
+ self.ldb_user = self.get_ldb_connection(self.regular_user, self.user_pass)
+ self.sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+
+ def tearDown(self):
+ super(AclUndeleteTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser1))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser2))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser3))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser4))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser5))
+ delete_force(self.ldb_admin, self.new_dn_ou)
+ delete_force(self.ldb_admin, self.ou1 + self.base_dn)
+
+ del self.ldb_user
+
+ def GUID_string(self, guid):
+ return get_string(ldb.schema_format_value("objectGUID", guid))
+
+ def create_delete_user(self, new_user):
+ self.ldb_admin.newuser(new_user, self.user_pass)
+
+ res = self.ldb_admin.search(expression="(objectClass=*)",
+ base=self.get_user_dn(new_user),
+ scope=SCOPE_BASE,
+ controls=["show_deleted:1"])
+ guid = res[0]["objectGUID"][0]
+ self.ldb_admin.delete(self.get_user_dn(new_user))
+ res = self.ldb_admin.search(base="<GUID=%s>" % self.GUID_string(guid),
+ scope=SCOPE_BASE, controls=["show_deleted:1"])
+ self.assertEqual(len(res), 1)
+ return str(res[0].dn)
+
+ def undelete_deleted(self, olddn, newdn):
+ msg = Message()
+ msg.dn = Dn(self.ldb_user, olddn)
+ msg["isDeleted"] = MessageElement([], FLAG_MOD_DELETE, "isDeleted")
+ msg["distinguishedName"] = MessageElement([newdn], FLAG_MOD_REPLACE, "distinguishedName")
+ res = self.ldb_user.modify(msg, ["show_recycled:1"])
+
+ def undelete_deleted_with_mod(self, olddn, newdn):
+ msg = Message()
+ msg.dn = Dn(ldb, olddn)
+ msg["isDeleted"] = MessageElement([], FLAG_MOD_DELETE, "isDeleted")
+ msg["distinguishedName"] = MessageElement([newdn], FLAG_MOD_REPLACE, "distinguishedName")
+ msg["url"] = MessageElement(["www.samba.org"], FLAG_MOD_REPLACE, "url")
+ res = self.ldb_user.modify(msg, ["show_deleted:1"])
+
+ def test_undelete(self):
+ # it appears the user has to have LC on the old parent to be able to move the object
+ # otherwise we get no such object. Since only System can modify the SD on deleted object
+ # we cannot grant this permission via LDAP, and this leaves us with "negative" tests at the moment
+
+ # deny write property on rdn, should fail
+ mod = "(OD;;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn1, mod)
+ try:
+ self.undelete_deleted(self.deleted_dn1, self.testuser1_dn)
+ self.fail()
+ except LdbError as e35:
+ (num, _) = e35.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ # seems that permissions on isDeleted and distinguishedName are irrelevant
+ mod = "(OD;;WP;bf96798f-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn2, mod)
+ mod = "(OD;;WP;bf9679e4-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn2, mod)
+ self.undelete_deleted(self.deleted_dn2, self.testuser2_dn)
+
+ # attempt undelete with simultaneous addition of url, WP to which is denied
+ mod = "(OD;;WP;9a9a0221-4a5b-11d1-a9c3-0000f80367c1;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn3, mod)
+ try:
+ self.undelete_deleted_with_mod(self.deleted_dn3, self.testuser3_dn)
+ self.fail()
+ except LdbError as e36:
+ (num, _) = e36.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ # undelete in an ou, in which we have no right to create children
+ mod = "(D;;CC;;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.ou1 + self.base_dn, mod)
+ try:
+ self.undelete_deleted(self.deleted_dn4, self.new_dn_ou)
+ self.fail()
+ except LdbError as e37:
+ (num, _) = e37.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ # delete is not required
+ mod = "(D;;SD;;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn5, mod)
+ self.undelete_deleted(self.deleted_dn5, self.testuser5_dn)
+
+ # deny Reanimate-Tombstone, should fail
+ mod = "(OD;;CR;45ec5156-db7e-47bb-b53f-dbeb2d03c40f;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.base_dn, mod)
+ try:
+ self.undelete_deleted(self.deleted_dn4, self.testuser4_dn)
+ self.fail()
+ except LdbError as e38:
+ (num, _) = e38.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+
+class AclSPNTests(AclTests):
+
+ def setUp(self):
+ super(AclSPNTests, self).setUp()
+ self.dcname = "TESTSRV8"
+ self.rodcname = "TESTRODC8"
+ self.computername = "testcomp8"
+ self.test_user = "spn_test_user8"
+ self.computerdn = "CN=%s,CN=computers,%s" % (self.computername, self.base_dn)
+ self.user_object = "user_with_spn"
+ self.user_object_dn = "CN=%s,CN=Users,%s" % (self.user_object, self.base_dn)
+ self.dc_dn = "CN=%s,OU=Domain Controllers,%s" % (self.dcname, self.base_dn)
+ self.site = "Default-First-Site-Name"
+ self.rodcctx = DCJoinContext(server=host, creds=creds, lp=lp,
+ site=self.site, netbios_name=self.rodcname,
+ targetdir=None, domain=None)
+ self.dcctx = DCJoinContext(server=host, creds=creds, lp=lp,
+ site=self.site, netbios_name=self.dcname,
+ targetdir=None, domain=None)
+ self.ldb_admin.newuser(self.test_user, self.user_pass)
+ self.ldb_user1 = self.get_ldb_connection(self.test_user, self.user_pass)
+ self.user_sid1 = self.sd_utils.get_object_sid(self.get_user_dn(self.test_user))
+ self.create_computer(self.computername, self.dcctx.dnsdomain)
+ self.create_rodc(self.rodcctx)
+ self.create_dc(self.dcctx)
+
+ def tearDown(self):
+ super(AclSPNTests, self).tearDown()
+ self.rodcctx.cleanup_old_join()
+ self.dcctx.cleanup_old_join()
+ delete_force(self.ldb_admin, "cn=%s,cn=computers,%s" % (self.computername, self.base_dn))
+ delete_force(self.ldb_admin, self.get_user_dn(self.test_user))
+ delete_force(self.ldb_admin, self.user_object_dn)
+
+ del self.ldb_user1
+
+ def replace_spn(self, _ldb, dn, spn):
+ print("Setting spn %s on %s" % (spn, dn))
+ res = self.ldb_admin.search(dn, expression="(objectClass=*)",
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ if "servicePrincipalName" in res[0].keys():
+ flag = FLAG_MOD_REPLACE
+ else:
+ flag = FLAG_MOD_ADD
+
+ msg = Message()
+ msg.dn = Dn(self.ldb_admin, dn)
+ msg["servicePrincipalName"] = MessageElement(spn, flag,
+ "servicePrincipalName")
+ _ldb.modify(msg)
+
+ def create_computer(self, computername, domainname):
+ dn = "CN=%s,CN=computers,%s" % (computername, self.base_dn)
+ samaccountname = computername + "$"
+ dnshostname = "%s.%s" % (computername, domainname)
+ self.ldb_admin.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "dNSHostName": dnshostname})
+
+ # same as for join_RODC, but do not set any SPNs
+ def create_rodc(self, ctx):
+ ctx.nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn]
+ ctx.full_nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn]
+ ctx.krbtgt_dn = "CN=krbtgt_%s,CN=Users,%s" % (ctx.myname, ctx.base_dn)
+
+ ctx.never_reveal_sid = ["<SID=%s-%s>" % (ctx.domsid, security.DOMAIN_RID_RODC_DENY),
+ "<SID=%s>" % security.SID_BUILTIN_ADMINISTRATORS,
+ "<SID=%s>" % security.SID_BUILTIN_SERVER_OPERATORS,
+ "<SID=%s>" % security.SID_BUILTIN_BACKUP_OPERATORS,
+ "<SID=%s>" % security.SID_BUILTIN_ACCOUNT_OPERATORS]
+ ctx.reveal_sid = "<SID=%s-%s>" % (ctx.domsid, security.DOMAIN_RID_RODC_ALLOW)
+
+ mysid = ctx.get_mysid()
+ admin_dn = "<SID=%s>" % mysid
+ ctx.managedby = admin_dn
+
+ ctx.userAccountControl = (samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
+ samba.dsdb.UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION |
+ samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT)
+
+ ctx.connection_dn = "CN=RODC Connection (FRS),%s" % ctx.ntds_dn
+ ctx.secure_channel_type = misc.SEC_CHAN_RODC
+ ctx.RODC = True
+ ctx.replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC |
+ drsuapi.DRSUAPI_DRS_PER_SYNC |
+ drsuapi.DRSUAPI_DRS_GET_ANC |
+ drsuapi.DRSUAPI_DRS_NEVER_SYNCED |
+ drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING)
+
+ ctx.join_add_objects()
+
+ def create_dc(self, ctx):
+ ctx.nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn]
+ ctx.full_nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn]
+ ctx.userAccountControl = samba.dsdb.UF_SERVER_TRUST_ACCOUNT | samba.dsdb.UF_TRUSTED_FOR_DELEGATION
+ ctx.secure_channel_type = misc.SEC_CHAN_BDC
+ ctx.replica_flags = (drsuapi.DRSUAPI_DRS_WRIT_REP |
+ drsuapi.DRSUAPI_DRS_INIT_SYNC |
+ drsuapi.DRSUAPI_DRS_PER_SYNC |
+ drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS |
+ drsuapi.DRSUAPI_DRS_NEVER_SYNCED)
+
+ ctx.join_add_objects()
+
+ def dc_spn_test(self, ctx):
+ netbiosdomain = self.dcctx.get_domain_name()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s/%s" % (ctx.myname, netbiosdomain))
+ except LdbError as e39:
+ (num, _) = e39.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ mod = "(OA;;SW;f3a64788-5306-11d1-a9c5-0000f80367c1;;%s)" % str(self.user_sid1)
+ self.sd_utils.dacl_add_ace(ctx.acct_dn, mod)
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s/%s" % (ctx.myname, netbiosdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s" % (ctx.myname))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, netbiosdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s/%s" % (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "GC/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsforest))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s/%s" % (ctx.myname, netbiosdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, netbiosdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s" % (ctx.myname))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s/%s" % (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "DNS/%s/%s" % (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "RestrictedKrbHost/%s/%s" %
+ (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "RestrictedKrbHost/%s" %
+ (ctx.myname))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "Dfsr-12F9A27C-BF97-4787-9364-D31B6C55EB04/%s/%s" %
+ (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "NtFrs-88f5d2bd-b646-11d2-a6d3-00c04fc9b232/%s/%s" %
+ (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s._msdcs.%s" %
+ (ctx.ntds_guid, ctx.dnsdomain))
+
+ # the following spns do not match the restrictions and should fail
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s.%s/ForestDnsZones.%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsdomain))
+ except LdbError as e40:
+ (num, _) = e40.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s.%s/DomainDnsZones.%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsdomain))
+ except LdbError as e41:
+ (num, _) = e41.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "nosuchservice/%s/%s" % ("abcd", "abcd"))
+ except LdbError as e42:
+ (num, _) = e42.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "GC/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, netbiosdomain))
+ except LdbError as e43:
+ (num, _) = e43.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "E3514235-4B06-11D1-AB04-00C04FC2DCD2/%s/%s" %
+ (ctx.ntds_guid, ctx.dnsdomain))
+ except LdbError as e44:
+ (num, _) = e44.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def test_computer_spn(self):
+ # with WP, any value can be set
+ netbiosdomain = self.dcctx.get_domain_name()
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s/%s" %
+ (self.computername, netbiosdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s" % (self.computername))
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, netbiosdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "GC/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsforest))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s/%s" % (self.computername, netbiosdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s.%s/ForestDnsZones.%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s.%s/DomainDnsZones.%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, netbiosdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s" % (self.computername))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "DNS/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "RestrictedKrbHost/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "RestrictedKrbHost/%s" %
+ (self.computername))
+ self.replace_spn(self.ldb_admin, self.computerdn, "Dfsr-12F9A27C-BF97-4787-9364-D31B6C55EB04/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "NtFrs-88f5d2bd-b646-11d2-a6d3-00c04fc9b232/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "nosuchservice/%s/%s" % ("abcd", "abcd"))
+
+ # user has neither WP nor Validated-SPN, access denied expected
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s/%s" % (self.computername, netbiosdomain))
+ except LdbError as e45:
+ (num, _) = e45.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ mod = "(OA;;SW;f3a64788-5306-11d1-a9c5-0000f80367c1;;%s)" % str(self.user_sid1)
+ self.sd_utils.dacl_add_ace(self.computerdn, mod)
+ # grant Validated-SPN and check which values are accepted
+ # see 3.1.1.5.3.1.1.4 servicePrincipalName for reference
+
+ # for regular computer objects we shouldalways get constraint violation
+
+ # This does not pass against Windows, although it should according to docs
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s" % (self.computername))
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s.%s" %
+ (self.computername, self.dcctx.dnsdomain))
+
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s/%s" % (self.computername, netbiosdomain))
+ except LdbError as e46:
+ (num, _) = e46.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, netbiosdomain))
+ except LdbError as e47:
+ (num, _) = e47.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ except LdbError as e48:
+ (num, _) = e48.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ except LdbError as e49:
+ (num, _) = e49.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "GC/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsforest))
+ except LdbError as e50:
+ (num, _) = e50.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "ldap/%s/%s" % (self.computername, netbiosdomain))
+ except LdbError as e51:
+ (num, _) = e51.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "ldap/%s.%s/ForestDnsZones.%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ except LdbError as e52:
+ (num, _) = e52.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def test_spn_rwdc(self):
+ self.dc_spn_test(self.dcctx)
+
+ def test_spn_rodc(self):
+ self.dc_spn_test(self.rodcctx)
+
+ def test_user_spn(self):
+ #grant SW to a regular user and try to set the spn on a user object
+ #should get ERR_INSUFFICIENT_ACCESS_RIGHTS, since Validate-SPN only applies to computer
+ self.ldb_admin.newuser(self.user_object, self.user_pass)
+ mod = "(OA;;SW;f3a64788-5306-11d1-a9c5-0000f80367c1;;%s)" % str(self.user_sid1)
+ self.sd_utils.dacl_add_ace(self.user_object_dn, mod)
+ try:
+ self.replace_spn(self.ldb_user1, self.user_object_dn, "nosuchservice/%s/%s" % ("abcd", "abcd"))
+ except LdbError as e60:
+ (num, _) = e60.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_delete_add_spn(self):
+ # Grant Validated-SPN property.
+ mod = f'(OA;;SW;{security.GUID_DRS_VALIDATE_SPN};;{self.user_sid1})'
+ self.sd_utils.dacl_add_ace(self.computerdn, mod)
+
+ spn_base = f'HOST/{self.computername}'
+
+ allowed_spn = f'{spn_base}.{self.dcctx.dnsdomain}'
+ not_allowed_spn = f'{spn_base}/{self.dcctx.get_domain_name()}'
+
+ # Ensure we are able to add an allowed SPN.
+ msg = Message(Dn(self.ldb_user1, self.computerdn))
+ msg['servicePrincipalName'] = MessageElement(allowed_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ self.ldb_user1.modify(msg)
+
+ # Ensure we are not able to add a disallowed SPN.
+ msg = Message(Dn(self.ldb_user1, self.computerdn))
+ msg['servicePrincipalName'] = MessageElement(not_allowed_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ try:
+ self.ldb_user1.modify(msg)
+ except LdbError as e:
+ num, _ = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail(f'able to add disallowed SPN {not_allowed_spn}')
+
+ # Ensure that deleting an existing SPN followed by adding a disallowed
+ # SPN fails.
+ msg = Message(Dn(self.ldb_user1, self.computerdn))
+ msg['0'] = MessageElement([],
+ FLAG_MOD_DELETE,
+ 'servicePrincipalName')
+ msg['1'] = MessageElement(not_allowed_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ try:
+ self.ldb_user1.modify(msg)
+ except LdbError as e:
+ num, _ = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail(f'able to add disallowed SPN {not_allowed_spn}')
+
+ def test_delete_disallowed_spn(self):
+ # Grant Validated-SPN property.
+ mod = f'(OA;;SW;{security.GUID_DRS_VALIDATE_SPN};;{self.user_sid1})'
+ self.sd_utils.dacl_add_ace(self.computerdn, mod)
+
+ spn_base = f'HOST/{self.computername}'
+
+ not_allowed_spn = f'{spn_base}/{self.dcctx.get_domain_name()}'
+
+ # Add a disallowed SPN as admin.
+ msg = Message(Dn(self.ldb_admin, self.computerdn))
+ msg['servicePrincipalName'] = MessageElement(not_allowed_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ self.ldb_admin.modify(msg)
+
+ # Ensure we are able to delete a disallowed SPN.
+ msg = Message(Dn(self.ldb_user1, self.computerdn))
+ msg['servicePrincipalName'] = MessageElement(not_allowed_spn,
+ FLAG_MOD_DELETE,
+ 'servicePrincipalName')
+ try:
+ self.ldb_user1.modify(msg)
+ except LdbError:
+ self.fail(f'unable to delete disallowed SPN {not_allowed_spn}')
+
+
+# tests SEC_ADS_LIST vs. SEC_ADS_LIST_OBJECT
+@DynamicTestCase
+class AclVisibiltyTests(AclTests):
+
+ envs = {
+ "No": False,
+ "Do": True,
+ }
+ modes = {
+ "Allow": False,
+ "Deny": True,
+ }
+ perms = {
+ "nn": 0,
+ "Cn": security.SEC_ADS_LIST,
+ "nO": security.SEC_ADS_LIST_OBJECT,
+ "CO": security.SEC_ADS_LIST | security.SEC_ADS_LIST_OBJECT,
+ }
+
+ @classmethod
+ def setUpDynamicTestCases(cls):
+ for le in cls.envs.keys():
+ for lm in cls.modes.keys():
+ for l1 in cls.perms.keys():
+ for l2 in cls.perms.keys():
+ for l3 in cls.perms.keys():
+ tname = "%s_%s_%s_%s_%s" % (le, lm, l1, l2, l3)
+ ve = cls.envs[le]
+ vm = cls.modes[lm]
+ v1 = cls.perms[l1]
+ v2 = cls.perms[l2]
+ v3 = cls.perms[l3]
+ targs = (tname, ve, vm, v1, v2, v3)
+ cls.generate_dynamic_test("test_visibility",
+ tname, *targs)
+ return
+
+ def setUp(self):
+ super(AclVisibiltyTests, self).setUp()
+
+ strict_checking = samba.tests.env_get_var_value('STRICT_CHECKING', allow_missing=True)
+ if strict_checking is None:
+ strict_checking = '1'
+ self.strict_checking = bool(int(strict_checking))
+
+ # Get the old "dSHeuristics" if it was set
+ self.dsheuristics = self.ldb_admin.get_dsheuristics()
+ # Reset the "dSHeuristics" as they were before
+ self.addCleanup(self.ldb_admin.set_dsheuristics, self.dsheuristics)
+
+ # Domain Admins and SYSTEM get full access
+ self.sddl_dacl = "D:PAI(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)"
+ self.set_dacl_control = ["sd_flags:1:%d" % security.SECINFO_DACL]
+
+ self.level_idxs = [ 1, 2, 3, 4 ]
+ self.oul1 = "OU=acl_visibility_oul1"
+ self.oul1_dn_str = "%s,%s" % (self.oul1, self.base_dn)
+ self.oul2 = "OU=oul2,%s" % self.oul1
+ self.oul2_dn_str = "%s,%s" % (self.oul2, self.base_dn)
+ self.oul3 = "OU=oul3,%s" % self.oul2
+ self.oul3_dn_str = "%s,%s" % (self.oul3, self.base_dn)
+ self.user_name = "acl_visibility_user"
+ self.user_dn_str = "CN=%s,%s" % (self.user_name, self.oul3_dn_str)
+ delete_force(self.ldb_admin, self.user_dn_str)
+ delete_force(self.ldb_admin, self.oul3_dn_str)
+ delete_force(self.ldb_admin, self.oul2_dn_str)
+ delete_force(self.ldb_admin, self.oul1_dn_str)
+ self.ldb_admin.create_ou(self.oul1_dn_str)
+ self.sd_utils.modify_sd_on_dn(self.oul1_dn_str,
+ self.sddl_dacl,
+ controls=self.set_dacl_control)
+ self.ldb_admin.create_ou(self.oul2_dn_str)
+ self.sd_utils.modify_sd_on_dn(self.oul2_dn_str,
+ self.sddl_dacl,
+ controls=self.set_dacl_control)
+ self.ldb_admin.create_ou(self.oul3_dn_str)
+ self.sd_utils.modify_sd_on_dn(self.oul3_dn_str,
+ self.sddl_dacl,
+ controls=self.set_dacl_control)
+
+ self.ldb_admin.newuser(self.user_name, self.user_pass, userou=self.oul3)
+ self.user_sid = self.sd_utils.get_object_sid(self.user_dn_str)
+ self.ldb_user = self.get_ldb_connection(self.user_name, self.user_pass)
+
+ def tearDown(self):
+ super(AclVisibiltyTests, self).tearDown()
+ delete_force(self.ldb_admin, self.user_dn_str)
+ delete_force(self.ldb_admin, self.oul3_dn_str)
+ delete_force(self.ldb_admin, self.oul2_dn_str)
+ delete_force(self.ldb_admin, self.oul1_dn_str)
+
+ del self.ldb_user
+
+ def _test_visibility_with_args(self,
+ tname,
+ fDoListObject,
+ modeDeny,
+ l1_allow,
+ l2_allow,
+ l3_allow):
+ l1_deny = 0
+ l2_deny = 0
+ l3_deny = 0
+ if modeDeny:
+ l1_deny = ~l1_allow
+ l2_deny = ~l2_allow
+ l3_deny = ~l3_allow
+ print("Testing: fDoListObject=%s, modeDeny=%s, l1_allow=0x%02x, l2_allow=0x%02x, l3_allow=0x%02x)" % (
+ fDoListObject, modeDeny, l1_allow, l2_allow, l3_allow))
+ if fDoListObject:
+ self.ldb_admin.set_dsheuristics("001")
+ else:
+ self.ldb_admin.set_dsheuristics("000")
+
+ def _generate_dacl(allow, deny):
+ dacl = self.sddl_dacl
+ drights = ""
+ if deny & security.SEC_ADS_LIST:
+ drights += "LC"
+ if deny & security.SEC_ADS_LIST_OBJECT:
+ drights += "LO"
+ if len(drights) > 0:
+ dacl += "(D;;%s;;;%s)" % (drights, self.user_sid)
+ arights = ""
+ if allow & security.SEC_ADS_LIST:
+ arights += "LC"
+ if allow & security.SEC_ADS_LIST_OBJECT:
+ arights += "LO"
+ if len(arights) > 0:
+ dacl += "(A;;%s;;;%s)" % (arights, self.user_sid)
+ print("dacl: %s" % dacl)
+ return dacl
+
+ l1_dacl = _generate_dacl(l1_allow, l1_deny)
+ l2_dacl = _generate_dacl(l2_allow, l2_deny)
+ l3_dacl = _generate_dacl(l3_allow, l3_deny)
+ self.sd_utils.modify_sd_on_dn(self.oul1_dn_str,
+ l1_dacl,
+ controls=self.set_dacl_control)
+ self.sd_utils.modify_sd_on_dn(self.oul2_dn_str,
+ l2_dacl,
+ controls=self.set_dacl_control)
+ self.sd_utils.modify_sd_on_dn(self.oul3_dn_str,
+ l3_dacl,
+ controls=self.set_dacl_control)
+
+ def _generate_levels(_l1_allow,
+ _l1_deny,
+ _l2_allow,
+ _l2_deny,
+ _l3_allow,
+ _l3_deny):
+ _l0_allow = security.SEC_ADS_LIST | security.SEC_ADS_LIST_OBJECT | security.SEC_ADS_READ_PROP
+ _l0_deny = 0
+ _l4_allow = security.SEC_ADS_LIST | security.SEC_ADS_LIST_OBJECT | security.SEC_ADS_READ_PROP
+ _l4_deny = 0
+ _levels = [{
+ "dn": str(self.base_dn),
+ "allow": _l0_allow,
+ "deny": _l0_deny,
+ },{
+ "dn": str(self.oul1_dn_str),
+ "allow": _l1_allow,
+ "deny": _l1_deny,
+ },{
+ "dn": str(self.oul2_dn_str),
+ "allow": _l2_allow,
+ "deny": _l2_deny,
+ },{
+ "dn": str(self.oul3_dn_str),
+ "allow": _l3_allow,
+ "deny": _l3_deny,
+ },{
+ "dn": str(self.user_dn_str),
+ "allow": _l4_allow,
+ "deny": _l4_deny,
+ }]
+ return _levels
+
+ def _generate_admin_levels():
+ _l1_allow = security.SEC_ADS_LIST | security.SEC_ADS_READ_PROP
+ _l1_deny = 0
+ _l2_allow = security.SEC_ADS_LIST | security.SEC_ADS_READ_PROP
+ _l2_deny = 0
+ _l3_allow = security.SEC_ADS_LIST | security.SEC_ADS_READ_PROP
+ _l3_deny = 0
+ return _generate_levels(_l1_allow, _l1_deny,
+ _l2_allow, _l2_deny,
+ _l3_allow, _l3_deny)
+
+ def _generate_user_levels():
+ return _generate_levels(l1_allow, l1_deny,
+ l2_allow, l2_deny,
+ l3_allow, l3_deny)
+
+ admin_levels = _generate_admin_levels()
+ user_levels = _generate_user_levels()
+
+ def _msg_require_name(msg, idx, e):
+ self.assertIn("name", msg)
+ self.assertEqual(len(msg["name"]), 1)
+
+ def _msg_no_name(msg, idx, e):
+ self.assertNotIn("name", msg)
+
+ def _has_right(allow, deny, bit):
+ if allow & bit:
+ if not (deny & bit):
+ return True
+ return False
+
+ def _is_visible(p_allow, p_deny, o_allow, o_deny):
+ plc = _has_right(p_allow, p_deny, security.SEC_ADS_LIST)
+ if plc:
+ return True
+ if not fDoListObject:
+ return False
+ plo = _has_right(p_allow, p_deny, security.SEC_ADS_LIST_OBJECT)
+ if not plo:
+ return False
+ olo = _has_right(o_allow, o_deny, security.SEC_ADS_LIST_OBJECT)
+ if not olo:
+ return False
+ return True
+
+ def _generate_expected(scope, base_level, levels):
+ expected = {}
+
+ p = levels[base_level-1]
+ o = levels[base_level]
+ base_visible = _is_visible(p["allow"], p["deny"],
+ o["allow"], o["deny"])
+
+ if scope == SCOPE_BASE:
+ lmin = base_level
+ lmax = base_level
+ elif scope == SCOPE_ONELEVEL:
+ lmin = base_level+1
+ lmax = base_level+1
+ else:
+ lmin = base_level
+ lmax = len(levels)
+
+ next_idx = 0
+ for li in self.level_idxs:
+ if li < lmin:
+ continue
+ if li > lmax:
+ break
+ p = levels[li-1]
+ o = levels[li]
+ visible = _is_visible(p["allow"], p["deny"],
+ o["allow"], o["deny"])
+ if not visible:
+ continue
+ read = _has_right(o["allow"], o["deny"], security.SEC_ADS_READ_PROP)
+ if read:
+ check_msg_fn = _msg_require_name
+ else:
+ check_msg_fn = _msg_no_name
+ expected[o["dn"]] = {
+ "idx": next_idx,
+ "check_msg_fn": check_msg_fn,
+ }
+ next_idx += 1
+
+ if len(expected) == 0 and not base_visible:
+ # This means we're expecting NO_SUCH_OBJECT
+ return None
+ return expected
+
+ def _verify_result_array(results,
+ description,
+ expected):
+ print("%s Results: %d" % (description, len(results)))
+ for msg in results:
+ print("%s" % msg)
+ self.assertIsNotNone(expected)
+ print("%s Expected: %d" % (description, len(expected)))
+ for e in expected:
+ print("%s" % e)
+ self.assertEqual(len(results), len(expected))
+ idx = 0
+ found = {}
+ for msg in results:
+ dn_str = str(msg.dn)
+ self.assertIn(dn_str, expected)
+ self.assertNotIn(dn_str, found)
+ found[dn_str] = idx
+ e = expected[dn_str]
+ if self.strict_checking:
+ self.assertEqual(idx, int(e["idx"]))
+ if "check_msg_fn" in e:
+ check_msg_fn = e["check_msg_fn"]
+ check_msg_fn(msg, idx, e)
+ idx += 1
+
+ return
+
+ for li in self.level_idxs:
+ base_dn = admin_levels[li]["dn"]
+ for scope in [SCOPE_BASE, SCOPE_ONELEVEL, SCOPE_SUBTREE]:
+ print("\nTesting SCOPE[%d] %s" % (scope, base_dn))
+ admin_expected = _generate_expected(scope, li, admin_levels)
+ admin_res = self.ldb_admin.search(base_dn, scope=scope, attrs=["name"])
+ _verify_result_array(admin_res, "Admin", admin_expected)
+
+ user_expected = _generate_expected(scope, li, user_levels)
+ try:
+ user_res = self.ldb_user.search(base_dn, scope=scope, attrs=["name"])
+ except LdbError as e:
+ (num, _) = e.args
+ if user_expected is None:
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ print("User: NO_SUCH_OBJECT")
+ continue
+ self.fail(e)
+ _verify_result_array(user_res, "User", user_expected)
+
+# Important unit running information
+
+ldb = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/acl_modify.py b/source4/dsdb/tests/python/acl_modify.py
new file mode 100755
index 0000000..c85748a
--- /dev/null
+++ b/source4/dsdb/tests/python/acl_modify.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from ldb import ERR_INSUFFICIENT_ACCESS_RIGHTS
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from samba.dcerpc import security
+
+from samba.auth import system_session
+from samba import gensec, sd_utils
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+import samba.tests
+import samba.dsdb
+
+
+parser = optparse.OptionParser("acl.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]
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+
+
+class AclTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(AclTests, self).setUp()
+
+ strict_checking = samba.tests.env_get_var_value('STRICT_CHECKING', allow_missing=True)
+ if strict_checking is None:
+ strict_checking = '1'
+ self.strict_checking = bool(int(strict_checking))
+
+ self.ldb_admin = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.domain_sid = security.dom_sid(self.ldb_admin.get_domain_sid())
+ self.user_pass = "samba123@"
+ self.configuration_dn = self.ldb_admin.get_config_basedn().get_linearized()
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+ self.addCleanup(self.delete_admin_connection)
+ # used for anonymous login
+ self.creds_tmp = Credentials()
+ self.creds_tmp.set_username("")
+ self.creds_tmp.set_password("")
+ self.creds_tmp.set_domain(creds.get_domain())
+ self.creds_tmp.set_realm(creds.get_realm())
+ self.creds_tmp.set_workstation(creds.get_workstation())
+ print("baseDN: %s" % self.base_dn)
+
+ # set AttributeAuthorizationOnLDAPAdd and BlockOwnerImplicitRights
+ self.set_heuristic(samba.dsdb.DS_HR_ATTR_AUTHZ_ON_LDAP_ADD, b'11')
+
+ def set_heuristic(self, index, values):
+ self.assertGreater(index, 0)
+ self.assertLess(index, 30)
+ self.assertIsInstance(values, bytes)
+
+ # Get the old "dSHeuristics" if it was set
+ dsheuristics = self.ldb_admin.get_dsheuristics()
+ # Reset the "dSHeuristics" as they were before
+ self.addCleanup(self.ldb_admin.set_dsheuristics, dsheuristics)
+ # Set the "dSHeuristics" to activate the correct behaviour
+ default_heuristics = b"000000000100000000020000000003"
+ if dsheuristics is None:
+ dsheuristics = b""
+ dsheuristics += default_heuristics[len(dsheuristics):]
+ dsheuristics = (dsheuristics[:index - 1] +
+ values +
+ dsheuristics[index - 1 + len(values):])
+ self.ldb_admin.set_dsheuristics(dsheuristics)
+
+ def get_user_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ # Test if we have any additional groups for users than default ones
+ def assert_user_no_group_member(self, username):
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % self.get_user_dn(username))
+ try:
+ self.assertEqual(res[0]["memberOf"][0], "")
+ except KeyError:
+ pass
+ else:
+ self.fail()
+
+ def delete_admin_connection(self):
+ del self.sd_utils
+ del self.ldb_admin
+
+
+class AclModifyTests(AclTests):
+
+ def setup_computer_with_hostname(self, account_name):
+ ou_dn = f'OU={account_name},{self.base_dn}'
+ dn = f'CN={account_name},{ou_dn}'
+
+ user, password = "mouse", "mus musculus 123!"
+ self.addCleanup(self.ldb_admin.deleteuser, user)
+
+ self.ldb_admin.newuser(user, password)
+ self.ldb_user = self.get_ldb_connection(user, password)
+
+ self.addCleanup(self.ldb_admin.delete, ou_dn,
+ controls=["tree_delete:0"])
+ self.ldb_admin.create_ou(ou_dn)
+
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': account_name + '$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_admin, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+
+ self.ldb_admin.modify(m)
+ return host_name, dn
+
+ def test_modify_delete_dns_host_name_specified(self):
+ '''Test deleting dNSHostName'''
+ account_name = self.id().rsplit(".", 1)[1][:63]
+ host_name, dn = self.setup_computer_with_hostname(account_name)
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_DELETE,
+ 'dNSHostName')
+
+ self.assertRaisesLdbError(
+ ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "User able to delete dNSHostName (with specified name)",
+ self.ldb_user.modify, m)
+
+ def test_modify_delete_dns_host_name_unspecified(self):
+ '''Test deleting dNSHostName'''
+ account_name = self.id().rsplit(".", 1)[1][:63]
+ host_name, dn = self.setup_computer_with_hostname(account_name)
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement([],
+ FLAG_MOD_DELETE,
+ 'dNSHostName')
+
+ self.assertRaisesLdbError(
+ ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "User able to delete dNSHostName (without specified name)",
+ self.ldb_user.modify, m)
+
+ def test_modify_delete_dns_host_name_ldif_specified(self):
+ '''Test deleting dNSHostName'''
+ account_name = self.id().rsplit(".", 1)[1][:63]
+ host_name, dn = self.setup_computer_with_hostname(account_name)
+
+ ldif = f"""
+dn: {dn}
+changetype: modify
+delete: dNSHostName
+dNSHostName: {host_name}
+"""
+ self.assertRaisesLdbError(
+ ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "User able to delete dNSHostName (with specified name)",
+ self.ldb_user.modify_ldif, ldif)
+
+ def test_modify_delete_dns_host_name_ldif_unspecified(self):
+ '''Test deleting dNSHostName'''
+ account_name = self.id().rsplit(".", 1)[1][:63]
+ host_name, dn = self.setup_computer_with_hostname(account_name)
+
+ ldif = f"""
+dn: {dn}
+changetype: modify
+delete: dNSHostName
+"""
+ self.assertRaisesLdbError(
+ ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "User able to delete dNSHostName (without specific name)",
+ self.ldb_user.modify_ldif, ldif)
+
+
+ldb = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_medley_performance.py b/source4/dsdb/tests/python/ad_dc_medley_performance.py
new file mode 100644
index 0000000..010790d
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_medley_performance.py
@@ -0,0 +1,520 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import time
+import itertools
+
+from samba.netcmd.main import cmd_sambatool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+from ldb import Message, MessageElement, Dn, LdbError
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from ldb import SCOPE_BASE, SCOPE_SUBTREE, SCOPE_ONELEVEL
+from ldb import ERR_NO_SUCH_OBJECT
+
+parser = optparse.OptionParser("ad_dc_medley_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+sambaopts.add_option("-p", "--use-paged-search", action="store_true",
+ help="Use paged search module")
+
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+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)
+
+random.seed(1)
+
+
+class PerfTestException(Exception):
+ pass
+
+
+BATCH_SIZE = 2000
+LINK_BATCH_SIZE = 1000
+DELETE_BATCH_SIZE = 50
+N_GROUPS = 29
+
+
+class GlobalState(object):
+ next_user_id = 0
+ n_groups = 0
+ next_linked_user = 0
+ next_relinked_user = 0
+ next_linked_user_3 = 0
+ next_removed_link_0 = 0
+ test_number = 0
+ active_links = set()
+
+
+class UserTests(samba.tests.TestCase):
+
+ def add_if_possible(self, *args, **kwargs):
+ """In these tests sometimes things are left in the database
+ deliberately, so we don't worry if we fail to add them a second
+ time."""
+ try:
+ self.ldb.add(*args, **kwargs)
+ except LdbError:
+ pass
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.state = GlobalState # the class itself, not an instance
+ self.lp = lp
+
+ kwargs = {}
+ if opts.use_paged_search:
+ kwargs["options"] = ["modules:paged_searches"]
+
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp, **kwargs)
+ self.base_dn = self.ldb.domain_dn()
+ self.ou = "OU=pid%s,%s" % (os.getpid(), self.base_dn)
+ self.ou_users = "OU=users,%s" % self.ou
+ self.ou_groups = "OU=groups,%s" % self.ou
+ self.ou_computers = "OU=computers,%s" % self.ou
+
+ self.state.test_number += 1
+ random.seed(self.state.test_number)
+
+ def tearDown(self):
+ super(UserTests, self).tearDown()
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def test_00_01_do_nothing_relevant(self):
+ # takes around 1 second on i7-4770
+ j = 0
+ for i in range(30000000):
+ j += i
+
+ def test_00_02_do_nothing_sleepily(self):
+ time.sleep(1)
+
+ def test_00_03_add_ous_and_groups(self):
+ # initialise the database
+ for dn in (self.ou,
+ self.ou_users,
+ self.ou_groups,
+ self.ou_computers):
+ self.ldb.add({
+ "dn": dn,
+ "objectclass": "organizationalUnit"
+ })
+
+ for i in range(N_GROUPS):
+ self.ldb.add({
+ "dn": "cn=g%d,%s" % (i, self.ou_groups),
+ "objectclass": "group"
+ })
+
+ self.state.n_groups = N_GROUPS
+
+ def _add_users(self, start, end):
+ for i in range(start, end):
+ self.ldb.add({
+ "dn": "cn=u%d,%s" % (i, self.ou_users),
+ "objectclass": "user"
+ })
+
+ def _add_users_ldif(self, start, end):
+ lines = []
+ for i in range(start, end):
+ lines.append("dn: cn=u%d,%s" % (i, self.ou_users))
+ lines.append("objectclass: user")
+ lines.append("")
+ self.ldb.add_ldif('\n'.join(lines))
+
+ def _test_join(self):
+ tmpdir = tempfile.mkdtemp()
+ if '://' in host:
+ server = host.split('://', 1)[1]
+ else:
+ server = host
+ cmd = cmd_sambatool.subcommands['domain'].subcommands['join']
+ result = cmd._run("samba-tool domain join",
+ creds.get_realm(),
+ "dc", "-U%s%%%s" % (creds.get_username(),
+ creds.get_password()),
+ '--targetdir=%s' % tmpdir,
+ '--server=%s' % server)
+
+ shutil.rmtree(tmpdir)
+
+ def _test_unindexed_search(self):
+ expressions = [
+ ('(&(objectclass=user)(description='
+ 'Built-in account for adminstering the computer/domain))'),
+ '(description=Built-in account for adminstering the computer/domain)',
+ '(objectCategory=*)',
+ '(samaccountname=Administrator*)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(25):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_indexed_search(self):
+ expressions = ['(objectclass=group)',
+ '(samaccountname=Administrator)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(4000):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_base_search(self):
+ for dn in [self.base_dn, self.ou, self.ou_users,
+ self.ou_groups, self.ou_computers]:
+ for i in range(4000):
+ try:
+ self.ldb.search(dn,
+ scope=SCOPE_BASE,
+ attrs=['cn'])
+ except LdbError as e:
+ (num, msg) = e.args
+ if num != ERR_NO_SUCH_OBJECT:
+ raise
+
+ def _test_base_search_failing(self):
+ pattern = 'missing%d' + self.ou
+ for i in range(4000):
+ try:
+ self.ldb.search(pattern % i,
+ scope=SCOPE_BASE,
+ attrs=['cn'])
+ except LdbError as e:
+ (num, msg) = e
+ if num != ERR_NO_SUCH_OBJECT:
+ raise
+
+ def search_expression_list(self, expressions, rounds,
+ attrs=['cn'],
+ scope=SCOPE_SUBTREE):
+ for expression in expressions:
+ t = time.time()
+ for i in range(rounds):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_complex_search(self, n=100):
+ classes = ['samaccountname', 'objectCategory', 'dn', 'member']
+ values = ['*', '*t*', 'g*', 'user']
+ comparators = ['=', '<=', '>='] # '~=' causes error
+ maybe_not = ['!(', '']
+ joiners = ['&', '|']
+
+ # The number of permuations is 18432, which is not huge but
+ # would take hours to search. So we take a sample.
+ all_permutations = list(itertools.product(joiners,
+ classes, classes,
+ values, values,
+ comparators, comparators,
+ maybe_not, maybe_not))
+
+ expressions = []
+
+ for (j, c1, c2, v1, v2,
+ o1, o2, n1, n2) in random.sample(all_permutations, n):
+ expression = ''.join(['(', j,
+ '(', n1, c1, o1, v1,
+ '))' if n1 else ')',
+ '(', n2, c2, o2, v2,
+ '))' if n2 else ')',
+ ')'])
+ expressions.append(expression)
+
+ self.search_expression_list(expressions, 1)
+
+ def _test_member_search(self, rounds=10):
+ expressions = []
+ for d in range(20):
+ expressions.append('(member=cn=u%d,%s)' % (d + 500, self.ou_users))
+ expressions.append('(member=u%d*)' % (d + 700,))
+
+ self.search_expression_list(expressions, rounds)
+
+ def _test_memberof_search(self, rounds=200):
+ expressions = []
+ for i in range(min(self.state.n_groups, rounds)):
+ expressions.append('(memberOf=cn=g%d,%s)' % (i, self.ou_groups))
+ expressions.append('(memberOf=cn=g%d*)' % (i,))
+ expressions.append('(memberOf=cn=*%s*)' % self.ou_groups)
+
+ self.search_expression_list(expressions, 2)
+
+ def _test_add_many_users(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users(s, e)
+ self.state.next_user_id = e
+
+ def _test_add_many_users_ldif(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users_ldif(s, e)
+ self.state.next_user_id = e
+
+ def _link_user_and_group(self, u, g):
+ link = (u, g)
+ if link in self.state.active_links:
+ return False
+
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
+ m["member"] = MessageElement("cn=u%d,%s" % (u, self.ou_users),
+ FLAG_MOD_ADD, "member")
+ self.ldb.modify(m)
+ self.state.active_links.add(link)
+ return True
+
+ def _unlink_user_and_group(self, u, g):
+ link = (u, g)
+ if link not in self.state.active_links:
+ return False
+
+ user = "cn=u%d,%s" % (u, self.ou_users)
+ group = "CN=g%d,%s" % (g, self.ou_groups)
+ m = Message()
+ m.dn = Dn(self.ldb, group)
+ m["member"] = MessageElement(user, FLAG_MOD_DELETE, "member")
+ self.ldb.modify(m)
+ self.state.active_links.remove(link)
+ return True
+
+ def _test_link_many_users(self, n=LINK_BATCH_SIZE):
+ # this links unevenly, putting more users in the first group
+ # and fewer in the last.
+ ng = self.state.n_groups
+ nu = self.state.next_user_id
+ while n:
+ u = random.randrange(nu)
+ g = random.randrange(random.randrange(ng) + 1)
+ if self._link_user_and_group(u, g):
+ n -= 1
+
+ def _test_link_many_users_batch(self, n=(LINK_BATCH_SIZE * 10)):
+ # this links unevenly, putting more users in the first group
+ # and fewer in the last.
+ ng = self.state.n_groups
+ nu = self.state.next_user_id
+ messages = []
+ for g in range(ng):
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
+ messages.append(m)
+
+ while n:
+ u = random.randrange(nu)
+ g = random.randrange(random.randrange(ng) + 1)
+ link = (u, g)
+ if link in self.state.active_links:
+ continue
+ m = messages[g]
+ m["member%s" % u] = MessageElement("cn=u%d,%s" %
+ (u, self.ou_users),
+ FLAG_MOD_ADD, "member")
+ self.state.active_links.add(link)
+ n -= 1
+
+ for m in messages:
+ try:
+ self.ldb.modify(m)
+ except LdbError as e:
+ print(e)
+ print(m)
+
+ def _test_remove_some_links(self, n=(LINK_BATCH_SIZE // 2)):
+ victims = random.sample(list(self.state.active_links), n)
+ for x in victims:
+ self._unlink_user_and_group(*x)
+
+ test_00_11_join_empty_dc = _test_join
+
+ test_00_12_adding_users_2000 = _test_add_many_users
+
+ test_00_20_join_unlinked_2k_users = _test_join
+ test_00_21_unindexed_search_2k_users = _test_unindexed_search
+ test_00_22_indexed_search_2k_users = _test_indexed_search
+
+ test_00_23_complex_search_2k_users = _test_complex_search
+ test_00_24_member_search_2k_users = _test_member_search
+ test_00_25_memberof_search_2k_users = _test_memberof_search
+
+ test_00_27_base_search_2k_users = _test_base_search
+ test_00_28_base_search_failing_2k_users = _test_base_search_failing
+
+ test_01_01_link_2k_users = _test_link_many_users
+ test_01_02_link_2k_users_batch = _test_link_many_users_batch
+
+ test_02_10_join_2k_linked_dc = _test_join
+ test_02_11_unindexed_search_2k_linked_dc = _test_unindexed_search
+ test_02_12_indexed_search_2k_linked_dc = _test_indexed_search
+
+ test_04_01_remove_some_links_2k = _test_remove_some_links
+
+ test_05_01_adding_users_after_links_4k_ldif = _test_add_many_users_ldif
+
+ test_06_04_link_users_4k = _test_link_many_users
+ test_06_05_link_users_4k_batch = _test_link_many_users_batch
+
+ test_07_01_adding_users_after_links_6k = _test_add_many_users
+
+ def _test_ldif_well_linked_group(self, link_chance=1.0):
+ g = self.state.n_groups
+ self.state.n_groups += 1
+ lines = ["dn: CN=g%d,%s" % (g, self.ou_groups),
+ "objectclass: group"]
+
+ for i in range(self.state.next_user_id):
+ if random.random() <= link_chance:
+ lines.append("member: cn=u%d,%s" % (i, self.ou_users))
+ self.state.active_links.add((i, g))
+
+ lines.append("")
+ self.ldb.add_ldif('\n'.join(lines))
+
+ test_09_01_add_fully_linked_group = _test_ldif_well_linked_group
+
+ def test_09_02_add_exponentially_diminishing_linked_groups(self):
+ linkage = 0.8
+ while linkage > 0.01:
+ self._test_ldif_well_linked_group(linkage)
+ linkage *= 0.75
+
+ test_09_04_link_users_6k = _test_link_many_users
+
+ test_10_01_unindexed_search_6k_users = _test_unindexed_search
+ test_10_02_indexed_search_6k_users = _test_indexed_search
+
+ test_10_27_base_search_6k_users = _test_base_search
+ test_10_28_base_search_failing_6k_users = _test_base_search_failing
+
+ def test_10_03_complex_search_6k_users(self):
+ self._test_complex_search(n=50)
+
+ def test_10_04_member_search_6k_users(self):
+ self._test_member_search(rounds=1)
+
+ def test_10_05_memberof_search_6k_users(self):
+ self._test_memberof_search(rounds=5)
+
+ test_11_02_join_full_dc = _test_join
+
+ test_12_01_remove_some_links_6k = _test_remove_some_links
+
+ def _test_delete_many_users(self, n=DELETE_BATCH_SIZE):
+ e = self.state.next_user_id
+ s = max(0, e - n)
+ self.state.next_user_id = s
+ for i in range(s, e):
+ self.ldb.delete("cn=u%d,%s" % (i, self.ou_users))
+
+ for x in tuple(self.state.active_links):
+ if s >= x[0] > e:
+ self.state.active_links.remove(x)
+
+ test_20_01_delete_users_6k = _test_delete_many_users
+
+ def test_21_01_delete_10_groups(self):
+ for i in range(self.state.n_groups - 10, self.state.n_groups):
+ self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
+ self.state.n_groups -= 10
+ for x in tuple(self.state.active_links):
+ if x[1] >= self.state.n_groups:
+ self.state.active_links.remove(x)
+
+ test_21_02_delete_users_5950 = _test_delete_many_users
+
+ def test_22_01_delete_all_groups(self):
+ for i in range(self.state.n_groups):
+ self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
+ self.state.n_groups = 0
+ self.state.active_links = set()
+
+ # XXX assert the state is as we think, using searches
+
+ def test_23_01_delete_users_5900_after_groups(self):
+ # we do not delete everything because it takes too long
+ n = 4 * DELETE_BATCH_SIZE
+ self._test_delete_many_users(n=n)
+
+ test_24_02_join_after_partial_cleanup = _test_join
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.makeSuite(UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_multi_bind.py b/source4/dsdb/tests/python/ad_dc_multi_bind.py
new file mode 100644
index 0000000..564fb9e
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_multi_bind.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import time
+
+from samba.netcmd.main import cmd_sambatool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+from ldb import Message, MessageElement, Dn, LdbError
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from ldb import SCOPE_BASE, SCOPE_SUBTREE, SCOPE_ONELEVEL
+
+parser = optparse.OptionParser("ad_dc_multi_bind.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+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)
+
+
+class UserTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.lp = lp
+
+ def tearDown(self):
+ super(UserTests, self).tearDown()
+
+ def test_1000_binds(self):
+
+ for x in range(1, 1000):
+ samdb = SamDB(host, credentials=creds,
+ session_info=system_session(self.lp), lp=self.lp)
+ samdb.search(base=samdb.domain_dn(),
+ scope=SCOPE_BASE, attrs=["*"])
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.makeSuite(UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_performance.py b/source4/dsdb/tests/python/ad_dc_performance.py
new file mode 100644
index 0000000..6ff84e4
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_performance.py
@@ -0,0 +1,339 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import time
+
+from samba.netcmd.main import cmd_sambatool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+from ldb import Message, MessageElement, Dn, LdbError
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from ldb import SCOPE_BASE, SCOPE_SUBTREE, SCOPE_ONELEVEL
+
+parser = optparse.OptionParser("ad_dc_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+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)
+
+random.seed(1)
+
+
+class PerfTestException(Exception):
+ pass
+
+
+BATCH_SIZE = 1000
+N_GROUPS = 5
+
+
+class GlobalState(object):
+ next_user_id = 0
+ n_groups = 0
+ next_linked_user = 0
+ next_relinked_user = 0
+ next_linked_user_3 = 0
+ next_removed_link_0 = 0
+
+
+class UserTests(samba.tests.TestCase):
+
+ def add_if_possible(self, *args, **kwargs):
+ """In these tests sometimes things are left in the database
+ deliberately, so we don't worry if we fail to add them a second
+ time."""
+ try:
+ self.ldb.add(*args, **kwargs)
+ except LdbError:
+ pass
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.state = GlobalState # the class itself, not an instance
+ self.lp = lp
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.ou = "OU=pid%s,%s" % (os.getpid(), self.base_dn)
+ self.ou_users = "OU=users,%s" % self.ou
+ self.ou_groups = "OU=groups,%s" % self.ou
+ self.ou_computers = "OU=computers,%s" % self.ou
+
+ for dn in (self.ou, self.ou_users, self.ou_groups,
+ self.ou_computers):
+ self.add_if_possible({
+ "dn": dn,
+ "objectclass": "organizationalUnit"})
+
+ def tearDown(self):
+ super(UserTests, self).tearDown()
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def _prepare_n_groups(self, n):
+ self.state.n_groups = n
+ for i in range(n):
+ self.add_if_possible({
+ "dn": "cn=g%d,%s" % (i, self.ou_groups),
+ "objectclass": "group"})
+
+ def _add_users(self, start, end):
+ for i in range(start, end):
+ self.ldb.add({
+ "dn": "cn=u%d,%s" % (i, self.ou_users),
+ "objectclass": "user"})
+
+ def _test_join(self):
+ tmpdir = tempfile.mkdtemp()
+ if '://' in host:
+ server = host.split('://', 1)[1]
+ else:
+ server = host
+ cmd = cmd_sambatool.subcommands['domain'].subcommands['join']
+ result = cmd._run("samba-tool domain join",
+ creds.get_realm(),
+ "dc", "-U%s%%%s" % (creds.get_username(),
+ creds.get_password()),
+ '--targetdir=%s' % tmpdir,
+ '--server=%s' % server)
+
+ shutil.rmtree(tmpdir)
+
+ def _test_unindexed_search(self):
+ expressions = [
+ ('(&(objectclass=user)(description='
+ 'Built-in account for adminstering the computer/domain))'),
+ '(description=Built-in account for adminstering the computer/domain)',
+ '(objectCategory=*)',
+ '(samaccountname=Administrator*)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(10):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d %s took %s' % (i, expression,
+ time.time() - t), file=sys.stderr)
+
+ def _test_indexed_search(self):
+ expressions = ['(objectclass=group)',
+ '(samaccountname=Administrator)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(100):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t), file=sys.stderr)
+
+ def _test_add_many_users(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users(s, e)
+ self.state.next_user_id = e
+
+ test_00_00_join_empty_dc = _test_join
+
+ test_00_01_adding_users_1000 = _test_add_many_users
+ test_00_02_adding_users_2000 = _test_add_many_users
+ test_00_03_adding_users_3000 = _test_add_many_users
+
+ test_00_10_join_unlinked_dc = _test_join
+ test_00_11_unindexed_search_3k_users = _test_unindexed_search
+ test_00_12_indexed_search_3k_users = _test_indexed_search
+
+ def _link_user_and_group(self, u, g):
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
+ m["member"] = MessageElement("cn=u%d,%s" % (u, self.ou_users),
+ FLAG_MOD_ADD, "member")
+ self.ldb.modify(m)
+
+ def _unlink_user_and_group(self, u, g):
+ user = "cn=u%d,%s" % (u, self.ou_users)
+ group = "CN=g%d,%s" % (g, self.ou_groups)
+ m = Message()
+ m.dn = Dn(self.ldb, group)
+ m["member"] = MessageElement(user, FLAG_MOD_DELETE, "member")
+ self.ldb.modify(m)
+
+ def _test_link_many_users(self, n=BATCH_SIZE):
+ self._prepare_n_groups(N_GROUPS)
+ s = self.state.next_linked_user
+ e = s + n
+ for i in range(s, e):
+ g = i % N_GROUPS
+ self._link_user_and_group(i, g)
+ self.state.next_linked_user = e
+
+ test_01_01_link_users_1000 = _test_link_many_users
+ test_01_02_link_users_2000 = _test_link_many_users
+ test_01_03_link_users_3000 = _test_link_many_users
+
+ def _test_link_many_users_offset_1(self, n=BATCH_SIZE):
+ s = self.state.next_relinked_user
+ e = s + n
+ for i in range(s, e):
+ g = (i + 1) % N_GROUPS
+ self._link_user_and_group(i, g)
+ self.state.next_relinked_user = e
+
+ test_02_01_link_users_again_1000 = _test_link_many_users_offset_1
+ test_02_02_link_users_again_2000 = _test_link_many_users_offset_1
+ test_02_03_link_users_again_3000 = _test_link_many_users_offset_1
+
+ test_02_10_join_partially_linked_dc = _test_join
+ test_02_11_unindexed_search_partially_linked_dc = _test_unindexed_search
+ test_02_12_indexed_search_partially_linked_dc = _test_indexed_search
+
+ def _test_link_many_users_3_groups(self, n=BATCH_SIZE, groups=3):
+ s = self.state.next_linked_user_3
+ e = s + n
+ self.state.next_linked_user_3 = e
+ for i in range(s, e):
+ g = (i + 2) % groups
+ if g not in (i % N_GROUPS, (i + 1) % N_GROUPS):
+ self._link_user_and_group(i, g)
+
+ test_03_01_link_users_again_1000_few_groups = _test_link_many_users_3_groups
+ test_03_02_link_users_again_2000_few_groups = _test_link_many_users_3_groups
+ test_03_03_link_users_again_3000_few_groups = _test_link_many_users_3_groups
+
+ def _test_remove_links_0(self, n=BATCH_SIZE):
+ s = self.state.next_removed_link_0
+ e = s + n
+ self.state.next_removed_link_0 = e
+ for i in range(s, e):
+ g = i % N_GROUPS
+ self._unlink_user_and_group(i, g)
+
+ test_04_01_remove_some_links_1000 = _test_remove_links_0
+ test_04_02_remove_some_links_2000 = _test_remove_links_0
+ test_04_03_remove_some_links_3000 = _test_remove_links_0
+
+ # back to using _test_add_many_users
+ test_05_01_adding_users_after_links_4000 = _test_add_many_users
+
+ # reset the link count, to replace the original links
+ def test_06_01_relink_users_1000(self):
+ self.state.next_linked_user = 0
+ self._test_link_many_users()
+
+ test_06_02_link_users_2000 = _test_link_many_users
+ test_06_03_link_users_3000 = _test_link_many_users
+ test_06_04_link_users_4000 = _test_link_many_users
+ test_06_05_link_users_again_4000 = _test_link_many_users_offset_1
+ test_06_06_link_users_again_4000_few_groups = _test_link_many_users_3_groups
+
+ test_07_01_adding_users_after_links_5000 = _test_add_many_users
+
+ def _test_link_random_users_and_groups(self, n=BATCH_SIZE, groups=100):
+ self._prepare_n_groups(groups)
+ for i in range(n):
+ u = random.randrange(self.state.next_user_id)
+ g = random.randrange(groups)
+ try:
+ self._link_user_and_group(u, g)
+ except LdbError:
+ pass
+
+ test_08_01_link_random_users_100_groups = _test_link_random_users_and_groups
+ test_08_02_link_random_users_100_groups = _test_link_random_users_and_groups
+
+ test_10_01_unindexed_search_full_dc = _test_unindexed_search
+ test_10_02_indexed_search_full_dc = _test_indexed_search
+ test_11_02_join_full_dc = _test_join
+
+ def test_20_01_delete_50_groups(self):
+ for i in range(self.state.n_groups - 50, self.state.n_groups):
+ self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
+ self.state.n_groups -= 50
+
+ def _test_delete_many_users(self, n=BATCH_SIZE):
+ e = self.state.next_user_id
+ s = max(0, e - n)
+ self.state.next_user_id = s
+ for i in range(s, e):
+ self.ldb.delete("cn=u%d,%s" % (i, self.ou_users))
+
+ test_21_01_delete_users_5000_lightly_linked = _test_delete_many_users
+ test_21_02_delete_users_4000_lightly_linked = _test_delete_many_users
+ test_21_03_delete_users_3000 = _test_delete_many_users
+
+ def test_22_01_delete_all_groups(self):
+ for i in range(self.state.n_groups):
+ self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
+ self.state.n_groups = 0
+
+ test_23_01_delete_users_after_groups_2000 = _test_delete_many_users
+ test_23_00_delete_users_after_groups_1000 = _test_delete_many_users
+
+ test_24_02_join_after_cleanup = _test_join
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.makeSuite(UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_provision_performance.py b/source4/dsdb/tests/python/ad_dc_provision_performance.py
new file mode 100644
index 0000000..3ce0eb7
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_provision_performance.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import subprocess
+
+from samba.netcmd.main import cmd_sambatool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+parser = optparse.OptionParser("ad_dc_provision_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+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)
+
+random.seed(1)
+
+
+class PerfTestException(Exception):
+ pass
+
+
+class UserTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.tmpdir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self.tmpdir)
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def _test_provision_subprocess(self, options=None, subdir=None):
+ if subdir is None:
+ d = self.tmpdir
+ else:
+ d = os.path.join(self.tmpdir, str(subdir))
+ os.mkdir(d)
+
+ cmd = ['bin/samba-tool', 'domain', 'provision', '--targetdir',
+ d, '--realm=realm.com', '--use-ntvfs', '--domain=dom']
+
+ if options:
+ options.extend(options)
+ subprocess.check_call(cmd)
+
+ test_01_00_provision_subprocess = _test_provision_subprocess
+
+ def test_01_00_provision_subprocess_overwrite(self):
+ for i in range(2):
+ self._test_provision_subprocess()
+
+ def test_02_00_provision_cmd_sambatool(self):
+ cmd = cmd_sambatool.subcommands['domain'].subcommands['provision']
+ result = cmd._run("samba-tool domain provision",
+ '--targetdir=%s' % self.tmpdir,
+ '--use-ntvfs')
+
+ def test_03_00_provision_server_role(self):
+ for role in ('member', 'server', 'member', 'standalone'):
+ self._test_provision_subprocess(options=['--server-role', role],
+ subdir=role)
+
+ def test_04_00_provision_blank(self):
+ for i in range(2):
+ self._test_provision_subprocess(options=['--blank'],
+ subdir=i)
+
+ def test_05_00_provision_partitions_only(self):
+ self._test_provision_subprocess(options=['--partitions-only'])
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.makeSuite(UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_search_performance.py b/source4/dsdb/tests/python/ad_dc_search_performance.py
new file mode 100644
index 0000000..44e4680
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_search_performance.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import time
+import itertools
+
+from samba.netcmd.main import cmd_sambatool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+from ldb import Message, MessageElement, Dn, LdbError
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from ldb import SCOPE_BASE, SCOPE_SUBTREE, SCOPE_ONELEVEL
+
+parser = optparse.OptionParser("ad_dc_search_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+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)
+
+random.seed(1)
+
+
+class PerfTestException(Exception):
+ pass
+
+
+BATCH_SIZE = 1000
+N_GROUPS = 5
+
+
+class GlobalState(object):
+ next_user_id = 0
+ n_groups = 0
+ next_linked_user = 0
+ next_relinked_user = 0
+ next_linked_user_3 = 0
+ next_removed_link_0 = 0
+
+
+class UserTests(samba.tests.TestCase):
+
+ def add_if_possible(self, *args, **kwargs):
+ """In these tests sometimes things are left in the database
+ deliberately, so we don't worry if we fail to add them a second
+ time."""
+ try:
+ self.ldb.add(*args, **kwargs)
+ except LdbError:
+ pass
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.state = GlobalState # the class itself, not an instance
+ self.lp = lp
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.ou = "OU=pid%s,%s" % (os.getpid(), self.base_dn)
+ self.ou_users = "OU=users,%s" % self.ou
+ self.ou_groups = "OU=groups,%s" % self.ou
+ self.ou_computers = "OU=computers,%s" % self.ou
+
+ for dn in (self.ou, self.ou_users, self.ou_groups,
+ self.ou_computers):
+ self.add_if_possible({
+ "dn": dn,
+ "objectclass": "organizationalUnit"})
+
+ def tearDown(self):
+ super(UserTests, self).tearDown()
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def _prepare_n_groups(self, n):
+ self.state.n_groups = n
+ for i in range(n):
+ self.add_if_possible({
+ "dn": "cn=g%d,%s" % (i, self.ou_groups),
+ "objectclass": "group"})
+
+ def _add_users(self, start, end):
+ for i in range(start, end):
+ self.ldb.add({
+ "dn": "cn=u%d,%s" % (i, self.ou_users),
+ "objectclass": "user"})
+
+ def _add_users_ldif(self, start, end):
+ lines = []
+ for i in range(start, end):
+ lines.append("dn: cn=u%d,%s" % (i, self.ou_users))
+ lines.append("objectclass: user")
+ lines.append("")
+ self.ldb.add_ldif('\n'.join(lines))
+
+ def _test_unindexed_search(self):
+ expressions = [
+ ('(&(objectclass=user)(description='
+ 'Built-in account for adminstering the computer/domain))'),
+ '(description=Built-in account for adminstering the computer/domain)',
+ '(objectCategory=*)',
+ '(samaccountname=Administrator*)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(50):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_indexed_search(self):
+ expressions = ['(objectclass=group)',
+ '(samaccountname=Administrator)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(10000):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_complex_search(self):
+ classes = ['samaccountname', 'objectCategory', 'dn', 'member']
+ values = ['*', '*t*', 'g*', 'user']
+ comparators = ['=', '<=', '>='] # '~=' causes error
+ maybe_not = ['!(', '']
+ joiners = ['&', '|']
+
+ # The number of permutations is 18432, which is not huge but
+ # would take hours to search. So we take a sample.
+ all_permutations = list(itertools.product(joiners,
+ classes, classes,
+ values, values,
+ comparators, comparators,
+ maybe_not, maybe_not))
+ random.seed(1)
+
+ for (j, c1, c2, v1, v2,
+ o1, o2, n1, n2) in random.sample(all_permutations, 100):
+ expression = ''.join(['(', j,
+ '(', n1, c1, o1, v1,
+ '))' if n1 else ')',
+ '(', n2, c2, o2, v2,
+ '))' if n2 else ')',
+ ')'])
+ print(expression)
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+
+ def _test_member_search(self, rounds=10):
+ expressions = []
+ for d in range(50):
+ expressions.append('(member=cn=u%d,%s)' % (d + 500, self.ou_users))
+ expressions.append('(member=u%d*)' % (d + 700,))
+ for i in range(N_GROUPS):
+ expressions.append('(memberOf=cn=g%d,%s)' % (i, self.ou_groups))
+ expressions.append('(memberOf=cn=g%d*)' % (i,))
+ expressions.append('(memberOf=cn=*%s*)' % self.ou_groups)
+
+ for expression in expressions:
+ t = time.time()
+ for i in range(rounds):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_add_many_users(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users(s, e)
+ self.state.next_user_id = e
+
+ def _test_add_many_users_ldif(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users_ldif(s, e)
+ self.state.next_user_id = e
+
+ def _link_user_and_group(self, u, g):
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
+ m["member"] = MessageElement("cn=u%d,%s" % (u, self.ou_users),
+ FLAG_MOD_ADD, "member")
+ self.ldb.modify(m)
+
+ def _test_link_many_users(self, n=BATCH_SIZE):
+ self._prepare_n_groups(N_GROUPS)
+ s = self.state.next_linked_user
+ e = s + n
+ for i in range(s, e):
+ # put everyone in group 0, and one other group
+ g = i % (N_GROUPS - 1) + 1
+ self._link_user_and_group(i, g)
+ self._link_user_and_group(i, 0)
+ self.state.next_linked_user = e
+
+ test_00_01_adding_users_1000 = _test_add_many_users
+
+ test_00_10_complex_search_1k_users = _test_complex_search
+ test_00_11_unindexed_search_1k_users = _test_unindexed_search
+ test_00_12_indexed_search_1k_users = _test_indexed_search
+ test_00_13_member_search_1k_users = _test_member_search
+
+ test_01_02_adding_users_2000_ldif = _test_add_many_users_ldif
+ test_01_03_adding_users_3000 = _test_add_many_users
+
+ test_01_10_complex_search_3k_users = _test_complex_search
+ test_01_11_unindexed_search_3k_users = _test_unindexed_search
+ test_01_12_indexed_search_3k_users = _test_indexed_search
+
+ def test_01_13_member_search_3k_users(self):
+ self._test_member_search(rounds=5)
+
+ test_02_01_link_users_1000 = _test_link_many_users
+ test_02_02_link_users_2000 = _test_link_many_users
+ test_02_03_link_users_3000 = _test_link_many_users
+
+ test_03_10_complex_search_linked_users = _test_complex_search
+ test_03_11_unindexed_search_linked_users = _test_unindexed_search
+ test_03_12_indexed_search_linked_users = _test_indexed_search
+
+ def test_03_13_member_search_linked_users(self):
+ self._test_member_search(rounds=2)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.makeSuite(UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/asq.py b/source4/dsdb/tests/python/asq.py
new file mode 100644
index 0000000..72fa8bb
--- /dev/null
+++ b/source4/dsdb/tests/python/asq.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+#
+# Test ASQ LDAP control behaviour in Samba
+# Copyright (C) Andrew Bartlett 2019-2020
+#
+# Based on Unit tests for the notification control
+# Copyright (C) Stefan Metzmacher 2016
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import optparse
+import sys
+import os
+import random
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba import ldb
+from samba.samdb import SamDB
+from samba.ndr import ndr_unpack
+from samba import gensec
+from samba.credentials import Credentials
+import samba.tests
+
+from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, LdbError
+from ldb import ERR_TIME_LIMIT_EXCEEDED, ERR_ADMIN_LIMIT_EXCEEDED, ERR_UNWILLING_TO_PERFORM
+from ldb import Message
+
+parser = optparse.OptionParser("asq.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)
+
+url = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class ASQLDAPTest(samba.tests.TestCase):
+
+ def setUp(self):
+ super(ASQLDAPTest, self).setUp()
+ self.ldb = samba.Ldb(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.get_default_basedn()
+ self.NAME_ASQ="asq_" + format(random.randint(0, 99999), "05")
+ self.OU_NAME_ASQ= self.NAME_ASQ + "_ou"
+ self.ou_dn = ldb.Dn(self.ldb, "ou=" + self.OU_NAME_ASQ + "," + str(self.base_dn))
+
+ samba.tests.delete_force(self.ldb, self.ou_dn,
+ controls=['tree_delete:1'])
+
+ self.ldb.add({
+ "dn": self.ou_dn,
+ "objectclass": "organizationalUnit",
+ "ou": self.OU_NAME_ASQ})
+
+ self.members = []
+ self.members2 = []
+
+ for x in range(20):
+ name = self.NAME_ASQ + "_" + str(x)
+ dn = ldb.Dn(self.ldb,
+ "cn=" + name + "," + str(self.ou_dn))
+ self.members.append(dn)
+ self.ldb.add({
+ "dn": dn,
+ "objectclass": "group"})
+
+ for x in range(20):
+ name = self.NAME_ASQ + "_" + str(x + 20)
+ dn = ldb.Dn(self.ldb,
+ "cn=" + name + "," + str(self.ou_dn))
+ self.members2.append(dn)
+ self.ldb.add({
+ "dn": dn,
+ "objectclass": "group",
+ "member": [str(x) for x in self.members]})
+
+ name = self.NAME_ASQ + "_" + str(x + 40)
+ self.top_dn = ldb.Dn(self.ldb,
+ "cn=" + name + "," + str(self.ou_dn))
+ self.ldb.add({
+ "dn": self.top_dn,
+ "objectclass": "group",
+ "member": [str(x) for x in self.members2]})
+
+ def tearDown(self):
+ samba.tests.delete_force(self.ldb, self.ou_dn,
+ controls=['tree_delete:1'])
+
+ def test_asq(self):
+ """Testing ASQ behaviour.
+
+ ASQ is very strange, it turns a BASE search into a search for
+ all the objects pointed to by the specified attribute,
+ returning multiple entries!
+
+ """
+
+ msgs = self.ldb.search(base=self.top_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["asq:1:member"])
+
+ self.assertEqual(len(msgs), 20)
+
+ for msg in msgs:
+ self.assertNotEqual(msg.dn, self.top_dn)
+ self.assertIn(msg.dn, self.members2)
+ for group in msg["member"]:
+ self.assertIn(ldb.Dn(self.ldb, str(group)),
+ self.members)
+
+ def test_asq_paged(self):
+ """Testing ASQ behaviour with paged_results set.
+
+ ASQ is very strange, it turns a BASE search into a search for
+ all the objects pointed to by the specified attribute,
+ returning multiple entries!
+
+ """
+
+ msgs = self.ldb.search(base=self.top_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["asq:1:member",
+ "paged_results:1:1024"])
+
+ self.assertEqual(len(msgs), 20)
+
+ for msg in msgs:
+ self.assertNotEqual(msg.dn, self.top_dn)
+ self.assertIn(msg.dn, self.members2)
+ for group in msg["member"]:
+ self.assertIn(ldb.Dn(self.ldb, str(group)),
+ self.members)
+
+ def test_asq_vlv(self):
+ """Testing ASQ behaviour with VLV set.
+
+ ASQ is very strange, it turns a BASE search into a search for
+ all the objects pointed to by the specified attribute,
+ returning multiple entries!
+
+ """
+
+ sort_control = "server_sort:1:0:cn"
+
+ msgs = self.ldb.search(base=self.top_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["asq:1:member",
+ sort_control,
+ "vlv:1:20:20:11:0"])
+
+ self.assertEqual(len(msgs), 20)
+
+ for msg in msgs:
+ self.assertNotEqual(msg.dn, self.top_dn)
+ self.assertIn(msg.dn, self.members2)
+ for group in msg["member"]:
+ self.assertIn(ldb.Dn(self.ldb, str(group)),
+ self.members)
+
+ def test_asq_vlv_paged(self):
+ """Testing ASQ behaviour with VLV and paged_results set.
+
+ ASQ is very strange, it turns a BASE search into a search for
+ all the objects pointed to by the specified attribute,
+ returning multiple entries!
+
+ Thankfully combining both of these gives
+ unavailable-critical-extension against Windows 1709
+
+ """
+
+ sort_control = "server_sort:1:0:cn"
+
+ try:
+ msgs = self.ldb.search(base=self.top_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["asq:1:member",
+ sort_control,
+ "vlv:1:20:20:11:0",
+ "paged_results:1:1024"])
+ self.fail("should have failed with LDAP_UNAVAILABLE_CRITICAL_EXTENSION")
+ except ldb.LdbError as e:
+ (enum, estr) = e.args
+ self.assertEqual(enum, ldb.ERR_UNSUPPORTED_CRITICAL_EXTENSION)
+
+if "://" not in url:
+ if os.path.isfile(url):
+ url = "tdb://%s" % url
+ else:
+ url = "ldap://%s" % url
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/attr_from_server.py b/source4/dsdb/tests/python/attr_from_server.py
new file mode 100644
index 0000000..aca356b
--- /dev/null
+++ b/source4/dsdb/tests/python/attr_from_server.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+#
+# Tests a corner-case involving the fromServer attribute, which is slightly
+# unique: it's an Object(DS-DN) (like a one-way link), but it is also a
+# mandatory attribute (for nTDSConnection). The corner-case is that the
+# fromServer can potentially end up pointing to a non-existent object.
+# This can happen with other one-way links, but these other one-way links
+# are not mandatory attributes.
+#
+# Copyright (C) Andrew Bartlett 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+import os
+import time
+import ldb
+import samba.tests
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+from samba.dcerpc import misc
+from samba.provision import DEFAULTSITE
+
+# note we must connect to the local ldb file on disk, in order to
+# add system-only nTDSDSA objects
+parser = optparse.OptionParser("attr_from_server.py <LDB-filepath>")
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+ldb_path = args[0]
+
+
+class FromServerAttrTest(samba.tests.TestCase):
+ def setUp(self):
+ super(FromServerAttrTest, self).setUp()
+ self.ldb = samba.tests.connect_samdb(ldb_path)
+
+ def tearDown(self):
+ super(FromServerAttrTest, self).tearDown()
+
+ def set_attribute(self, dn, attr, value, operation=ldb.FLAG_MOD_ADD):
+ """Modifies an attribute for an object"""
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, dn)
+ m[attr] = ldb.MessageElement(value, operation, attr)
+ self.ldb.modify(m)
+
+ def get_object_guid(self, dn):
+ res = self.ldb.search(base=dn, attrs=["objectGUID"],
+ scope=ldb.SCOPE_BASE)
+ self.assertTrue(len(res) == 1)
+ return str(misc.GUID(res[0]['objectGUID'][0]))
+
+ def test_dangling_server_attr(self):
+ """
+ Tests a scenario where an object has a fromServer attribute that points
+ to an object that no longer exists.
+ """
+
+ # add a temporary server and its associated NTDS Settings object
+ config_dn = self.ldb.get_config_basedn()
+ sites_dn = "CN=Sites,{0}".format(config_dn)
+ servers_dn = "CN=Servers,CN={0},{1}".format(DEFAULTSITE, sites_dn)
+ tmp_server = "CN=TMPSERVER,{0}".format(servers_dn)
+ self.ldb.add({"dn": tmp_server, "objectclass": "server"})
+ server_guid = self.get_object_guid(tmp_server)
+ tmp_ntds_settings = "CN=NTDS Settings,{0}".format(tmp_server)
+ self.ldb.add({"dn": tmp_ntds_settings, "objectClass": "nTDSDSA"},
+ ["relax:0"])
+
+ # add an NTDS connection under the testenv DC that points to the tmp DC
+ testenv_dc = "CN={0},{1}".format(os.environ["SERVER"], servers_dn)
+ ntds_conn = "CN=Test-NTDS-Conn,CN=NTDS Settings,{0}".format(testenv_dc)
+ ldif = """
+dn: {dn}
+objectClass: nTDSConnection
+fromServer: CN=NTDS Settings,{fromServer}
+options: 1
+enabledConnection: TRUE
+""".format(dn=ntds_conn, fromServer=tmp_server)
+ self.ldb.add_ldif(ldif)
+ self.addCleanup(self.ldb.delete, ntds_conn)
+
+ # sanity-check we can modify the NTDS Connection object
+ self.set_attribute(ntds_conn, 'description', 'Test-value')
+
+ # sanity-check we can't modify the fromServer to point to a bad DN
+ try:
+ bad_dn = "CN=NTDS Settings,CN=BAD-DC,{0}".format(servers_dn)
+ self.set_attribute(ntds_conn, 'fromServer', bad_dn,
+ operation=ldb.FLAG_MOD_REPLACE)
+ self.fail("Successfully set fromServer to bad DN")
+ except ldb.LdbError as err:
+ enum = err.args[0]
+ self.assertEqual(enum, ldb.ERR_CONSTRAINT_VIOLATION)
+
+ # delete the tmp server, i.e. pretend we demoted it
+ self.ldb.delete(tmp_server, ["tree_delete:1"])
+
+ # check we can still see the deleted server object
+ search_expr = '(objectGUID={0})'.format(server_guid)
+ res = self.ldb.search(config_dn, scope=ldb.SCOPE_SUBTREE,
+ expression=search_expr,
+ controls=["show_deleted:1"])
+ self.assertTrue(len(res) == 1, "Could not find deleted server entry")
+
+ # now pretend some time has passed and the deleted server object
+ # has been tombstone-expunged from the DB
+ time.sleep(1)
+ current_time = int(time.time())
+ self.ldb.garbage_collect_tombstones([str(config_dn)], current_time,
+ tombstone_lifetime=0)
+
+ # repeat the search to sanity-check the deleted object is really gone
+ res = self.ldb.search(config_dn, scope=ldb.SCOPE_SUBTREE,
+ expression=search_expr,
+ controls=["show_deleted:1"])
+ self.assertTrue(len(res) == 0, "Did not expunge deleted server")
+
+ # the nTDSConnection now has a (mandatory) fromServer attribute that
+ # points to an object that no longer exists. Now try to modify an
+ # unrelated attribute on the nTDSConnection
+ try:
+ self.set_attribute(ntds_conn, 'description', 'Test-value-2',
+ operation=ldb.FLAG_MOD_REPLACE)
+ except ldb.LdbError as err:
+ print(err)
+ self.fail("Could not modify NTDS connection")
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/confidential_attr.py b/source4/dsdb/tests/python/confidential_attr.py
new file mode 100755
index 0000000..edbc959
--- /dev/null
+++ b/source4/dsdb/tests/python/confidential_attr.py
@@ -0,0 +1,1137 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Tests that confidential attributes (or attributes protected by a ACL that
+# denies read access) cannot be guessed through wildcard DB searches.
+#
+# Copyright (C) Catalyst.Net Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+
+import samba
+import random
+import statistics
+import time
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+import samba.getopt as options
+from ldb import SCOPE_BASE, SCOPE_SUBTREE
+from samba.dsdb import SEARCH_FLAG_CONFIDENTIAL, SEARCH_FLAG_RODC_ATTRIBUTE, SEARCH_FLAG_PRESERVEONDELETE
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE, FLAG_MOD_ADD
+from samba.auth import system_session
+from samba import gensec, sd_utils
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+from samba.dcerpc import security
+
+import samba.tests
+import samba.dsdb
+
+parser = optparse.OptionParser("confidential_attr.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]
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+class ConfidentialAttrCommon(samba.tests.TestCase):
+
+ def setUp(self):
+ super(ConfidentialAttrCommon, self).setUp()
+
+ self.ldb_admin = SamDB(ldaphost, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.user_pass = "samba123@"
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.schema_dn = self.ldb_admin.get_schema_basedn()
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+
+ # the tests work by setting the 'Confidential' bit in the searchFlags
+ # for an existing schema attribute. This only works against Windows if
+ # the systemFlags does not have FLAG_SCHEMA_BASE_OBJECT set for the
+ # schema attribute being modified. There are only a few attributes that
+ # meet this criteria (most of which only apply to 'user' objects)
+ self.conf_attr = "homePostalAddress"
+ attr_cn = "CN=Address-Home"
+ # schemaIdGuid for homePostalAddress (used for ACE tests)
+ self.conf_attr_guid = "16775781-47f3-11d1-a9c3-0000f80367c1"
+ self.conf_attr_sec_guid = "77b5b886-944a-11d1-aebd-0000f80367c1"
+ self.attr_dn = "{0},{1}".format(attr_cn, self.schema_dn)
+
+ userou = "OU=conf-attr-test"
+ self.ou = "{0},{1}".format(userou, self.base_dn)
+ samba.tests.delete_force(self.ldb_admin, self.ou, controls=['tree_delete:1'])
+ self.ldb_admin.create_ou(self.ou)
+ self.addCleanup(samba.tests.delete_force, self.ldb_admin, self.ou, controls=['tree_delete:1'])
+
+ # use a common username prefix, so we can use sAMAccountName=CATC-* as
+ # a search filter to only return the users we're interested in
+ self.user_prefix = "catc-"
+
+ # add a test object with this attribute set
+ self.conf_value = "abcdef"
+ self.conf_user = "{0}conf-user".format(self.user_prefix)
+ self.ldb_admin.newuser(self.conf_user, self.user_pass, userou=userou)
+ self.conf_dn = self.get_user_dn(self.conf_user)
+ self.add_attr(self.conf_dn, self.conf_attr, self.conf_value)
+
+ # add a sneaky user that will try to steal our secrets
+ self.user = "{0}sneaky-user".format(self.user_prefix)
+ self.ldb_admin.newuser(self.user, self.user_pass, userou=userou)
+ self.ldb_user = self.get_ldb_connection(self.user, self.user_pass)
+
+ self.all_users = [self.user, self.conf_user]
+
+ # add some other users that also have confidential attributes, so we
+ # check we don't disclose their details, particularly in '!' searches
+ for i in range(1, 3):
+ username = "{0}other-user{1}".format(self.user_prefix, i)
+ self.ldb_admin.newuser(username, self.user_pass, userou=userou)
+ userdn = self.get_user_dn(username)
+ self.add_attr(userdn, self.conf_attr, "xyz{0}".format(i))
+ self.all_users.append(username)
+
+ # there are 4 users in the OU, plus the OU itself
+ self.test_dn = self.ou
+ self.total_objects = len(self.all_users) + 1
+ self.objects_with_attr = 3
+
+ # sanity-check the flag is not already set (this'll cause problems if
+ # previous test run didn't clean up properly)
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+ if search_flags & SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE:
+ self.set_attr_search_flags(self.attr_dn, str(search_flags &~ (SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE)))
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+ self.assertEqual(0, search_flags & (SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE),
+ f"{self.conf_attr} searchFlags did not reset to omit SEARCH_FLAG_CONFIDENTIAL and SEARCH_FLAG_RODC_ATTRIBUTE ({search_flags})")
+
+ def add_attr(self, dn, attr, value):
+ m = Message()
+ m.dn = Dn(self.ldb_admin, dn)
+ m[attr] = MessageElement(value, FLAG_MOD_ADD, attr)
+ self.ldb_admin.modify(m)
+
+ def set_attr_search_flags(self, attr_dn, flags):
+ """Modifies the searchFlags for an object in the schema"""
+ m = Message()
+ m.dn = Dn(self.ldb_admin, attr_dn)
+ m['searchFlags'] = MessageElement(flags, FLAG_MOD_REPLACE,
+ 'searchFlags')
+ self.ldb_admin.modify(m)
+
+ # note we have to update the schema for this change to take effect (on
+ # Windows, at least)
+ self.ldb_admin.set_schema_update_now()
+
+ def get_attr_search_flags(self, attr_dn):
+ """Marks the attribute under test as being confidential"""
+ res = self.ldb_admin.search(attr_dn, scope=SCOPE_BASE,
+ attrs=['searchFlags'])
+ return res[0]['searchFlags'][0]
+
+ def make_attr_confidential(self):
+ """Marks the attribute under test as being confidential"""
+
+ # work out the original 'searchFlags' value before we overwrite it
+ old_value = self.get_attr_search_flags(self.attr_dn)
+
+ self.set_attr_search_flags(self.attr_dn, str(SEARCH_FLAG_CONFIDENTIAL))
+
+ # reset the value after the test completes
+ self.addCleanup(self.set_attr_search_flags, self.attr_dn, old_value)
+
+ def get_user_dn(self, name):
+ return "CN={0},{1}".format(name, self.ou)
+
+ def get_user_sid_string(self, username):
+ user_dn = self.get_user_dn(username)
+ user_sid = self.sd_utils.get_object_sid(user_dn)
+ return str(user_sid)
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ features = creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL
+ creds_tmp.set_gensec_features(features)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS)
+ ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ def assert_search_result(self, expected_num, expr, samdb):
+
+ # try asking for different attributes back: None/all, the confidential
+ # attribute itself, and a random unrelated attribute
+ attr_filters = [None, ["*"], [self.conf_attr], ['name']]
+ for attr in attr_filters:
+ res = samdb.search(self.test_dn, expression=expr,
+ scope=SCOPE_SUBTREE, attrs=attr)
+ self.assertEqual(len(res), expected_num,
+ "%u results, not %u for search %s, attr %s" %
+ (len(res), expected_num, expr, str(attr)))
+
+ # return a selection of searches that match exactly against the test object
+ def get_exact_match_searches(self):
+ first_char = self.conf_value[:1]
+ last_char = self.conf_value[-1:]
+ test_attr = self.conf_attr
+
+ searches = [
+ # search for the attribute using a sub-string wildcard
+ # (which could reveal the attribute's actual value)
+ "({0}={1}*)".format(test_attr, first_char),
+ "({0}=*{1})".format(test_attr, last_char),
+
+ # sanity-check equality against an exact match on value
+ "({0}={1})".format(test_attr, self.conf_value),
+
+ # '~=' searches don't work against Samba
+ # sanity-check an approx search against an exact match on value
+ # "({0}~={1})".format(test_attr, self.conf_value),
+
+ # check wildcard in an AND search...
+ "(&({0}={1}*)(objectclass=*))".format(test_attr, first_char),
+
+ # ...an OR search (against another term that will never match)
+ "(|({0}={1}*)(objectclass=banana))".format(test_attr, first_char)]
+
+ return searches
+
+ # return searches that match any object with the attribute under test
+ def get_match_all_searches(self):
+ searches = [
+ # check a full wildcard against the confidential attribute
+ # (which could reveal the attribute's presence/absence)
+ "({0}=*)".format(self.conf_attr),
+
+ # check wildcard in an AND search...
+ "(&(objectclass=*)({0}=*))".format(self.conf_attr),
+
+ # ...an OR search (against another term that will never match)
+ "(|(objectclass=banana)({0}=*))".format(self.conf_attr),
+
+ # check <=, and >= expressions that would normally find a match
+ "({0}>=0)".format(self.conf_attr),
+ "({0}<=ZZZZZZZZZZZ)".format(self.conf_attr)]
+
+ return searches
+
+ def assert_conf_attr_searches(self, has_rights_to=0, samdb=None):
+ """Check searches against the attribute under test work as expected"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ if has_rights_to == "all":
+ has_rights_to = self.objects_with_attr
+
+ # these first few searches we just expect to match against the one
+ # object under test that we're trying to guess the value of
+ expected_num = 1 if has_rights_to > 0 else 0
+ for search in self.get_exact_match_searches():
+ self.assert_search_result(expected_num, search, samdb)
+
+ # these next searches will match any objects we have rights to see
+ expected_num = has_rights_to
+ for search in self.get_match_all_searches():
+ self.assert_search_result(expected_num, search, samdb)
+
+ # The following are double negative searches (i.e. NOT non-matching-
+ # condition) which will therefore match ALL objects, including the test
+ # object(s).
+ def get_negative_match_all_searches(self):
+ first_char = self.conf_value[:1]
+ last_char = self.conf_value[-1:]
+ not_first_char = chr(ord(first_char) + 1)
+ not_last_char = chr(ord(last_char) + 1)
+
+ searches = [
+ "(!({0}={1}*))".format(self.conf_attr, not_first_char),
+ "(!({0}=*{1}))".format(self.conf_attr, not_last_char)]
+ return searches
+
+ # the following searches will not match against the test object(s). So
+ # a user with sufficient rights will see an inverse sub-set of objects.
+ # (An unprivileged user would either see all objects on Windows, or no
+ # objects on Samba)
+ def get_inverse_match_searches(self):
+ first_char = self.conf_value[:1]
+ last_char = self.conf_value[-1:]
+ searches = [
+ "(!({0}={1}*))".format(self.conf_attr, first_char),
+ "(!({0}=*{1}))".format(self.conf_attr, last_char)]
+ return searches
+
+ def negative_searches_all_rights(self, total_objects=None):
+ expected_results = {}
+
+ if total_objects is None:
+ total_objects = self.total_objects
+
+ # these searches should match ALL objects (including the OU)
+ for search in self.get_negative_match_all_searches():
+ expected_results[search] = total_objects
+
+ # a ! wildcard should only match the objects without the attribute
+ search = "(!({0}=*))".format(self.conf_attr)
+ expected_results[search] = total_objects - self.objects_with_attr
+
+ # whereas the inverse searches should match all objects *except* the
+ # one under test
+ for search in self.get_inverse_match_searches():
+ expected_results[search] = total_objects - 1
+
+ return expected_results
+
+ # Returns the expected negative (i.e. '!') search behaviour when talking to
+ # a DC, i.e. we assert that users
+ # without rights always see ALL objects in '!' searches
+ def negative_searches_return_all(self, has_rights_to=0,
+ total_objects=None):
+ """Asserts user without rights cannot see objects in '!' searches"""
+ expected_results = {}
+
+ if total_objects is None:
+ total_objects = self.total_objects
+
+ # Windows 'hides' objects by always returning all of them, so negative
+ # searches that match all objects will simply return all objects
+ for search in self.get_negative_match_all_searches():
+ expected_results[search] = total_objects
+
+ # if we're matching on everything except the one object under test
+ # (i.e. the inverse subset), we'll still see all objects if
+ # has_rights_to == 0. Or we'll see all bar one if has_rights_to == 1.
+ inverse_searches = self.get_inverse_match_searches()
+ inverse_searches += ["(!({0}=*))".format(self.conf_attr)]
+
+ for search in inverse_searches:
+ expected_results[search] = total_objects - has_rights_to
+
+ return expected_results
+
+ # Returns the expected negative (i.e. '!') search behaviour. This varies
+ # depending on what type of DC we're talking to (i.e. Windows or Samba)
+ # and what access rights the user has.
+ # Note we only handle has_rights_to="all", 1 (the test object), or 0 (i.e.
+ # we don't have rights to any objects)
+ def negative_search_expected_results(self, has_rights_to, total_objects=None):
+
+ if has_rights_to == "all":
+ expect_results = self.negative_searches_all_rights(total_objects)
+
+ else:
+ expect_results = self.negative_searches_return_all(has_rights_to,
+ total_objects)
+ return expect_results
+
+ def assert_negative_searches(self, has_rights_to=0, samdb=None):
+ """Asserts user without rights cannot see objects in '!' searches"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # build a dictionary of key=search-expr, value=expected_num assertions
+ expected_results = self.negative_search_expected_results(has_rights_to)
+
+ for search, expected_num in expected_results.items():
+ self.assert_search_result(expected_num, search, samdb)
+
+ def assert_attr_returned(self, expect_attr, samdb, attrs):
+ # does a query that should always return a successful result, and
+ # checks whether the confidential attribute is present
+ res = samdb.search(self.conf_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(1, len(res))
+
+ attr_returned = False
+ for msg in res:
+ if self.conf_attr in msg:
+ attr_returned = True
+ self.assertEqual(expect_attr, attr_returned)
+
+ def assert_attr_visible(self, expect_attr, samdb=None):
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # sanity-check confidential attribute is/isn't returned as expected
+ # based on the filter attributes we ask for
+ self.assert_attr_returned(expect_attr, samdb, attrs=None)
+ self.assert_attr_returned(expect_attr, samdb, attrs=["*"])
+ self.assert_attr_returned(expect_attr, samdb, attrs=[self.conf_attr])
+
+ # filtering on a different attribute should never return the conf_attr
+ self.assert_attr_returned(expect_attr=False, samdb=samdb,
+ attrs=['name'])
+
+ def assert_attr_visible_to_admin(self):
+ # sanity-check the admin user can always see the confidential attribute
+ self.assert_conf_attr_searches(has_rights_to="all",
+ samdb=self.ldb_admin)
+ self.assert_negative_searches(has_rights_to="all",
+ samdb=self.ldb_admin)
+ self.assert_attr_visible(expect_attr=True, samdb=self.ldb_admin)
+
+
+class ConfidentialAttrTest(ConfidentialAttrCommon):
+ def test_basic_search(self):
+ """Basic test confidential attributes aren't disclosed via searches"""
+
+ # check we can see a non-confidential attribute in a basic searches
+ self.assert_conf_attr_searches(has_rights_to="all")
+ self.assert_negative_searches(has_rights_to="all")
+ self.assert_attr_visible(expect_attr=True)
+
+ # now make the attribute confidential. Repeat the tests and check that
+ # an ordinary user can't see the attribute, or indirectly match on the
+ # attribute via the search expression
+ self.make_attr_confidential()
+
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+
+ # sanity-check we haven't hidden the attribute from the admin as well
+ self.assert_attr_visible_to_admin()
+
+ def _test_search_with_allow_acl(self, allow_ace):
+ """Checks a ACE with 'CR' rights can override a confidential attr"""
+ # make the test attribute confidential and check user can't see it
+ self.make_attr_confidential()
+
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+
+ # apply the allow ACE to the object under test
+ self.sd_utils.dacl_add_ace(self.conf_dn, allow_ace)
+
+ # the user should now be able to see the attribute for the one object
+ # we gave it rights to
+ self.assert_conf_attr_searches(has_rights_to=1)
+ self.assert_negative_searches(has_rights_to=1)
+ self.assert_attr_visible(expect_attr=True)
+
+ # sanity-check the admin can still see the attribute
+ self.assert_attr_visible_to_admin()
+
+ def test_search_with_attr_acl_override(self):
+ """Make the confidential attr visible via an OA attr ACE"""
+
+ # set the SEC_ADS_CONTROL_ACCESS bit ('CR') for the user for the
+ # attribute under test, so the user can see it once more
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OA;;CR;{0};;{1})".format(self.conf_attr_guid, user_sid)
+
+ self._test_search_with_allow_acl(ace)
+
+ def test_search_with_propset_acl_override(self):
+ """Make the confidential attr visible via a Property-set ACE"""
+
+ # set the SEC_ADS_CONTROL_ACCESS bit ('CR') for the user for the
+ # property-set containing the attribute under test (i.e. the
+ # attributeSecurityGuid), so the user can see it once more
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OA;;CR;{0};;{1})".format(self.conf_attr_sec_guid, user_sid)
+
+ self._test_search_with_allow_acl(ace)
+
+ def test_search_with_acl_override(self):
+ """Make the confidential attr visible via a general 'allow' ACE"""
+
+ # set the allow SEC_ADS_CONTROL_ACCESS bit ('CR') for the user
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(A;;CR;;;{0})".format(user_sid)
+
+ self._test_search_with_allow_acl(ace)
+
+ def test_search_with_blanket_oa_acl(self):
+ """Make the confidential attr visible via a non-specific OA ACE"""
+
+ # this just checks that an Object Access (OA) ACE without a GUID
+ # specified will work the same as an 'Access' (A) ACE
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OA;;CR;;;{0})".format(user_sid)
+
+ self._test_search_with_allow_acl(ace)
+
+ def _test_search_with_neutral_acl(self, neutral_ace):
+ """Checks that a user does NOT gain access via an unrelated ACE"""
+
+ # make the test attribute confidential and check user can't see it
+ self.make_attr_confidential()
+
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+
+ # apply the ACE to the object under test
+ self.sd_utils.dacl_add_ace(self.conf_dn, neutral_ace)
+
+ # this should make no difference to the user's ability to see the attr
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+
+ # sanity-check the admin can still see the attribute
+ self.assert_attr_visible_to_admin()
+
+ def test_search_with_neutral_acl(self):
+ """Give the user all rights *except* CR for any attributes"""
+
+ # give the user all rights *except* CR and check it makes no difference
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(A;;RPWPCCDCLCLORCWOWDSDDTSW;;;{0})".format(user_sid)
+ self._test_search_with_neutral_acl(ace)
+
+ def test_search_with_neutral_attr_acl(self):
+ """Give the user all rights *except* CR for the attribute under test"""
+
+ # giving user all OA rights *except* CR should make no difference
+ user_sid = self.get_user_sid_string(self.user)
+ rights = "RPWPCCDCLCLORCWOWDSDDTSW"
+ ace = "(OA;;{0};{1};;{2})".format(rights, self.conf_attr_guid, user_sid)
+ self._test_search_with_neutral_acl(ace)
+
+ def test_search_with_neutral_cr_acl(self):
+ """Give the user CR rights for *another* unrelated attribute"""
+
+ # giving user object-access CR rights to an unrelated attribute
+ user_sid = self.get_user_sid_string(self.user)
+ # use the GUID for sAMAccountName here (for no particular reason)
+ unrelated_attr = "3e0abfd0-126a-11d0-a060-00aa006c33ed"
+ ace = "(OA;;CR;{0};;{1})".format(unrelated_attr, user_sid)
+ self._test_search_with_neutral_acl(ace)
+
+
+# Check that a Deny ACL on an attribute doesn't reveal confidential info
+class ConfidentialAttrTestDenyAcl(ConfidentialAttrCommon):
+
+ def assert_not_in_result(self, res, exclude_dn):
+ for msg in res:
+ self.assertNotEqual(msg.dn, exclude_dn,
+ "Search revealed object {0}".format(exclude_dn))
+
+ # deny ACL tests are slightly different as we are only denying access to
+ # the one object under test (rather than any objects with that attribute).
+ # Therefore we need an extra check that we don't reveal the test object
+ # in the search, if we're not supposed to
+ def assert_search_result(self, expected_num, expr, samdb,
+ excl_testobj=False):
+
+ # try asking for different attributes back: None/all, the confidential
+ # attribute itself, and a random unrelated attribute
+ attr_filters = [None, ["*"], [self.conf_attr], ['name']]
+ for attr in attr_filters:
+ res = samdb.search(self.test_dn, expression=expr,
+ scope=SCOPE_SUBTREE, attrs=attr)
+ self.assertEqual(len(res), expected_num,
+ "%u results, not %u for search %s, attr %s" %
+ (len(res), expected_num, expr, str(attr)))
+
+ # assert we haven't revealed the hidden test-object
+ if excl_testobj:
+ self.assert_not_in_result(res, exclude_dn=self.conf_dn)
+
+ # we make a few tweaks to the regular version of this function to cater to
+ # denying specifically one object via an ACE
+ def assert_conf_attr_searches(self, has_rights_to=0, samdb=None):
+ """Check searches against the attribute under test work as expected"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # make sure the test object is not returned if we've been denied rights
+ # to it via an ACE
+ excl_testobj = has_rights_to == "deny-one"
+
+ # these first few searches we just expect to match against the one
+ # object under test that we're trying to guess the value of
+ expected_num = 1 if has_rights_to == "all" else 0
+
+ for search in self.get_exact_match_searches():
+ self.assert_search_result(expected_num, search, samdb,
+ excl_testobj)
+
+ # these next searches will match any objects with the attribute that
+ # we have rights to see (i.e. all except the object under test)
+ if has_rights_to == "all":
+ expected_num = self.objects_with_attr
+ elif has_rights_to == "deny-one":
+ expected_num = self.objects_with_attr - 1
+
+ for search in self.get_match_all_searches():
+ self.assert_search_result(expected_num, search, samdb,
+ excl_testobj)
+
+ # override method specifically for deny ACL test cases
+ def negative_searches_return_all(self, has_rights_to=0,
+ total_objects=None):
+ expected_results = {}
+
+ # When a user lacks access rights to an object, Windows 'hides' it in
+ # '!' searches by always returning it, regardless of whether it matches
+ searches = self.get_negative_match_all_searches()
+ searches += self.get_inverse_match_searches()
+ for search in searches:
+ expected_results[search] = self.total_objects
+
+ # in the wildcard case, the one object we don't have rights to gets
+ # bundled in with the objects that don't have the attribute at all
+ search = "(!({0}=*))".format(self.conf_attr)
+ has_rights_to = self.objects_with_attr - 1
+ expected_results[search] = self.total_objects - has_rights_to
+ return expected_results
+
+ # override method specifically for deny ACL test cases
+ def assert_negative_searches(self, has_rights_to=0, samdb=None):
+ """Asserts user without rights cannot see objects in '!' searches"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # As the deny ACL is only denying access to one particular object, add
+ # an extra check that the denied object is not returned. (We can only
+ # assert this if the '!'/negative search behaviour is to suppress any
+ # objects we don't have access rights to)
+ excl_testobj = False
+
+ # build a dictionary of key=search-expr, value=expected_num assertions
+ expected_results = self.negative_search_expected_results(has_rights_to)
+
+ for search, expected_num in expected_results.items():
+ self.assert_search_result(expected_num, search, samdb,
+ excl_testobj=excl_testobj)
+
+ def _test_search_with_deny_acl(self, ace):
+ # check the user can see the attribute initially
+ self.assert_conf_attr_searches(has_rights_to="all")
+ self.assert_negative_searches(has_rights_to="all")
+ self.assert_attr_visible(expect_attr=True)
+
+ # add the ACE that denies access to the attr under test
+ self.sd_utils.dacl_add_ace(self.conf_dn, ace)
+
+ # the user shouldn't be able to see the attribute anymore
+ self.assert_conf_attr_searches(has_rights_to="deny-one")
+ self.assert_negative_searches(has_rights_to="deny-one")
+ self.assert_attr_visible(expect_attr=False)
+
+ # sanity-check we haven't hidden the attribute from the admin as well
+ self.assert_attr_visible_to_admin()
+
+ def test_search_with_deny_attr_acl(self):
+ """Checks a deny ACE works the same way as a confidential attribute"""
+
+ # add an ACE that denies the user Read Property (RP) access to the attr
+ # (which is similar to making the attribute confidential)
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OD;;RP;{0};;{1})".format(self.conf_attr_guid, user_sid)
+
+ # check the user cannot see the attribute anymore
+ self._test_search_with_deny_acl(ace)
+
+ def test_search_with_deny_acl(self):
+ """Checks a blanket deny ACE denies access to an object's attributes"""
+
+ # add an blanket deny ACE for Read Property (RP) rights
+ user_dn = self.get_user_dn(self.user)
+ user_sid = self.sd_utils.get_object_sid(user_dn)
+ ace = "(D;;RP;;;{0})".format(str(user_sid))
+
+ # check the user cannot see the attribute anymore
+ self._test_search_with_deny_acl(ace)
+
+ def test_search_with_deny_propset_acl(self):
+ """Checks a deny ACE on the attribute's Property-Set"""
+
+ # add an blanket deny ACE for Read Property (RP) rights
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OD;;RP;{0};;{1})".format(self.conf_attr_sec_guid, user_sid)
+
+ # check the user cannot see the attribute anymore
+ self._test_search_with_deny_acl(ace)
+
+ def test_search_with_blanket_oa_deny_acl(self):
+ """Checks a non-specific 'OD' ACE works the same as a 'D' ACE"""
+
+ # this just checks that adding a 'Object Deny' (OD) ACE without
+ # specifying a GUID will work the same way as a 'Deny' (D) ACE
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OD;;RP;;;{0})".format(user_sid)
+
+ # check the user cannot see the attribute anymore
+ self._test_search_with_deny_acl(ace)
+
+
+# Check that using the dirsync controls doesn't reveal confidential attributes
+class ConfidentialAttrTestDirsync(ConfidentialAttrCommon):
+
+ def setUp(self):
+ super(ConfidentialAttrTestDirsync, self).setUp()
+ self.dirsync = ["dirsync:1:1:1000"]
+
+ # because we need to search on the base DN when using the dirsync
+ # controls, we need an extra filter for the inverse ('!') search,
+ # so we don't get thousands of objects returned
+ self.extra_filter = \
+ "(&(samaccountname={0}*)(!(isDeleted=*)))".format(self.user_prefix)
+ self.single_obj_filter = \
+ "(&(samaccountname={0})(!(isDeleted=*)))".format(self.conf_user)
+
+ self.attr_filters = [None, ["*"], ["name"]]
+
+ # Note dirsync behaviour is slightly different for the attribute under
+ # test - when you have full access rights, it only returns the objects
+ # that actually have this attribute (i.e. it doesn't return an empty
+ # message with just the DN). So we add the 'name' attribute into the
+ # attribute filter to avoid complicating our assertions further
+ self.attr_filters += [[self.conf_attr, "name"]]
+
+ # override method specifically for dirsync, i.e. add dirsync controls
+ def assert_search_result(self, expected_num, expr, samdb, base_dn=None):
+
+ # Note dirsync must always search on the partition base DN
+ base_dn = self.base_dn
+
+ # we need an extra filter for dirsync because:
+ # - we search on the base DN, so otherwise the '!' searches return
+ # thousands of unrelated results, and
+ # - we make the test attribute preserve-on-delete in one case, so we
+ # want to weed out results from any previous test runs
+ search = "(&{0}{1})".format(expr, self.extra_filter)
+
+ # If we expect to return multiple results, only check the first
+ if expected_num > 0:
+ attr_filters = [self.attr_filters[0]]
+ else:
+ attr_filters = self.attr_filters
+
+ for attr in attr_filters:
+ res = samdb.search(base_dn, expression=search, scope=SCOPE_SUBTREE,
+ attrs=attr, controls=self.dirsync)
+ self.assertEqual(len(res), expected_num,
+ "%u results, not %u for search %s, attr %s" %
+ (len(res), expected_num, search, str(attr)))
+
+ # override method specifically for dirsync, i.e. add dirsync controls
+ def assert_attr_returned(self, expect_attr, samdb, attrs,
+ no_result_ok=False):
+
+ # When using dirsync, the base DN we search on needs to be a naming
+ # context. Add an extra filter to ignore all the objects we aren't
+ # interested in
+ expr = self.single_obj_filter
+ res = samdb.search(self.base_dn, expression=expr, scope=SCOPE_SUBTREE,
+ attrs=attrs, controls=self.dirsync)
+ if not no_result_ok:
+ self.assertEqual(1, len(res))
+
+ attr_returned = False
+ for msg in res:
+ if self.conf_attr in msg and len(msg[self.conf_attr]) > 0:
+ attr_returned = True
+ self.assertEqual(expect_attr, attr_returned)
+
+ # override method specifically for dirsync (it has slightly different
+ # behaviour to normal when requesting specific attributes)
+ def assert_attr_visible(self, expect_attr, samdb=None):
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # sanity-check confidential attribute is/isn't returned as expected
+ # based on the filter attributes we ask for
+ self.assert_attr_returned(expect_attr, samdb, attrs=None)
+ self.assert_attr_returned(expect_attr, samdb, attrs=["*"])
+
+ if expect_attr:
+ self.assert_attr_returned(expect_attr, samdb,
+ attrs=[self.conf_attr])
+ else:
+ # The behaviour with dirsync when asking solely for an attribute
+ # that you don't have rights to is a bit strange. Samba returns
+ # no result rather than an empty message with just the DN.
+ # Presumably this is due to dirsync module behaviour. It's not
+ # disclosive in that the DC behaves the same way as if you asked
+ # for a garbage/non-existent attribute
+ self.assert_attr_returned(expect_attr, samdb,
+ attrs=[self.conf_attr],
+ no_result_ok=True)
+ self.assert_attr_returned(expect_attr, samdb,
+ attrs=["garbage"], no_result_ok=True)
+
+ # filtering on a different attribute should never return the conf_attr
+ self.assert_attr_returned(expect_attr=False, samdb=samdb,
+ attrs=['name'])
+
+ # override method specifically for dirsync (total object count differs)
+ def assert_negative_searches(self, has_rights_to=0, samdb=None):
+ """Asserts user without rights cannot see objects in '!' searches"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # because dirsync uses an extra filter, the total objects we expect
+ # here only includes the user objects (not the parent OU)
+ total_objects = len(self.all_users)
+ expected_results = self.negative_search_expected_results(has_rights_to,
+ total_objects)
+
+ for search, expected_num in expected_results.items():
+ self.assert_search_result(expected_num, search, samdb)
+
+ def test_search_with_dirsync(self):
+ """Checks dirsync controls don't reveal confidential attributes"""
+
+ self.assert_conf_attr_searches(has_rights_to="all")
+ self.assert_attr_visible(expect_attr=True)
+ self.assert_negative_searches(has_rights_to="all")
+
+ # make the test attribute confidential and check user can't see it,
+ # even if they use the dirsync controls
+ self.make_attr_confidential()
+
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+ self.assert_negative_searches(has_rights_to=0)
+
+ # as a final sanity-check, make sure the admin can still see the attr
+ self.assert_conf_attr_searches(has_rights_to="all",
+ samdb=self.ldb_admin)
+ self.assert_attr_visible(expect_attr=True, samdb=self.ldb_admin)
+ self.assert_negative_searches(has_rights_to="all",
+ samdb=self.ldb_admin)
+
+ def get_guid_string(self, dn):
+ """Returns an object's GUID (in string format)"""
+ res = self.ldb_admin.search(base=dn, attrs=["objectGUID"],
+ scope=SCOPE_BASE)
+ guid = res[0]['objectGUID'][0]
+ return self.ldb_admin.schema_format_value("objectGUID", guid).decode('utf-8')
+
+ def make_attr_preserve_on_delete(self):
+ """Marks the attribute under test as being preserve on delete"""
+
+ # work out the original 'searchFlags' value before we overwrite it
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+
+ # check we've already set the confidential flag
+ self.assertNotEqual(0, search_flags & SEARCH_FLAG_CONFIDENTIAL)
+ search_flags |= SEARCH_FLAG_PRESERVEONDELETE
+
+ self.set_attr_search_flags(self.attr_dn, str(search_flags))
+
+ def change_attr_under_test(self, attr_name, attr_cn):
+ # change the attribute that the test code uses
+ self.conf_attr = attr_name
+ self.attr_dn = "{0},{1}".format(attr_cn, self.schema_dn)
+
+ # set the new attribute for the user-under-test
+ self.add_attr(self.conf_dn, self.conf_attr, self.conf_value)
+
+ # 2 other users also have the attribute-under-test set (to a randomish
+ # value). Set the new attribute for them now (normally this gets done
+ # in the setUp())
+ for username in self.all_users:
+ if "other-user" in username:
+ dn = self.get_user_dn(username)
+ self.add_attr(dn, self.conf_attr, "xyz-blah")
+
+ def test_search_with_dirsync_deleted_objects(self):
+ """Checks dirsync doesn't reveal confidential info for deleted objs"""
+
+ # change the attribute we're testing (we'll preserve on delete for this
+ # test case, which means the attribute-under-test hangs around after
+ # the test case finishes, and would interfere with the searches for
+ # subsequent other test cases)
+ self.change_attr_under_test("carLicense", "CN=carLicense")
+
+ # Windows dirsync behaviour is a little strange when you request
+ # attributes that deleted objects no longer have, so just request 'all
+ # attributes' to simplify the test logic
+ self.attr_filters = [None, ["*"]]
+
+ # normally dirsync uses extra filters to exclude deleted objects that
+ # we're not interested in. Override these filters so they WILL include
+ # deleted objects, but only from this particular test run. We can do
+ # this by matching lastKnownParent against this test case's OU, which
+ # will match any deleted child objects.
+ ou_guid = self.get_guid_string(self.ou)
+ deleted_filter = "(lastKnownParent=<GUID={0}>)".format(ou_guid)
+
+ # the extra-filter will get combined via AND with the search expression
+ # we're testing, i.e. filter on the confidential attribute AND only
+ # include non-deleted objects, OR deleted objects from this test run
+ exclude_deleted_objs_filter = self.extra_filter
+ self.extra_filter = "(|{0}{1})".format(exclude_deleted_objs_filter,
+ deleted_filter)
+
+ # for matching on a single object, the search expresseion becomes:
+ # match exactly by account-name AND either a non-deleted object OR a
+ # deleted object from this test run
+ match_by_name = "(samaccountname={0})".format(self.conf_user)
+ not_deleted = "(!(isDeleted=*))"
+ self.single_obj_filter = "(&{0}(|{1}{2}))".format(match_by_name,
+ not_deleted,
+ deleted_filter)
+
+ # check that the search filters work as expected
+ self.assert_conf_attr_searches(has_rights_to="all")
+ self.assert_attr_visible(expect_attr=True)
+ self.assert_negative_searches(has_rights_to="all")
+
+ # make the test attribute confidential *and* preserve on delete.
+ self.make_attr_confidential()
+ self.make_attr_preserve_on_delete()
+
+ # check we can't see the objects now, even with using dirsync controls
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+ self.assert_negative_searches(has_rights_to=0)
+
+ # now delete the users (except for the user whose LDB connection
+ # we're currently using)
+ for user in self.all_users:
+ if user is not self.user:
+ self.ldb_admin.delete(self.get_user_dn(user))
+
+ # check we still can't see the objects
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+
+ def test_timing_attack(self):
+ # Create the machine account.
+ mach_name = f'conf_timing_{random.randint(0, 0xffff)}'
+ mach_dn = Dn(self.ldb_admin, f'CN={mach_name},{self.ou}')
+ details = {
+ 'dn': mach_dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': f'{mach_name}$',
+ }
+ self.ldb_admin.add(details)
+
+ # Get the machine account's GUID.
+ res = self.ldb_admin.search(mach_dn,
+ attrs=['objectGUID'],
+ scope=SCOPE_BASE)
+ mach_guid = res[0].get('objectGUID', idx=0)
+
+ # Now we can create an msFVE-RecoveryInformation object that is a child
+ # of the machine account object.
+ recovery_dn = Dn(self.ldb_admin, str(mach_dn))
+ recovery_dn.add_child('CN=recovery_info')
+
+ secret_pw = 'Secret007'
+ not_secret_pw = 'Secret008'
+
+ secret_pw_utf8 = secret_pw.encode('utf-8')
+
+ # The crucial attribute, msFVE-RecoveryPassword, is a confidential
+ # attribute.
+ conf_attr = 'msFVE-RecoveryPassword'
+
+ m = Message(recovery_dn)
+ m['objectClass'] = 'msFVE-RecoveryInformation'
+ m['msFVE-RecoveryGuid'] = mach_guid
+ m[conf_attr] = secret_pw
+ self.ldb_admin.add(m)
+
+ attrs = [conf_attr]
+
+ # Search for the confidential attribute as administrator, ensuring it
+ # is visible.
+ res = self.ldb_admin.search(recovery_dn,
+ attrs=attrs,
+ scope=SCOPE_BASE)
+ self.assertEqual(1, len(res))
+ pw = res[0].get(conf_attr, idx=0)
+ self.assertEqual(secret_pw_utf8, pw)
+
+ # Repeat the search with an expression matching on the confidential
+ # attribute. This should also work.
+ res = self.ldb_admin.search(
+ recovery_dn,
+ attrs=attrs,
+ expression=f'({conf_attr}={secret_pw})',
+ scope=SCOPE_BASE)
+ self.assertEqual(1, len(res))
+ pw = res[0].get(conf_attr, idx=0)
+ self.assertEqual(secret_pw_utf8, pw)
+
+ # Search for the attribute as an unprivileged user. It should not be
+ # visible.
+ user_res = self.ldb_user.search(recovery_dn,
+ attrs=attrs,
+ scope=SCOPE_BASE)
+ pw = user_res[0].get(conf_attr, idx=0)
+ # The attribute should be None.
+ self.assertIsNone(pw)
+
+ # We use LDAP_MATCHING_RULE_TRANSITIVE_EVAL to create a search
+ # expression that takes a long time to execute, by setting off another
+ # search each time it is evaluated. It makes no difference that the
+ # object on which we're searching has no 'member' attribute.
+ dummy_dn = 'cn=user,cn=users,dc=samba,dc=example,dc=com'
+ slow_subexpr = f'(member:1.2.840.113556.1.4.1941:={dummy_dn})'
+ slow_expr = f'(|{slow_subexpr * 100})'
+
+ # The full search expression. It comprises a match on the confidential
+ # attribute joined by an AND to our slow search expression, The AND
+ # operator is short-circuiting, so if our first subexpression fails to
+ # match, we'll bail out of the search early. Otherwise, we'll evaluate
+ # the slow part; as its subexpressions are joined by ORs, and will all
+ # fail to match, every one of them will need to be evaluated. By
+ # measuring how long the search takes, we'll be able to infer whether
+ # the confidential attribute matched or not.
+
+ # This is bad if we are not an administrator, and are able to use this
+ # to determine the values of confidential attributes. Therefore we need
+ # to ensure we can't observe any difference in timing.
+ correct_expr = f'(&({conf_attr}={secret_pw}){slow_expr})'
+ wrong_expr = f'(&({conf_attr}={not_secret_pw}){slow_expr})'
+
+ def standard_uncertainty_bounds(times):
+ mean = statistics.mean(times)
+ stdev = statistics.stdev(times, mean)
+
+ return (mean - stdev, mean + stdev)
+
+ # Perform a number of searches with both correct and incorrect
+ # expressions, and return the uncertainty bounds for each.
+ def time_searches(samdb):
+ warmup_samples = 3
+ samples = 10
+ matching_times = []
+ non_matching_times = []
+
+ for _ in range(warmup_samples):
+ samdb.search(recovery_dn,
+ attrs=attrs,
+ expression=correct_expr,
+ scope=SCOPE_BASE)
+
+ for _ in range(samples):
+ # Measure the time taken for a search, for both a matching and
+ # a non-matching search expression.
+
+ prev = time.time()
+ samdb.search(recovery_dn,
+ attrs=attrs,
+ expression=correct_expr,
+ scope=SCOPE_BASE)
+ now = time.time()
+ matching_times.append(now - prev)
+
+ prev = time.time()
+ samdb.search(recovery_dn,
+ attrs=attrs,
+ expression=wrong_expr,
+ scope=SCOPE_BASE)
+ now = time.time()
+ non_matching_times.append(now - prev)
+
+ matching = standard_uncertainty_bounds(matching_times)
+ non_matching = standard_uncertainty_bounds(non_matching_times)
+ return matching, non_matching
+
+ def assertRangesDistinct(a, b):
+ a0, a1 = a
+ b0, b1 = b
+ self.assertLess(min(a1, b1), max(a0, b0))
+
+ def assertRangesOverlap(a, b):
+ a0, a1 = a
+ b0, b1 = b
+ self.assertGreaterEqual(min(a1, b1), max(a0, b0))
+
+ # For an administrator, the uncertainty bounds for matching and
+ # non-matching searches should be distinct. This shows that the two
+ # cases are distinguishable, and therefore that confidential attributes
+ # are visible.
+ admin_matching, admin_non_matching = time_searches(self.ldb_admin)
+ assertRangesDistinct(admin_matching, admin_non_matching)
+
+ # The user cannot view the confidential attribute, so the uncertainty
+ # bounds for matching and non-matching searches must overlap. The two
+ # cases must be indistinguishable.
+ user_matching, user_non_matching = time_searches(self.ldb_user)
+ assertRangesOverlap(user_matching, user_non_matching)
+
+# Check that using the dirsync controls doesn't reveal confidential
+# "RODC filtered attribute" values to users with only
+# GUID_DRS_GET_CHANGES. The tests is so similar to the Confidential
+# attribute test we base it on that.
+class RodcFilteredAttrDirsync(ConfidentialAttrTestDirsync):
+
+ def setUp(self):
+ super().setUp()
+ self.dirsync = ["dirsync:1:0:1000"]
+
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user))
+ mod = "(OA;;CR;%s;;%s)" % (security.GUID_DRS_GET_CHANGES,
+ str(user_sid))
+ self.sd_utils.dacl_add_ace(self.base_dn, mod)
+
+ self.ldb_user = self.get_ldb_connection(self.user, self.user_pass)
+
+ self.addCleanup(self.sd_utils.dacl_delete_aces, self.base_dn, mod)
+
+ def make_attr_confidential(self):
+ """Marks the attribute under test as being confidential AND RODC
+ filtered (which should mean it is not visible with only
+ GUID_DRS_GET_CHANGES)
+ """
+
+ # work out the original 'searchFlags' value before we overwrite it
+ old_value = self.get_attr_search_flags(self.attr_dn)
+
+ self.set_attr_search_flags(self.attr_dn, str(SEARCH_FLAG_RODC_ATTRIBUTE|SEARCH_FLAG_CONFIDENTIAL))
+
+ # reset the value after the test completes
+ self.addCleanup(self.set_attr_search_flags, self.attr_dn, old_value)
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/deletetest.py b/source4/dsdb/tests/python/deletetest.py
new file mode 100755
index 0000000..7375f26
--- /dev/null
+++ b/source4/dsdb/tests/python/deletetest.py
@@ -0,0 +1,565 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_BASE, LdbError, Message, MessageElement, Dn, FLAG_MOD_ADD, FLAG_MOD_DELETE, FLAG_MOD_REPLACE
+from ldb import ERR_NO_SUCH_OBJECT, ERR_NOT_ALLOWED_ON_NON_LEAF, ERR_ENTRY_ALREADY_EXISTS, ERR_ATTRIBUTE_OR_VALUE_EXISTS
+from ldb import ERR_UNWILLING_TO_PERFORM, ERR_OPERATIONS_ERROR
+from samba.samdb import SamDB
+from samba.tests import delete_force
+from samba import dsdb
+from samba.common import get_string
+
+parser = optparse.OptionParser("deletetest.py [options] <host|file>")
+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)
+
+
+class BaseDeleteTests(samba.tests.TestCase):
+
+ def GUID_string(self, guid):
+ return get_string(self.ldb.schema_format_value("objectGUID", guid))
+
+ def setUp(self):
+ super(BaseDeleteTests, self).setUp()
+ self.ldb = SamDB(host, credentials=creds, session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.ldb.domain_dn()
+ self.configuration_dn = self.ldb.get_config_basedn().get_linearized()
+
+ def search_guid(self, guid):
+ print("SEARCH by GUID %s" % self.GUID_string(guid))
+
+ res = self.ldb.search(base="<GUID=%s>" % self.GUID_string(guid),
+ scope=SCOPE_BASE,
+ controls=["show_deleted:1"],
+ attrs=["*", "parentGUID"])
+ self.assertEqual(len(res), 1)
+ return res[0]
+
+ def search_dn(self, dn):
+ print("SEARCH by DN %s" % dn)
+
+ res = self.ldb.search(expression="(objectClass=*)",
+ base=dn,
+ scope=SCOPE_BASE,
+ controls=["show_deleted:1"],
+ attrs=["*", "parentGUID"])
+ self.assertEqual(len(res), 1)
+ return res[0]
+
+
+class BasicDeleteTests(BaseDeleteTests):
+
+ def setUp(self):
+ super(BasicDeleteTests, self).setUp()
+
+ def del_attr_values(self, delObj):
+ print("Checking attributes for %s" % delObj["dn"])
+
+ self.assertEqual(str(delObj["isDeleted"][0]), "TRUE")
+ self.assertTrue(not("objectCategory" in delObj))
+ self.assertTrue(not("sAMAccountType" in delObj))
+
+ def preserved_attributes_list(self, liveObj, delObj):
+ print("Checking for preserved attributes list")
+
+ preserved_list = ["nTSecurityDescriptor", "attributeID", "attributeSyntax", "dNReferenceUpdate", "dNSHostName",
+ "flatName", "governsID", "groupType", "instanceType", "lDAPDisplayName", "legacyExchangeDN",
+ "isDeleted", "isRecycled", "lastKnownParent", "msDS-LastKnownRDN", "mS-DS-CreatorSID",
+ "mSMQOwnerID", "nCName", "objectClass", "distinguishedName", "objectGUID", "objectSid",
+ "oMSyntax", "proxiedObjectName", "name", "replPropertyMetaData", "sAMAccountName",
+ "securityIdentifier", "sIDHistory", "subClassOf", "systemFlags", "trustPartner", "trustDirection",
+ "trustType", "trustAttributes", "userAccountControl", "uSNChanged", "uSNCreated", "whenCreated"]
+
+ for a in liveObj:
+ if a in preserved_list:
+ self.assertTrue(a in delObj)
+
+ def check_rdn(self, liveObj, delObj, rdnName):
+ print("Checking for correct rDN")
+ rdn = liveObj[rdnName][0]
+ rdn2 = delObj[rdnName][0]
+ name2 = delObj["name"][0]
+ dn_rdn = delObj.dn.get_rdn_value()
+ guid = liveObj["objectGUID"][0]
+ self.assertEqual(str(rdn2), ("%s\nDEL:%s" % (rdn, self.GUID_string(guid))))
+ self.assertEqual(str(name2), ("%s\nDEL:%s" % (rdn, self.GUID_string(guid))))
+ self.assertEqual(str(name2), dn_rdn)
+
+ def delete_deleted(self, ldb, dn):
+ print("Testing the deletion of the already deleted dn %s" % dn)
+
+ try:
+ ldb.delete(dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ def test_delete_protection(self):
+ """Delete protection tests"""
+
+ print(self.base_dn)
+
+ delete_force(self.ldb, "cn=entry1,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=entry2,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container"})
+ self.ldb.add({
+ "dn": "cn=entry1,cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container"})
+ self.ldb.add({
+ "dn": "cn=entry2,cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container"})
+
+ try:
+ self.ldb.delete("cn=ldaptestcontainer," + self.base_dn)
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+
+ self.ldb.delete("cn=ldaptestcontainer," + self.base_dn, ["tree_delete:1"])
+
+ try:
+ res = self.ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE, attrs=[])
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ try:
+ res = self.ldb.search("cn=entry1,cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE, attrs=[])
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ try:
+ res = self.ldb.search("cn=entry2,cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE, attrs=[])
+ self.fail()
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ delete_force(self.ldb, "cn=entry1,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=entry2,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ # Performs some protected object delete testing
+
+ res = self.ldb.search(base="", expression="", scope=SCOPE_BASE,
+ attrs=["dsServiceName", "dNSHostName"])
+ self.assertEqual(len(res), 1)
+
+ # Delete failing since DC's nTDSDSA object is protected
+ try:
+ self.ldb.delete(res[0]["dsServiceName"][0])
+ self.fail()
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ res = self.ldb.search(self.base_dn, attrs=["rIDSetReferences"],
+ expression="(&(objectClass=computer)(dNSHostName=" + str(res[0]["dNSHostName"][0]) + "))")
+ self.assertEqual(len(res), 1)
+
+ # Deletes failing since DC's rIDSet object is protected
+ try:
+ self.ldb.delete(res[0]["rIDSetReferences"][0])
+ self.fail()
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ try:
+ self.ldb.delete(res[0]["rIDSetReferences"][0], ["tree_delete:1"])
+ self.fail()
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Deletes failing since three main crossRef objects are protected
+
+ try:
+ self.ldb.delete("cn=Enterprise Schema,cn=Partitions," + self.configuration_dn)
+ self.fail()
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ try:
+ self.ldb.delete("cn=Enterprise Schema,cn=Partitions," + self.configuration_dn, ["tree_delete:1"])
+ self.fail()
+ except LdbError as e9:
+ (num, _) = e9.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb.delete("cn=Enterprise Configuration,cn=Partitions," + self.configuration_dn)
+ self.fail()
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+ try:
+ self.ldb.delete("cn=Enterprise Configuration,cn=Partitions," + self.configuration_dn, ["tree_delete:1"])
+ self.fail()
+ except LdbError as e11:
+ (num, _) = e11.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+
+ res = self.ldb.search("cn=Partitions," + self.configuration_dn, attrs=[],
+ expression="(nCName=%s)" % self.base_dn)
+ self.assertEqual(len(res), 1)
+
+ try:
+ self.ldb.delete(res[0].dn)
+ self.fail()
+ except LdbError as e12:
+ (num, _) = e12.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+ try:
+ self.ldb.delete(res[0].dn, ["tree_delete:1"])
+ self.fail()
+ except LdbError as e13:
+ (num, _) = e13.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+
+ # Delete failing since "SYSTEM_FLAG_DISALLOW_DELETE"
+ try:
+ self.ldb.delete("CN=Users," + self.base_dn)
+ self.fail()
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Tree-delete failing since "isCriticalSystemObject"
+ try:
+ self.ldb.delete("CN=Computers," + self.base_dn, ["tree_delete:1"])
+ self.fail()
+ except LdbError as e15:
+ (num, _) = e15.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+
+class BasicTreeDeleteTests(BasicDeleteTests):
+
+ def setUp(self):
+ super(BasicTreeDeleteTests, self).setUp()
+
+ # user current time in ms to make unique objects
+ import time
+ marker = str(int(round(time.time() * 1000)))
+ usr1_name = "u_" + marker
+ usr2_name = "u2_" + marker
+ grp_name = "g1_" + marker
+ site_name = "s1_" + marker
+
+ self.usr1 = "cn=%s,cn=users,%s" % (usr1_name, self.base_dn)
+ self.usr2 = "cn=%s,cn=users,%s" % (usr2_name, self.base_dn)
+ self.grp1 = "cn=%s,cn=users,%s" % (grp_name, self.base_dn)
+ self.sit1 = "cn=%s,cn=sites,%s" % (site_name, self.configuration_dn)
+ self.ss1 = "cn=NTDS Site Settings,cn=%s,cn=sites,%s" % (site_name, self.configuration_dn)
+ self.srv1 = "cn=Servers,cn=%s,cn=sites,%s" % (site_name, self.configuration_dn)
+ self.srv2 = "cn=TESTSRV,cn=Servers,cn=%s,cn=sites,%s" % (site_name, self.configuration_dn)
+
+ delete_force(self.ldb, self.usr1)
+ delete_force(self.ldb, self.usr2)
+ delete_force(self.ldb, self.grp1)
+ delete_force(self.ldb, self.ss1)
+ delete_force(self.ldb, self.srv2)
+ delete_force(self.ldb, self.srv1)
+ delete_force(self.ldb, self.sit1)
+
+ self.ldb.add({
+ "dn": self.usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": usr1_name})
+
+ self.ldb.add({
+ "dn": self.usr2,
+ "objectclass": "user",
+ "description": "test user 2 description",
+ "samaccountname": usr2_name})
+
+ self.ldb.add({
+ "dn": self.grp1,
+ "objectclass": "group",
+ "description": "test group",
+ "samaccountname": grp_name,
+ "member": [self.usr1, self.usr2],
+ "isDeleted": "FALSE"})
+
+ self.ldb.add({
+ "dn": self.sit1,
+ "objectclass": "site"})
+
+ self.ldb.add({
+ "dn": self.ss1,
+ "objectclass": ["applicationSiteSettings", "nTDSSiteSettings"]})
+
+ self.ldb.add({
+ "dn": self.srv1,
+ "objectclass": "serversContainer"})
+
+ self.ldb.add({
+ "dn": self.srv2,
+ "objectClass": "server"})
+
+ self.objLive1 = self.search_dn(self.usr1)
+ self.guid1 = self.objLive1["objectGUID"][0]
+
+ self.objLive2 = self.search_dn(self.usr2)
+ self.guid2 = self.objLive2["objectGUID"][0]
+
+ self.objLive3 = self.search_dn(self.grp1)
+ self.guid3 = self.objLive3["objectGUID"][0]
+
+ self.objLive4 = self.search_dn(self.sit1)
+ self.guid4 = self.objLive4["objectGUID"][0]
+
+ self.objLive5 = self.search_dn(self.ss1)
+ self.guid5 = self.objLive5["objectGUID"][0]
+
+ self.objLive6 = self.search_dn(self.srv1)
+ self.guid6 = self.objLive6["objectGUID"][0]
+
+ self.objLive7 = self.search_dn(self.srv2)
+ self.guid7 = self.objLive7["objectGUID"][0]
+
+ self.deleted_objects_config_dn \
+ = self.ldb.get_wellknown_dn(self.ldb.get_config_basedn(),
+ dsdb.DS_GUID_DELETED_OBJECTS_CONTAINER)
+ deleted_objects_config_obj \
+ = self.search_dn(self.deleted_objects_config_dn)
+
+ self.deleted_objects_config_guid \
+ = deleted_objects_config_obj["objectGUID"][0]
+
+ self.deleted_objects_domain_dn \
+ = self.ldb.get_wellknown_dn(self.ldb.get_default_basedn(),
+ dsdb.DS_GUID_DELETED_OBJECTS_CONTAINER)
+ deleted_objects_domain_obj \
+ = self.search_dn(self.deleted_objects_domain_dn)
+
+ self.deleted_objects_domain_guid \
+ = deleted_objects_domain_obj["objectGUID"][0]
+
+ self.deleted_objects_domain_dn \
+ = self.ldb.get_wellknown_dn(self.ldb.get_default_basedn(),
+ dsdb.DS_GUID_DELETED_OBJECTS_CONTAINER)
+ sites_obj = self.search_dn("cn=sites,%s"
+ % self.ldb.get_config_basedn())
+ self.sites_dn = sites_obj.dn
+ self.sites_guid \
+ = sites_obj["objectGUID"][0]
+
+ def test_all(self):
+ """Basic delete tests"""
+
+ self.ldb.delete(self.usr1)
+ self.ldb.delete(self.usr2)
+ self.ldb.delete(self.grp1)
+ self.ldb.delete(self.srv1, ["tree_delete:1"])
+ self.ldb.delete(self.sit1, ["tree_delete:1"])
+
+ self.check_all()
+
+ def test_tree_delete(self):
+ """Basic delete tests,
+ but use just one tree delete for the config records
+ """
+
+ self.ldb.delete(self.usr1)
+ self.ldb.delete(self.usr2)
+ self.ldb.delete(self.grp1)
+ self.ldb.delete(self.sit1, ["tree_delete:1"])
+
+ self.check_all()
+
+ def check_all(self):
+ objDeleted1 = self.search_guid(self.guid1)
+ objDeleted2 = self.search_guid(self.guid2)
+ objDeleted3 = self.search_guid(self.guid3)
+ objDeleted4 = self.search_guid(self.guid4)
+ objDeleted5 = self.search_guid(self.guid5)
+ objDeleted6 = self.search_guid(self.guid6)
+ objDeleted7 = self.search_guid(self.guid7)
+
+ self.del_attr_values(objDeleted1)
+ self.del_attr_values(objDeleted2)
+ self.del_attr_values(objDeleted3)
+ self.del_attr_values(objDeleted4)
+ self.del_attr_values(objDeleted5)
+ self.del_attr_values(objDeleted6)
+ self.del_attr_values(objDeleted7)
+
+ self.preserved_attributes_list(self.objLive1, objDeleted1)
+ self.preserved_attributes_list(self.objLive2, objDeleted2)
+ self.preserved_attributes_list(self.objLive3, objDeleted3)
+ self.preserved_attributes_list(self.objLive4, objDeleted4)
+ self.preserved_attributes_list(self.objLive5, objDeleted5)
+ self.preserved_attributes_list(self.objLive6, objDeleted6)
+ self.preserved_attributes_list(self.objLive7, objDeleted7)
+
+ self.check_rdn(self.objLive1, objDeleted1, "cn")
+ self.check_rdn(self.objLive2, objDeleted2, "cn")
+ self.check_rdn(self.objLive3, objDeleted3, "cn")
+ self.check_rdn(self.objLive4, objDeleted4, "cn")
+ self.check_rdn(self.objLive5, objDeleted5, "cn")
+ self.check_rdn(self.objLive6, objDeleted6, "cn")
+ self.check_rdn(self.objLive7, objDeleted7, "cn")
+
+ self.delete_deleted(self.ldb, self.usr1)
+ self.delete_deleted(self.ldb, self.usr2)
+ self.delete_deleted(self.ldb, self.grp1)
+ self.delete_deleted(self.ldb, self.sit1)
+ self.delete_deleted(self.ldb, self.ss1)
+ self.delete_deleted(self.ldb, self.srv1)
+ self.delete_deleted(self.ldb, self.srv2)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted1.dn))
+ self.assertEqual(objDeleted1.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted1["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted2.dn))
+ self.assertEqual(objDeleted2.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted2["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted3.dn))
+ self.assertEqual(objDeleted3.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted3["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted4.dn))
+ self.assertEqual(objDeleted4.dn.parent(),
+ self.sites_dn)
+ self.assertEqual(objDeleted4["parentGUID"][0],
+ self.sites_guid)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted5.dn))
+ self.assertEqual(objDeleted5.dn.parent(),
+ self.deleted_objects_config_dn)
+ self.assertEqual(objDeleted5["parentGUID"][0],
+ self.deleted_objects_config_guid)
+
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted6.dn))
+ self.assertEqual(objDeleted6.dn.parent(),
+ objDeleted4.dn)
+ self.assertEqual(objDeleted6["parentGUID"][0],
+ objDeleted4["objectGUID"][0])
+
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted7.dn))
+ self.assertEqual(objDeleted7.dn.parent(),
+ objDeleted6.dn)
+ self.assertEqual(objDeleted7["parentGUID"][0],
+ objDeleted6["objectGUID"][0])
+
+ objDeleted1 = self.search_guid(self.guid1)
+ objDeleted2 = self.search_guid(self.guid2)
+ objDeleted3 = self.search_guid(self.guid3)
+ objDeleted4 = self.search_guid(self.guid4)
+ objDeleted5 = self.search_guid(self.guid5)
+ objDeleted6 = self.search_guid(self.guid6)
+ objDeleted7 = self.search_guid(self.guid7)
+
+ self.del_attr_values(objDeleted1)
+ self.del_attr_values(objDeleted2)
+ self.del_attr_values(objDeleted3)
+ self.del_attr_values(objDeleted4)
+ self.del_attr_values(objDeleted5)
+ self.del_attr_values(objDeleted6)
+ self.del_attr_values(objDeleted7)
+
+ self.preserved_attributes_list(self.objLive1, objDeleted1)
+ self.preserved_attributes_list(self.objLive2, objDeleted2)
+ self.preserved_attributes_list(self.objLive3, objDeleted3)
+ self.preserved_attributes_list(self.objLive4, objDeleted4)
+ self.preserved_attributes_list(self.objLive5, objDeleted5)
+ self.preserved_attributes_list(self.objLive6, objDeleted6)
+ self.preserved_attributes_list(self.objLive7, objDeleted7)
+
+ self.check_rdn(self.objLive1, objDeleted1, "cn")
+ self.check_rdn(self.objLive2, objDeleted2, "cn")
+ self.check_rdn(self.objLive3, objDeleted3, "cn")
+ self.check_rdn(self.objLive4, objDeleted4, "cn")
+ self.check_rdn(self.objLive5, objDeleted5, "cn")
+ self.check_rdn(self.objLive6, objDeleted6, "cn")
+ self.check_rdn(self.objLive7, objDeleted7, "cn")
+
+ self.delete_deleted(self.ldb, self.usr1)
+ self.delete_deleted(self.ldb, self.usr2)
+ self.delete_deleted(self.ldb, self.grp1)
+ self.delete_deleted(self.ldb, self.sit1)
+ self.delete_deleted(self.ldb, self.ss1)
+ self.delete_deleted(self.ldb, self.srv1)
+ self.delete_deleted(self.ldb, self.srv2)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted1.dn))
+ self.assertEqual(objDeleted1.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted1["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted2.dn))
+ self.assertEqual(objDeleted2.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted2["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted3.dn))
+ self.assertEqual(objDeleted3.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted3["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted4.dn))
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted5.dn))
+ self.assertEqual(objDeleted5.dn.parent(),
+ self.deleted_objects_config_dn)
+ self.assertEqual(objDeleted5["parentGUID"][0],
+ self.deleted_objects_config_guid)
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted6.dn))
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted7.dn))
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/dirsync.py b/source4/dsdb/tests/python/dirsync.py
new file mode 100755
index 0000000..a0691f0
--- /dev/null
+++ b/source4/dsdb/tests/python/dirsync.py
@@ -0,0 +1,1107 @@
+#!/usr/bin/env python3
+#
+# Unit tests for dirsync control
+# Copyright (C) Matthieu Patou <mat@matws.net> 2011
+# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2014
+# Copyright (C) Catalyst.Net Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+import samba.getopt as options
+import base64
+
+import ldb
+from ldb import LdbError, SCOPE_BASE
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_DELETE, FLAG_MOD_REPLACE
+from samba.dsdb import SEARCH_FLAG_CONFIDENTIAL, SEARCH_FLAG_RODC_ATTRIBUTE
+from samba.dcerpc import security, misc, drsblobs
+from samba.ndr import ndr_unpack, ndr_pack
+
+from samba.auth import system_session
+from samba import gensec, sd_utils
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+import samba.tests
+from samba.tests import delete_force
+
+parser = optparse.OptionParser("dirsync.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.pop()
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+#
+# Tests start here
+#
+
+
+class DirsyncBaseTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.ldb_admin = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.domain_sid = security.dom_sid(self.ldb_admin.get_domain_sid())
+ self.user_pass = samba.generate_random_password(12, 16)
+ self.configuration_dn = self.ldb_admin.get_config_basedn().get_linearized()
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+ # used for anonymous login
+ print("baseDN: %s" % self.base_dn)
+
+ userou = "OU=dirsync-test"
+ self.ou = f"{userou},{self.base_dn}"
+ samba.tests.delete_force(self.ldb_admin, self.ou, controls=['tree_delete:1'])
+ self.ldb_admin.create_ou(self.ou)
+ self.addCleanup(samba.tests.delete_force, self.ldb_admin, self.ou, controls=['tree_delete:1'])
+
+ # Regular user
+ self.dirsync_user = "test_dirsync_user"
+ self.simple_user = "test_simple_user"
+ self.admin_user = "test_admin_user"
+ self.dirsync_pass = self.user_pass
+ self.simple_pass = self.user_pass
+ self.admin_pass = self.user_pass
+
+ self.ldb_admin.newuser(self.dirsync_user, self.dirsync_pass, userou=userou)
+ self.ldb_admin.newuser(self.simple_user, self.simple_pass, userou=userou)
+ self.ldb_admin.newuser(self.admin_user, self.admin_pass, userou=userou)
+ self.desc_sddl = self.sd_utils.get_sd_as_sddl(self.base_dn)
+
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.dirsync_user))
+ mod = "(OA;;CR;%s;;%s)" % (security.GUID_DRS_GET_CHANGES,
+ str(user_sid))
+ self.sd_utils.dacl_add_ace(self.base_dn, mod)
+ self.addCleanup(self.sd_utils.dacl_delete_aces, self.base_dn, mod)
+
+ # add admins to the Domain Admins group
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.admin_user],
+ add_members_operation=True)
+
+ def get_user_dn(self, name):
+ return ldb.Dn(self.ldb_admin, "CN={0},{1}".format(name, self.ou))
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+# tests on ldap add operations
+class SimpleDirsyncTests(DirsyncBaseTests):
+
+ # def test_dirsync_errors(self):
+
+ def test_dirsync_supported(self):
+ """Test the basic of the dirsync is supported"""
+ self.ldb_dirsync = self.get_ldb_connection(self.dirsync_user, self.user_pass)
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_admin.search(self.base_dn, expression="samaccountname=*", controls=["dirsync:1:0:1"])
+ res = self.ldb_dirsync.search(self.base_dn, expression="samaccountname=*", controls=["dirsync:1:0:1"])
+ try:
+ self.ldb_simple.search(self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ def test_parentGUID_referrals(self):
+ res2 = self.ldb_admin.search(self.base_dn, scope=SCOPE_BASE, attrs=["objectGUID"])
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="name=Configuration",
+ controls=["dirsync:1:0:1"])
+ self.assertEqual(res2[0].get("objectGUID"), res[0].get("parentGUID"))
+
+ def test_ok_not_rootdc(self):
+ """Test if it's ok to do dirsync on another NC that is not the root DC"""
+ self.ldb_admin.search(self.ldb_admin.get_config_basedn(),
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+
+ def test_dirsync_errors(self):
+ """Test if dirsync returns the correct LDAP errors in case of pb"""
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self.ldb_dirsync = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ try:
+ self.ldb_simple.search(self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ try:
+ self.ldb_simple.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ try:
+ self.ldb_simple.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:1:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_UNWILLING_TO_PERFORM") != -1)
+
+ try:
+ self.ldb_dirsync.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ try:
+ self.ldb_admin.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ try:
+ self.ldb_admin.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:1:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_UNWILLING_TO_PERFORM") != -1)
+
+ def test_dirsync_attributes(self):
+ """Check behavior with some attributes """
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ # Check that nTSecurityDescriptor is returned as it's the case when doing dirsync
+ self.assertTrue(res.msgs[0].get("ntsecuritydescriptor") is not None)
+ # Check that non replicated attributes are not returned
+ self.assertTrue(res.msgs[0].get("badPwdCount") is None)
+ # Check that non forward link are not returned
+ self.assertTrue(res.msgs[0].get("memberof") is None)
+
+ # Asking for instanceType will return also objectGUID
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["instanceType"],
+ controls=["dirsync:1:0:1"])
+ self.assertTrue(res.msgs[0].get("objectGUID") is not None)
+ self.assertTrue(res.msgs[0].get("instanceType") is not None)
+
+ # We don't return an entry if asked for objectGUID
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["objectGUID"],
+ controls=["dirsync:1:0:1"])
+ self.assertEqual(len(res.msgs), 0)
+
+ # a request on the root of a NC didn't return parentGUID
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["name"],
+ controls=["dirsync:1:0:1"])
+ self.assertTrue(res.msgs[0].get("objectGUID") is not None)
+ self.assertTrue(res.msgs[0].get("name") is not None)
+ self.assertTrue(res.msgs[0].get("parentGUID") is None)
+ self.assertTrue(res.msgs[0].get("instanceType") is not None)
+
+ # Asking for name will return also objectGUID and parentGUID
+ # and instanceType and of course name
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["name"],
+ controls=["dirsync:1:0:1"])
+ self.assertTrue(res.msgs[0].get("objectGUID") is not None)
+ self.assertTrue(res.msgs[0].get("name") is not None)
+ self.assertTrue(res.msgs[0].get("parentGUID") is not None)
+ self.assertTrue(res.msgs[0].get("instanceType") is not None)
+
+ # Asking for dn will not return not only DN but more like if attrs=*
+ # parentGUID should be returned
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["dn"],
+ controls=["dirsync:1:0:1"])
+ count = len(res.msgs[0])
+ res2 = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ controls=["dirsync:1:0:1"])
+ count2 = len(res2.msgs[0])
+ self.assertEqual(count, count2)
+
+ # Asking for cn will return nothing on objects that have CN as RDN
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["cn"],
+ controls=["dirsync:1:0:1"])
+ self.assertEqual(len(res.msgs), 0)
+ # Asking for parentGUID will return nothing too
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["parentGUID"],
+ controls=["dirsync:1:0:1"])
+ self.assertEqual(len(res.msgs), 0)
+ ouname = "OU=testou,%s" % self.ou
+ self.ouname = ouname
+ self.ldb_admin.create_ou(ouname)
+ delta = Message()
+ delta.dn = Dn(self.ldb_admin, ouname)
+ delta["cn"] = MessageElement("test ou",
+ FLAG_MOD_ADD,
+ "cn")
+ self.ldb_admin.modify(delta)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="name=testou",
+ attrs=["cn"],
+ controls=["dirsync:1:0:1"])
+
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 3)
+ delete_force(self.ldb_admin, ouname)
+
+ def test_dirsync_with_controls(self):
+ """Check that dirsync return correct information when dealing with the NC"""
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["name"],
+ controls=["dirsync:1:0:10000", "extended_dn:1", "show_deleted:1"])
+
+ def test_dirsync_basenc(self):
+ """Check that dirsync return correct information when dealing with the NC"""
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["name"],
+ controls=["dirsync:1:0:10000"])
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 3)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["ntSecurityDescriptor"],
+ controls=["dirsync:1:0:10000"])
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 3)
+
+ def test_dirsync_othernc(self):
+ """Check that dirsync return information for entries that are normally referrals (ie. other NCs)"""
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=configuration)",
+ attrs=["name"],
+ controls=["dirsync:1:0:10000"])
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 4)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=configuration)",
+ attrs=["ntSecurityDescriptor"],
+ controls=["dirsync:1:0:10000"])
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 3)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=domaindns)",
+ attrs=["ntSecurityDescriptor"],
+ controls=["dirsync:1:0:10000"])
+ nb = len(res.msgs)
+
+ # only sub nc returns a result when asked for objectGUID
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=domaindns)",
+ attrs=["objectGUID"],
+ controls=["dirsync:1:0:0"])
+ self.assertEqual(len(res.msgs), nb - 1)
+ if nb > 1:
+ self.assertTrue(res.msgs[0].get("objectGUID") is not None)
+ else:
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=configuration)",
+ attrs=["objectGUID"],
+ controls=["dirsync:1:0:0"])
+
+ def test_dirsync_send_delta(self):
+ """Check that dirsync return correct delta when sending the last cookie"""
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(samaccountname=test*)(!(isDeleted=*)))",
+ controls=["dirsync:1:0:10000"])
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control = str(":".join(ctl))
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(samaccountname=test*)(!(isDeleted=*)))",
+ controls=[control])
+ self.assertEqual(len(res), 0)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=["dirsync:1:0:100000"])
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control2 = str(":".join(ctl))
+
+ # Let's create an OU
+ ouname = "OU=testou2,%s" % self.base_dn
+ self.ouname = ouname
+ self.ldb_admin.create_ou(ouname)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=[control2])
+ self.assertEqual(len(res), 1)
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control3 = str(":".join(ctl))
+
+ delta = Message()
+ delta.dn = Dn(self.ldb_admin, str(ouname))
+
+ delta["cn"] = MessageElement("test ou",
+ FLAG_MOD_ADD,
+ "cn")
+ self.ldb_admin.modify(delta)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=[control3])
+
+ self.assertEqual(len(res.msgs), 1)
+ # 3 attributes: instanceType, cn and objectGUID
+ self.assertEqual(len(res.msgs[0]), 3)
+
+ delta = Message()
+ delta.dn = Dn(self.ldb_admin, str(ouname))
+ delta["cn"] = MessageElement([],
+ FLAG_MOD_DELETE,
+ "cn")
+ self.ldb_admin.modify(delta)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=[control3])
+
+ self.assertEqual(len(res.msgs), 1)
+ # So we won't have much attribute returned but instanceType and GUID
+ # are.
+ # 3 attributes: instanceType and objectGUID and cn but empty
+ self.assertEqual(len(res.msgs[0]), 3)
+ ouname = "OU=newouname,%s" % self.base_dn
+ self.ldb_admin.rename(str(res[0].dn), str(Dn(self.ldb_admin, ouname)))
+ self.ouname = ouname
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control4 = str(":".join(ctl))
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=[control3])
+
+ self.assertTrue(res[0].get("parentGUID") is not None)
+ self.assertTrue(res[0].get("name") is not None)
+ delete_force(self.ldb_admin, ouname)
+
+ def test_dirsync_linkedattributes_OBJECT_SECURITY(self):
+ """Check that dirsync returned deleted objects too"""
+ # Let's search for members
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=["dirsync:1:1:1"])
+
+ self.assertTrue(len(res[0].get("member")) > 0)
+ size = len(res[0].get("member"))
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "1"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+ self.ldb_admin.add_remove_group_members("Administrators", [self.simple_user],
+ add_members_operation=True)
+
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(len(res[0].get("member")), size + 1)
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "1"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ # remove the user from the group
+ self.ldb_admin.add_remove_group_members("Administrators", [self.simple_user],
+ add_members_operation=False)
+
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(len(res[0].get("member")), size)
+
+ self.ldb_admin.newgroup("testgroup")
+ self.addCleanup(self.ldb_admin.deletegroup, "testgroup")
+ self.ldb_admin.add_remove_group_members("testgroup", [self.simple_user],
+ add_members_operation=True)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=testgroup)",
+ controls=["dirsync:1:0:1"])
+
+ self.assertEqual(len(res[0].get("member")), 1)
+ self.assertTrue(res[0].get("member") != "")
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "1"
+ control1 = str(":".join(ctl))
+
+ # Check that reasking the same question but with an updated cookie
+ # didn't return any results.
+ print(control1)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=testgroup)",
+ controls=[control1])
+ self.assertEqual(len(res), 0)
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "1"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ self.ldb_admin.add_remove_group_members("testgroup", [self.simple_user],
+ add_members_operation=False)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=testgroup)",
+ attrs=["member"],
+ controls=[control1])
+
+ self.assertEqual(len(res[0].get("member")), 0)
+
+ def test_dirsync_deleted_items(self):
+ """Check that dirsync returned deleted objects too"""
+ # Let's create an OU
+ ouname = "OU=testou3,%s" % self.base_dn
+ self.ouname = ouname
+ self.ldb_admin.create_ou(ouname)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=["dirsync:1:0:1"])
+ guid = None
+ for e in res:
+ if str(e["name"]) == "testou3":
+ guid = str(ndr_unpack(misc.GUID, e.get("objectGUID")[0]))
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ # So now delete the object and check that
+ # we can see the object but deleted when admin
+ delete_force(self.ldb_admin, ouname)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectClass=organizationalUnit)",
+ controls=[control1])
+ self.assertEqual(len(res), 1)
+ guid2 = str(ndr_unpack(misc.GUID, res[0].get("objectGUID")[0]))
+ self.assertEqual(guid2, guid)
+ self.assertTrue(res[0].get("isDeleted"))
+ self.assertTrue(res[0].get("name") is not None)
+
+ def test_cookie_from_others(self):
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=["dirsync:1:0:1"])
+ ctl = str(res.controls[0]).split(":")
+ cookie = ndr_unpack(drsblobs.ldapControlDirSyncCookie, base64.b64decode(str(ctl[4])))
+ cookie.blob.guid1 = misc.GUID("128a99bf-abcd-1234-abcd-1fb625e530db")
+ controls = ["dirsync:1:0:0:%s" % base64.b64encode(ndr_pack(cookie)).decode('utf8')]
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=controls)
+
+ def test_dirsync_linkedattributes_range(self):
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_admin.search(self.base_dn,
+ attrs=["member;range=1-1"],
+ expression="(name=Administrators)",
+ controls=["dirsync:1:0:0"])
+
+ self.assertTrue(len(res) > 0)
+ self.assertTrue(res[0].get("member;range=1-1") is None)
+ self.assertTrue(res[0].get("member") is not None)
+ self.assertTrue(len(res[0].get("member")) > 0)
+
+ def test_dirsync_linkedattributes_range_user(self):
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ try:
+ res = self.ldb_simple.search(self.base_dn,
+ attrs=["member;range=1-1"],
+ expression="(name=Administrators)",
+ controls=["dirsync:1:0:0"])
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_dirsync_linkedattributes(self):
+ flag_incr_linked = 2147483648
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_admin.search(self.base_dn,
+ attrs=["member"],
+ expression="(name=Administrators)",
+ controls=["dirsync:1:%d:1" % flag_incr_linked])
+
+ self.assertTrue(res[0].get("member;range=1-1") is not None)
+ self.assertTrue(len(res[0].get("member;range=1-1")) > 0)
+ size = len(res[0].get("member;range=1-1"))
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "%d" % flag_incr_linked
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+ self.ldb_admin.add_remove_group_members("Administrators", [self.simple_user],
+ add_members_operation=True)
+ self.ldb_admin.add_remove_group_members("Administrators", [self.dirsync_user],
+ add_members_operation=True)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(len(res[0].get("member;range=1-1")), 2)
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "%d" % flag_incr_linked
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ # remove the user from the group
+ self.ldb_admin.add_remove_group_members("Administrators", [self.simple_user],
+ add_members_operation=False)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(res[0].get("member;range=1-1"), None)
+ self.assertEqual(len(res[0].get("member;range=0-0")), 1)
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "%d" % flag_incr_linked
+ ctl[3] = "10000"
+ control2 = str(":".join(ctl))
+
+ self.ldb_admin.add_remove_group_members("Administrators", [self.dirsync_user],
+ add_members_operation=False)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control2])
+
+ self.assertEqual(res[0].get("member;range=1-1"), None)
+ self.assertEqual(len(res[0].get("member;range=0-0")), 1)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(res[0].get("member;range=1-1"), None)
+ self.assertEqual(len(res[0].get("member;range=0-0")), 2)
+
+ def test_dirsync_extended_dn(self):
+ """Check that dirsync works together with the extended_dn control"""
+ # Let's search for members
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=["dirsync:1:1:1"])
+
+ self.assertTrue(len(res[0].get("member")) > 0)
+ size = len(res[0].get("member"))
+
+ resEX1 = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=["dirsync:1:1:1","extended_dn:1:1"])
+ self.assertTrue(len(resEX1[0].get("member")) > 0)
+ sizeEX1 = len(resEX1[0].get("member"))
+ self.assertEqual(sizeEX1, size)
+ self.assertIn(res[0]["member"][0], resEX1[0]["member"][0])
+ self.assertIn(b"<GUID=", resEX1[0]["member"][0])
+ self.assertIn(b">;<SID=S-1-5-21-", resEX1[0]["member"][0])
+
+ resEX0 = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=["dirsync:1:1:1","extended_dn:1:0"])
+ self.assertTrue(len(resEX0[0].get("member")) > 0)
+ sizeEX0 = len(resEX0[0].get("member"))
+ self.assertEqual(sizeEX0, size)
+ self.assertIn(res[0]["member"][0], resEX0[0]["member"][0])
+ self.assertIn(b"<GUID=", resEX0[0]["member"][0])
+ self.assertIn(b">;<SID=010500000000000515", resEX0[0]["member"][0])
+
+ def test_dirsync_deleted_items_OBJECT_SECURITY(self):
+ """Check that dirsync returned deleted objects too"""
+ # Let's create an OU
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ ouname = "OU=testou3,%s" % self.base_dn
+ self.ouname = ouname
+ self.ldb_admin.create_ou(ouname)
+
+ # Specify LDAP_DIRSYNC_OBJECT_SECURITY
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=["dirsync:1:1:1"])
+
+ guid = None
+ for e in res:
+ if str(e["name"]) == "testou3":
+ guid = str(ndr_unpack(misc.GUID, e.get("objectGUID")[0]))
+
+ self.assertTrue(guid is not None)
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "1"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ # So now delete the object and check that
+ # we can see the object but deleted when admin
+ # we just see the objectGUID when simple user
+ delete_force(self.ldb_admin, ouname)
+
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(objectClass=organizationalUnit)",
+ controls=[control1])
+ self.assertEqual(len(res), 1)
+ guid2 = str(ndr_unpack(misc.GUID, res[0].get("objectGUID")[0]))
+ self.assertEqual(guid2, guid)
+ self.assertEqual(str(res[0].dn), "")
+
+class SpecialDirsyncTests(DirsyncBaseTests):
+
+ def setUp(self):
+ super().setUp()
+
+ self.schema_dn = self.ldb_admin.get_schema_basedn()
+
+ # the tests work by setting the 'Confidential' or 'RODC Filtered' bit in the searchFlags
+ # for an existing schema attribute. This only works against Windows if
+ # the systemFlags does not have FLAG_SCHEMA_BASE_OBJECT set for the
+ # schema attribute being modified. There are only a few attributes that
+ # meet this criteria (most of which only apply to 'user' objects)
+ self.conf_attr = "homePostalAddress"
+ attr_cn = "CN=Address-Home"
+ # schemaIdGuid for homePostalAddress (used for ACE tests)
+ self.attr_dn = f"{attr_cn},{self.schema_dn}"
+
+ userou = "OU=conf-attr-test"
+ self.ou = "{0},{1}".format(userou, self.base_dn)
+ samba.tests.delete_force(self.ldb_admin, self.ou, controls=['tree_delete:1'])
+ self.ldb_admin.create_ou(self.ou)
+ self.addCleanup(samba.tests.delete_force, self.ldb_admin, self.ou, controls=['tree_delete:1'])
+
+ # add a test object with this attribute set
+ self.conf_value = "abcdef"
+ self.conf_user = "conf-user"
+ self.ldb_admin.newuser(self.conf_user, self.user_pass, userou=userou)
+ self.conf_dn = self.get_user_dn(self.conf_user)
+ self.add_attr(self.conf_dn, self.conf_attr, self.conf_value)
+
+ # sanity-check the flag is not already set (this'll cause problems if
+ # previous test run didn't clean up properly)
+
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+ if search_flags & SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE:
+ self.set_attr_search_flags(self.attr_dn, str(search_flags &~ (SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE)))
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+ self.assertEqual(0, search_flags & (SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE),
+ f"{self.conf_attr} searchFlags did not reset to omit SEARCH_FLAG_CONFIDENTIAL and SEARCH_FLAG_RODC_ATTRIBUTE ({search_flags})")
+
+ # work out the original 'searchFlags' value before we overwrite it
+ old_value = self.get_attr_search_flags(self.attr_dn)
+
+ self.set_attr_search_flags(self.attr_dn, str(self.flag_under_test))
+
+ # reset the value after the test completes
+ self.addCleanup(self.set_attr_search_flags, self.attr_dn, old_value)
+
+ def add_attr(self, dn, attr, value):
+ m = Message()
+ m.dn = dn
+ m[attr] = MessageElement(value, FLAG_MOD_ADD, attr)
+ self.ldb_admin.modify(m)
+
+ def set_attr_search_flags(self, attr_dn, flags):
+ """Modifies the searchFlags for an object in the schema"""
+ m = Message()
+ m.dn = Dn(self.ldb_admin, attr_dn)
+ m['searchFlags'] = MessageElement(flags, FLAG_MOD_REPLACE,
+ 'searchFlags')
+ self.ldb_admin.modify(m)
+
+ # note we have to update the schema for this change to take effect (on
+ # Windows, at least)
+ self.ldb_admin.set_schema_update_now()
+
+ def get_attr_search_flags(self, attr_dn):
+ res = self.ldb_admin.search(attr_dn, scope=SCOPE_BASE,
+ attrs=['searchFlags'])
+ return res[0]['searchFlags'][0]
+
+ def find_under_current_ou(self, res):
+ for msg in res:
+ if msg.dn == self.conf_dn:
+ return msg
+ self.fail(f"Failed to find object {self.conf_dn} in {len(res)} results")
+
+
+class ConfidentialDirsyncTests(SpecialDirsyncTests):
+
+ def setUp(self):
+ self.flag_under_test = SEARCH_FLAG_CONFIDENTIAL
+ super().setUp()
+
+ def test_unicodePwd_normal(self):
+ res = self.ldb_admin.search(self.base_dn,
+ attrs=["unicodePwd", "supplementalCredentials", "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})")
+
+ msg = res[0]
+
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get("unicodePwd") is None)
+ self.assertTrue(msg.get("supplementalCredentials") is None)
+
+ def _test_dirsync_unicodePwd(self, ldb_conn, control=None, insist_on_empty_element=False):
+ res = ldb_conn.search(self.base_dn,
+ attrs=["unicodePwd", "supplementalCredentials", "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=[control])
+
+ msg = self.find_under_current_ou(res)
+
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get("unicodePwd") is not None)
+ self.assertEqual(len(msg.get("unicodePwd")), 0)
+ self.assertTrue(msg.get("supplementalCredentials") is not None)
+ self.assertEqual(len(msg.get("supplementalCredentials")), 0)
+ else:
+ self.assertTrue(msg.get("unicodePwd") is None
+ or len(msg.get("unicodePwd")) == 0)
+ self.assertTrue(msg.get("supplementalCredentials") is None
+ or len(msg.get("supplementalCredentials")) == 0)
+
+ def test_dirsync_unicodePwd_OBJ_SEC(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:1:0")
+
+ def test_dirsync_unicodePwd_OBJ_SEC_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:1:0", insist_on_empty_element=True)
+
+ def test_dirsync_unicodePwd_with_GET_CHANGES_OBJ_SEC(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:1:0")
+
+ def test_dirsync_unicodePwd_with_GET_CHANGES_OBJ_SEC_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:1:0", insist_on_empty_element=True)
+
+ def test_dirsync_unicodePwd_with_GET_CHANGES(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:0:0")
+
+ def test_dirsync_unicodePwd_with_GET_CHANGES_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:0:0", insist_on_empty_element=True)
+
+ def test_normal(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})")
+
+ msg = res[0]
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr) is None)
+
+ def _test_dirsync_OBJECT_SECURITY(self, ldb_conn, insist_on_empty_element=False):
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:1:0"])
+
+ msg = self.find_under_current_ou(res)
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get(self.conf_attr) is not None)
+ self.assertEqual(len(msg.get(self.conf_attr)), 0)
+ else:
+ self.assertTrue(msg.get(self.conf_attr) is None
+ or len(msg.get(self.conf_attr)) == 0)
+
+ def test_dirsync_OBJECT_SECURITY(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_OBJECT_SECURITY_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn, insist_on_empty_element=True)
+
+ def test_dirsync_with_GET_CHANGES(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+
+ msg = self.find_under_current_ou(res)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr))
+ self.assertEqual(len(msg.get(self.conf_attr)), 1)
+
+ def test_dirsync_with_GET_CHANGES_OBJECT_SECURITY(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_with_GET_CHANGES_OBJECT_SECURITY_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn, insist_on_empty_element=True)
+
+class FilteredDirsyncTests(SpecialDirsyncTests):
+
+ def setUp(self):
+ self.flag_under_test = SEARCH_FLAG_RODC_ATTRIBUTE
+ super().setUp()
+
+ def test_attr(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})")
+
+ msg = res[0]
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr))
+ self.assertEqual(len(msg.get(self.conf_attr)), 1)
+
+ def _test_dirsync_OBJECT_SECURITY(self, ldb_conn):
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:1:0"])
+
+ msg = self.find_under_current_ou(res)
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr))
+ self.assertEqual(len(msg.get(self.conf_attr)), 1)
+
+ def test_dirsync_OBJECT_SECURITY(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_OBJECT_SECURITY_with_GET_CHANGES(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def _test_dirsync_with_GET_CHANGES(self, insist_on_empty_element=False):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ res = ldb_conn.search(self.base_dn,
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+
+ msg = self.find_under_current_ou(res)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get(self.conf_attr) is not None)
+ self.assertEqual(len(msg.get(self.conf_attr)), 0)
+ else:
+ self.assertTrue(msg.get(self.conf_attr) is None
+ or len(msg.get(self.conf_attr)) == 0)
+
+ def test_dirsync_with_GET_CHANGES(self):
+ self._test_dirsync_with_GET_CHANGES()
+
+ def test_dirsync_with_GET_CHANGES_insist_on_empty_element(self):
+ self._test_dirsync_with_GET_CHANGES(insist_on_empty_element=True)
+
+ def test_dirsync_with_GET_CHANGES_attr(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ try:
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+ self.fail("ldb.search() should have failed with LDAP_INSUFFICIENT_ACCESS_RIGHTS")
+ except ldb.LdbError as e:
+ (errno, errstr) = e.args
+ self.assertEqual(errno, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+class ConfidentialFilteredDirsyncTests(SpecialDirsyncTests):
+
+ def setUp(self):
+ self.flag_under_test = SEARCH_FLAG_RODC_ATTRIBUTE|SEARCH_FLAG_CONFIDENTIAL
+ super().setUp()
+
+ def test_attr(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = ldb_conn.search(self.base_dn,
+ attrs=["unicodePwd", "supplementalCredentials", "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})")
+
+ msg = res[0]
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr) is None)
+
+ def _test_dirsync_OBJECT_SECURITY(self, ldb_conn, insist_on_empty_element=False):
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:1:0"])
+
+ msg = self.find_under_current_ou(res)
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get(self.conf_attr) is not None)
+ self.assertEqual(len(msg.get(self.conf_attr)), 0)
+ else:
+ self.assertTrue(msg.get(self.conf_attr) is None
+ or len(msg.get(self.conf_attr)) == 0)
+
+ def test_dirsync_OBJECT_SECURITY(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_OBJECT_SECURITY_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn, insist_on_empty_element=True)
+
+ def test_dirsync_OBJECT_SECURITY_with_GET_CHANGES(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_OBJECT_SECURITY_with_GET_CHANGES_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn, insist_on_empty_element=True)
+
+ def _test_dirsync_with_GET_CHANGES(self, insist_on_empty_element=False):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ res = ldb_conn.search(self.base_dn,
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+
+ msg = self.find_under_current_ou(res)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get(self.conf_attr) is not None)
+ self.assertEqual(len(msg.get(self.conf_attr)), 0)
+ else:
+ self.assertTrue(msg.get(self.conf_attr) is None
+ or len(msg.get(self.conf_attr)) == 0)
+
+ def test_dirsync_with_GET_CHANGES(self):
+ self._test_dirsync_with_GET_CHANGES()
+
+ def test_dirsync_with_GET_CHANGES_insist_on_empty_element(self):
+ self._test_dirsync_with_GET_CHANGES(insist_on_empty_element=True)
+
+ def test_dirsync_with_GET_CHANGES_attr(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ try:
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+ self.fail("ldb.search() should have failed with LDAP_INSUFFICIENT_ACCESS_RIGHTS")
+ except ldb.LdbError as e:
+ (errno, errstr) = e.args
+ self.assertEqual(errno, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+
+if not getattr(opts, "listtests", False):
+ lp = sambaopts.get_loadparm()
+ samba.tests.cmdline_credentials = credopts.get_credentials(lp)
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/dsdb_schema_info.py b/source4/dsdb/tests/python/dsdb_schema_info.py
new file mode 100644
index 0000000..50b04cd
--- /dev/null
+++ b/source4/dsdb/tests/python/dsdb_schema_info.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+#
+# Unix SMB/CIFS implementation.
+# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2010
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+#
+# Usage:
+# export DC_SERVER=target_dc_or_local_samdb_url
+# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
+# PYTHONPATH="$PYTHONPATH:$samba4srcdir/lib/ldb/tests/python" $SUBUNITRUN dsdb_schema_info -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
+#
+
+import sys
+import time
+import random
+
+sys.path.insert(0, "bin/python")
+import samba.tests
+
+from ldb import SCOPE_BASE, LdbError
+
+import samba.dcerpc.drsuapi
+from samba.dcerpc.drsblobs import schemaInfoBlob
+from samba.ndr import ndr_unpack
+from samba.dcerpc.misc import GUID
+
+
+class SchemaInfoTestCase(samba.tests.TestCase):
+
+ # static SamDB connection
+ sam_db = None
+
+ def setUp(self):
+ super(SchemaInfoTestCase, self).setUp()
+
+ # connect SamDB if we haven't yet
+ if self.sam_db is None:
+ ldb_url = "ldap://%s" % samba.tests.env_get_var_value("DC_SERVER")
+ SchemaInfoTestCase.sam_db = samba.tests.connect_samdb(ldb_url)
+
+ # fetch rootDSE
+ res = self.sam_db.search(base="", expression="", scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.schema_dn = res[0]["schemaNamingContext"][0]
+ self.base_dn = res[0]["defaultNamingContext"][0]
+ self.forest_level = int(res[0]["forestFunctionality"][0])
+
+ # get DC invocation_id
+ self.invocation_id = GUID(self.sam_db.get_invocation_id())
+
+ def tearDown(self):
+ super(SchemaInfoTestCase, self).tearDown()
+
+ def _getSchemaInfo(self):
+ try:
+ schema_info_data = self.sam_db.searchone(attribute="schemaInfo",
+ basedn=self.schema_dn,
+ expression="(objectClass=*)",
+ scope=SCOPE_BASE)
+ self.assertEqual(len(schema_info_data), 21)
+ schema_info = ndr_unpack(schemaInfoBlob, schema_info_data)
+ self.assertEqual(schema_info.marker, 0xFF)
+ except KeyError:
+ # create default schemaInfo if
+ # attribute value is not created yet
+ schema_info = schemaInfoBlob()
+ schema_info.revision = 0
+ schema_info.invocation_id = self.invocation_id
+ return schema_info
+
+ def _checkSchemaInfo(self, schi_before, schi_after):
+ self.assertEqual(schi_before.revision + 1, schi_after.revision)
+ self.assertEqual(schi_before.invocation_id, schi_after.invocation_id)
+ self.assertEqual(schi_after.invocation_id, self.invocation_id)
+
+ def _ldap_schemaUpdateNow(self):
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.sam_db.modify_ldif(ldif)
+
+ def _make_obj_names(self, prefix):
+ obj_name = prefix + time.strftime("%s", time.gmtime())
+ obj_ldap_name = obj_name.replace("-", "")
+ obj_dn = "CN=%s,%s" % (obj_name, self.schema_dn)
+ return (obj_name, obj_ldap_name, obj_dn)
+
+ def _make_attr_ldif(self, attr_name, attr_dn, sub_oid):
+ ldif = """
+dn: """ + attr_dn + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.7.%d.""" % sub_oid + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ return ldif
+
+ def test_AddModifyAttribute(self):
+ # get initial schemaInfo
+ schi_before = self._getSchemaInfo()
+
+ # create names for an attribute to add
+ (attr_name, attr_ldap_name, attr_dn) = self._make_obj_names("schemaInfo-Attr-")
+ ldif = self._make_attr_ldif(attr_name, attr_dn, 1)
+
+ # add the new attribute
+ self.sam_db.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+ # compare resulting schemaInfo
+ schi_after = self._getSchemaInfo()
+ self._checkSchemaInfo(schi_before, schi_after)
+
+ # rename the Attribute
+ attr_dn_new = attr_dn.replace(attr_name, attr_name + "-NEW")
+ try:
+ self.sam_db.rename(attr_dn, attr_dn_new)
+ except LdbError as e:
+ (num, _) = e.args
+ self.fail("failed to change CN for %s: %s" % (attr_name, _))
+
+ # compare resulting schemaInfo
+ schi_after = self._getSchemaInfo()
+ self._checkSchemaInfo(schi_before, schi_after)
+ pass
+
+ def _make_class_ldif(self, class_name, class_dn, sub_oid):
+ ldif = """
+dn: """ + class_dn + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.7.%d.""" % sub_oid + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ return ldif
+
+ def test_AddModifyClass(self, controls=[], class_pre="schemaInfo-Class-"):
+ # get initial schemaInfo
+ schi_before = self._getSchemaInfo()
+
+ # create names for a Class to add
+ (class_name, class_ldap_name, class_dn) =\
+ self._make_obj_names(class_pre)
+ ldif = self._make_class_ldif(class_name, class_dn, 1)
+
+ # add the new Class
+ self.sam_db.add_ldif(ldif, controls=controls)
+ self._ldap_schemaUpdateNow()
+ # compare resulting schemaInfo
+ schi_after = self._getSchemaInfo()
+ self._checkSchemaInfo(schi_before, schi_after)
+
+ # rename the Class
+ class_dn_new = class_dn.replace(class_name, class_name + "-NEW")
+ try:
+ self.sam_db.rename(class_dn, class_dn_new, controls=controls)
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.fail("failed to change CN for %s: %s" % (class_name, _))
+
+ # compare resulting schemaInfo
+ schi_after = self._getSchemaInfo()
+ self._checkSchemaInfo(schi_before, schi_after)
+
+ def test_AddModifyClassLocalRelaxed(self):
+ lp = self.get_loadparm()
+ self.sam_db = samba.tests.connect_samdb(lp.samdb_url())
+ self.test_AddModifyClass(controls=["relax:0"],
+ class_pre="schemaInfo-Relaxed-")
diff --git a/source4/dsdb/tests/python/large_ldap.py b/source4/dsdb/tests/python/large_ldap.py
new file mode 100644
index 0000000..cd81763
--- /dev/null
+++ b/source4/dsdb/tests/python/large_ldap.py
@@ -0,0 +1,333 @@
+#!/usr/bin/env python3
+#
+# Test large LDAP response behaviour in Samba
+# Copyright (C) Andrew Bartlett 2019
+#
+# Based on Unit tests for the notification control
+# Copyright (C) Stefan Metzmacher 2016
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import optparse
+import sys
+import os
+import random
+import time
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba import ldb, sd_utils
+from samba.samdb import SamDB
+from samba.ndr import ndr_unpack
+from samba import gensec
+from samba.credentials import Credentials
+import samba.tests
+
+from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, LdbError
+from ldb import ERR_TIME_LIMIT_EXCEEDED, ERR_ADMIN_LIMIT_EXCEEDED, ERR_UNWILLING_TO_PERFORM
+from ldb import Message
+
+parser = optparse.OptionParser("large_ldap.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)
+
+url = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class ManyLDAPTest(samba.tests.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ cls.base_dn = self.ldb.domain_dn()
+ cls.OU_NAME_MANY="many_ou" + format(random.randint(0, 99999), "05")
+ cls.ou_dn = ldb.Dn(self.ldb, "ou=" + self.OU_NAME_MANY + "," + str(self.base_dn))
+
+ samba.tests.delete_force(cls.ldb, cls.ou_dn,
+ controls=['tree_delete:1'])
+
+ cls.ldb.add({
+ "dn": cls.ou_dn,
+ "objectclass": "organizationalUnit",
+ "ou": cls.OU_NAME_MANY})
+
+ for x in range(2000):
+ ou_name = cls.OU_NAME_MANY + str(x)
+ cls.ldb.add({
+ "dn": "ou=" + ou_name + "," + str(cls.ou_dn),
+ "objectclass": "organizationalUnit",
+ "ou": ou_name})
+
+ @classmethod
+ def tearDownClass(cls):
+ samba.tests.delete_force(cls.ldb, self.ou_dn,
+ controls=['tree_delete:1'])
+
+ def test_unindexed_iterator_search(self):
+ """Testing a search for all the OUs.
+
+ Needed to test that more that IOV_MAX responses can be returned
+ """
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ count = 0
+ msg1 = None
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(ou=" + self.OU_NAME_MANY + "*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName"])
+
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count += 1
+ res1 = search1.result()
+
+ # Check we got everything
+ self.assertEqual(count, 2001)
+
+class LargeLDAPTest(samba.tests.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ cls.base_dn = cls.ldb.domain_dn()
+
+ cls.sd_utils = sd_utils.SDUtils(cls.ldb)
+ cls.USER_NAME = "large_user" + format(random.randint(0, 99999), "05") + "-"
+ cls.OU_NAME="large_user_ou" + format(random.randint(0, 99999), "05")
+ cls.ou_dn = ldb.Dn(cls.ldb, "ou=" + cls.OU_NAME + "," + str(cls.base_dn))
+
+
+ samba.tests.delete_force(cls.ldb, cls.ou_dn,
+ controls=['tree_delete:1'])
+
+ cls.ldb.add({
+ "dn": cls.ou_dn,
+ "objectclass": "organizationalUnit",
+ "ou": cls.OU_NAME})
+
+ for x in range(200):
+ user_name = cls.USER_NAME + format(x, "03")
+ cls.ldb.add({
+ "dn": "cn=" + user_name + "," + str(cls.ou_dn),
+ "objectclass": "user",
+ "sAMAccountName": user_name,
+ "jpegPhoto": b'a' * (2 * 1024 * 1024)})
+
+ ace = "(OD;;RP;{6bc69afa-7bd9-4184-88f5-28762137eb6a};;S-1-%d)" % x
+ dn = ldb.Dn(cls.ldb, "cn=" + user_name + "," + str(cls.ou_dn))
+
+ # add an ACE that denies access to the above random attr
+ # for a not-existing user. This makes each SD distinct
+ # and so will slow SD parsing.
+ cls.sd_utils.dacl_add_ace(dn, ace)
+
+ @classmethod
+ def tearDownClass(cls):
+ # Remake the connection for tear-down (old Samba drops the socket)
+ cls.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ samba.tests.delete_force(cls.ldb, cls.ou_dn,
+ controls=['tree_delete:1'])
+
+ def test_unindexed_iterator_search(self):
+ """Testing an unindexed search that will break the result size limit"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ count = 0
+ msg1 = None
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(sAMAccountName=" + self.USER_NAME + "*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName"])
+
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count += 1
+
+ res1 = search1.result()
+
+ self.assertEqual(count, 200)
+
+ # Now try breaking the 256MB limit
+
+ count_jpeg = 0
+ msg1 = None
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(sAMAccountName=" + self.USER_NAME + "*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName", "jpegPhoto"])
+ try:
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ msg1 = reply
+ count_jpeg += 1
+ except LdbError as e:
+ enum = err.args[0]
+ self.assertEqual(enum, ldb.ERR_SIZE_LIMIT_EXCEEDED)
+
+ # Assert we don't get all the entries but still the error
+ self.assertGreater(count, count_jpeg)
+
+ # Now try for just 100MB (server will do some chunking for this)
+
+ count_jpeg2 = 0
+ msg1 = None
+ try:
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(sAMAccountName=" + self.USER_NAME + "1*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName", "jpegPhoto"])
+ except LdbError as e:
+ enum = e.args[0]
+ estr = e.args[1]
+ self.fail(estr)
+
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ msg1 = reply
+ count_jpeg2 += 1
+
+ # Assert we got some entries
+ self.assertEqual(count_jpeg2, 100)
+
+ def test_iterator_search(self):
+ """Testing an indexed search that will break the result size limit"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ count = 0
+ msg1 = None
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(&(objectClass=user)(sAMAccountName=" + self.USER_NAME + "*))",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName"])
+
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count += 1
+ res1 = search1.result()
+
+ # Now try breaking the 256MB limit
+
+ count_jpeg = 0
+ msg1 = None
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(&(objectClass=user)(sAMAccountName=" + self.USER_NAME + "*))",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName", "jpegPhoto"])
+ try:
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count_jpeg =+ 1
+ except LdbError as e:
+ enum = err.args[0]
+ self.assertEqual(enum, ldb.ERR_SIZE_LIMIT_EXCEEDED)
+
+ # Assert we don't get all the entries but still the error
+ self.assertGreater(count, count_jpeg)
+
+ def test_timeout(self):
+
+ policy_dn = ldb.Dn(self.ldb,
+ 'CN=Default Query Policy,CN=Query-Policies,'
+ 'CN=Directory Service,CN=Windows NT,CN=Services,'
+ f'{self.ldb.get_config_basedn().get_linearized()}')
+
+ # Get the current value of lDAPAdminLimits.
+ res = self.ldb.search(base=policy_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['lDAPAdminLimits'])
+ msg = res[0]
+ admin_limits = msg['lDAPAdminLimits']
+
+ # Ensure we restore the previous value of the attribute.
+ admin_limits.set_flags(ldb.FLAG_MOD_REPLACE)
+ self.addCleanup(self.ldb.modify, msg)
+
+ # Temporarily lower the value of MaxQueryDuration so we can test
+ # timeout behaviour.
+ timeout = 5
+ query_duration = f'MaxQueryDuration={timeout}'.encode()
+
+ admin_limits = [limit for limit in admin_limits
+ if not limit.lower().startswith(b'maxqueryduration=')]
+ admin_limits.append(query_duration)
+
+ # Set the new attribute value.
+ msg = ldb.Message(policy_dn)
+ msg['lDAPAdminLimits'] = ldb.MessageElement(admin_limits,
+ ldb.FLAG_MOD_REPLACE,
+ 'lDAPAdminLimits')
+ self.ldb.modify(msg)
+
+ # Use a new connection so that the limits are reloaded.
+ samdb = SamDB(url, credentials=creds,
+ session_info=system_session(lp),
+ lp=lp)
+
+ # Create a large search expression that will take a long time to
+ # evaluate.
+ expression = '(jpegPhoto=*X*)' * 2000
+ expression = f'(|{expression})'
+
+ # Perform the LDAP search.
+ prev = time.time()
+ with self.assertRaises(ldb.LdbError) as err:
+ samdb.search(base=self.ou_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ expression=expression,
+ attrs=['objectGUID'])
+ now = time.time()
+ duration = now - prev
+
+ # Ensure that we timed out.
+ enum, _ = err.exception.args
+ self.assertEqual(ldb.ERR_TIME_LIMIT_EXCEEDED, enum)
+
+ # Ensure that the time spent searching is within the limit we
+ # set. We allow a marginal amount over as the Samba timeout
+ # handling is not very accurate (and does not need to be)
+ self.assertLess(timeout - 1, duration)
+ self.assertLess(duration, timeout * 4)
+
+
+if "://" not in url:
+ if os.path.isfile(url):
+ url = "tdb://%s" % url
+ else:
+ url = "ldap://%s" % url
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ldap.py b/source4/dsdb/tests/python/ldap.py
new file mode 100755
index 0000000..a899578
--- /dev/null
+++ b/source4/dsdb/tests/python/ldap.py
@@ -0,0 +1,3332 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This is a port of the original in testprogs/ejs/ldap.js
+
+# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008-2011
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import optparse
+import sys
+import time
+import base64
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, 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_NOT_ALLOWED_ON_NON_LEAF, ERR_OTHER, ERR_INVALID_DN_SYNTAX
+from ldb import ERR_NO_SUCH_ATTRIBUTE, ERR_INVALID_ATTRIBUTE_SYNTAX
+from ldb import ERR_OBJECT_CLASS_VIOLATION, ERR_NOT_ALLOWED_ON_RDN
+from ldb import ERR_NAMING_VIOLATION, ERR_CONSTRAINT_VIOLATION
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from ldb import timestring
+from samba import Ldb
+from samba.samdb import SamDB
+from samba.dsdb import (UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_PASSWD_NOTREQD, UF_ACCOUNTDISABLE, ATYPE_NORMAL_ACCOUNT,
+ ATYPE_WORKSTATION_TRUST, SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE,
+ SYSTEM_FLAG_CONFIG_ALLOW_RENAME, SYSTEM_FLAG_CONFIG_ALLOW_MOVE,
+ SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE)
+from samba.dcerpc.security import DOMAIN_RID_DOMAIN_MEMBERS
+
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import security, lsa
+from samba.tests import delete_force
+from samba.common import get_string
+
+parser = optparse.OptionParser("ldap.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)
+
+
+class BasicTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(BasicTests, self).setUp()
+ self.ldb = ldb
+ self.gc_ldb = gc_ldb
+ self.base_dn = ldb.domain_dn()
+ self.configuration_dn = ldb.get_config_basedn().get_linearized()
+ self.schema_dn = ldb.get_schema_basedn().get_linearized()
+ self.domain_sid = security.dom_sid(ldb.get_domain_sid())
+
+ delete_force(self.ldb, "cn=posixuser,cn=users," + 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, "cn=ldaptestuser3,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser4,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser4,cn=ldaptestcontainer2," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser5,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)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptest2computer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer3,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestutf8user èùéìòà,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestutf8user2 èùéìòà,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer2," + self.base_dn)
+ delete_force(self.ldb, "cn=parentguidtest,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=parentguidtest,cn=testotherusers," + self.base_dn)
+ delete_force(self.ldb, "cn=testotherusers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestobject," + self.base_dn)
+ delete_force(self.ldb, "description=xyz,cn=users," + self.base_dn)
+ delete_force(self.ldb, "ou=testou,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=Test Secret,cn=system," + self.base_dn)
+ delete_force(self.ldb, "cn=testtimevaluesuser1,cn=users," + self.base_dn)
+
+ def test_objectclasses(self):
+ """Test objectClass behaviour"""
+ # Invalid objectclass specified
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": []})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # Invalid objectclass specified
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "X"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # Invalid objectCategory specified
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "objectCategory": self.base_dn})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Multi-valued "systemFlags"
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "systemFlags": ["0", str(SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE)]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # We cannot instantiate from an abstract object class ("connectionPoint"
+ # or "leaf"). In the first case we use "connectionPoint" (subclass of
+ # "leaf") to prevent a naming violation - this returns us a
+ # "ERR_UNWILLING_TO_PERFORM" since it is not structural. In the second
+ # case however we get "ERR_OBJECT_CLASS_VIOLATION" since an abstract
+ # class is also not allowed to be auxiliary.
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "connectionPoint"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": ["person", "leaf"]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Objects instantiated using "satisfied" abstract classes (concrete
+ # subclasses) are allowed
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": ["top", "leaf", "connectionPoint", "serviceConnectionPoint"]})
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Two disjoint top-most structural object classes aren't allowed
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": ["person", "container"]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Test allowed system flags
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "systemFlags": str(~(SYSTEM_FLAG_CONFIG_ALLOW_RENAME | SYSTEM_FLAG_CONFIG_ALLOW_MOVE | SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE))})
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["systemFlags"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["systemFlags"][0]), "0")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person"})
+
+ # We can remove derivation classes of the structural objectclass
+ # but they're going to be re-added afterwards
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("top", FLAG_MOD_DELETE,
+ "objectClass")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectClass"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue(b"top" in res[0]["objectClass"])
+
+ # The top-most structural class cannot be deleted since there are
+ # attributes of it in use
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("person", FLAG_MOD_DELETE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # We cannot delete classes which weren't specified
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("computer", FLAG_MOD_DELETE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # An invalid class cannot be added
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("X", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # We cannot add a the new top-most structural class "user" here since
+ # we are missing at least one new mandatory attribute (in this case
+ # "sAMAccountName")
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("user", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # An already specified objectclass cannot be added another time
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("person", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ # Auxiliary classes can always be added
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_ADD,
+ "objectClass")
+ ldb.modify(m)
+
+ # This does not work since object class "leaf" is not auxiliary nor it
+ # stands in direct relation to "person" (and it is abstract too!)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("leaf", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Objectclass replace operations can be performed as well
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement(["top", "person", "bootableDevice"],
+ FLAG_MOD_REPLACE, "objectClass")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement(["person", "bootableDevice"],
+ FLAG_MOD_REPLACE, "objectClass")
+ ldb.modify(m)
+
+ # This does not work since object class "leaf" is not auxiliary nor it
+ # stands in direct relation to "person" (and it is abstract too!)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement(["top", "person", "bootableDevice",
+ "leaf"], FLAG_MOD_REPLACE, "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # More than one change operation is allowed
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m.add(MessageElement("bootableDevice", FLAG_MOD_DELETE, "objectClass"))
+ m.add(MessageElement("bootableDevice", FLAG_MOD_ADD, "objectClass"))
+ ldb.modify(m)
+
+ # We cannot remove all object classes by an empty replace
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement([], FLAG_MOD_REPLACE, "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement(["top", "computer"], FLAG_MOD_REPLACE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Classes can be removed unless attributes of them are used.
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_DELETE,
+ "objectClass")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectClass"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("bootableDevice" in res[0]["objectClass"])
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_ADD,
+ "objectClass")
+ ldb.modify(m)
+
+ # Add an attribute specific to the "bootableDevice" class
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["bootParameter"] = MessageElement("test", FLAG_MOD_ADD,
+ "bootParameter")
+ ldb.modify(m)
+
+ # Classes can be removed unless attributes of them are used. Now there
+ # exist such attributes on the entry.
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_DELETE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Remove the previously specified attribute
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["bootParameter"] = MessageElement("test", FLAG_MOD_DELETE,
+ "bootParameter")
+ ldb.modify(m)
+
+ # Classes can be removed unless attributes of them are used.
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_DELETE,
+ "objectClass")
+ ldb.modify(m)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "user"})
+
+ # Add a new top-most structural class "container". This does not work
+ # since it stands in no direct relation to the current one.
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("container", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Try to add a new top-most structural class "inetOrgPerson"
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("inetOrgPerson", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Try to remove the structural class "user"
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("user", FLAG_MOD_DELETE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Try to replace top-most structural class to "inetOrgPerson"
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("inetOrgPerson", FLAG_MOD_REPLACE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Add a new auxiliary object class "posixAccount" to "ldaptestuser"
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("posixAccount", FLAG_MOD_ADD,
+ "objectClass")
+ ldb.modify(m)
+
+ # Be sure that "top" is the first and the (most) structural object class
+ # the last value of the "objectClass" attribute - MS-ADTS 3.1.1.1.4
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectClass"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["objectClass"][0]), "top")
+ self.assertEqual(str(res[0]["objectClass"][len(res[0]["objectClass"]) - 1]), "user")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_system_only(self):
+ """Test systemOnly objects"""
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestobject," + self.base_dn,
+ "objectclass": "configuration"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=Test Secret,cn=system," + self.base_dn,
+ "objectclass": "secret"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestobject," + self.base_dn)
+ delete_force(self.ldb, "cn=Test Secret,cn=system," + self.base_dn)
+
+ # Create secret over LSA and try to change it
+
+ lsa_conn = lsa.lsarpc("ncacn_np:%s" % args[0], lp, creds)
+ lsa_handle = lsa_conn.OpenPolicy2(system_name="\\",
+ attr=lsa.ObjectAttribute(),
+ access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
+ secret_name = lsa.String()
+ secret_name.string = "G$Test"
+ sec_handle = lsa_conn.CreateSecret(handle=lsa_handle,
+ name=secret_name,
+ access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
+ lsa_conn.Close(lsa_handle)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=Test Secret,cn=system," + self.base_dn)
+ m["description"] = MessageElement("desc", FLAG_MOD_REPLACE,
+ "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=Test Secret,cn=system," + self.base_dn)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container",
+ "isCriticalSystemObject": "TRUE"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcontainer," + self.base_dn)
+ m["isCriticalSystemObject"] = MessageElement("TRUE", FLAG_MOD_REPLACE,
+ "isCriticalSystemObject")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ # Proof if DC SAM object has "isCriticalSystemObject" set
+ res = self.ldb.search("", scope=SCOPE_BASE, attrs=["serverName"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("serverName" in res[0])
+ res = self.ldb.search(res[0]["serverName"][0], scope=SCOPE_BASE,
+ attrs=["serverReference"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("serverReference" in res[0])
+ res = self.ldb.search(res[0]["serverReference"][0], scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("isCriticalSystemObject" in res[0])
+ self.assertEqual(str(res[0]["isCriticalSystemObject"][0]), "TRUE")
+
+ def test_invalid_parent(self):
+ """Test adding an object with invalid parent"""
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=thisdoesnotexist123,"
+ + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=thisdoesnotexist123,"
+ + self.base_dn)
+
+ try:
+ self.ldb.add({
+ "dn": "ou=testou,cn=users," + self.base_dn,
+ "objectclass": "organizationalUnit"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NAMING_VIOLATION)
+
+ delete_force(self.ldb, "ou=testou,cn=users," + self.base_dn)
+
+ def test_invalid_attribute(self):
+ """Test invalid attributes on schema/objectclasses"""
+ # attributes not in schema test
+
+ # add operation
+
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "thisdoesnotexist": "x"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ # modify operation
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["thisdoesnotexist"] = MessageElement("x", FLAG_MOD_REPLACE,
+ "thisdoesnotexist")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ #
+ # When searching the unknown attribute should be ignored
+ expr = "(|(cn=ldaptestgroup)(thisdoesnotexist=x))"
+ res = ldb.search(base=self.base_dn,
+ expression=expr,
+ scope=SCOPE_SUBTREE)
+ self.assertTrue(len(res) == 1,
+ "Search including unknown attribute failed")
+
+ # likewise, if we specifically request an unknown attribute
+ res = ldb.search(base=self.base_dn,
+ expression="(cn=ldaptestgroup)",
+ scope=SCOPE_SUBTREE,
+ attrs=["thisdoesnotexist"])
+ self.assertTrue(len(res) == 1,
+ "Search requesting unknown attribute failed")
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ # attributes not in objectclasses and mandatory attributes missing test
+ # Use here a non-SAM entry since it doesn't have special triggers
+ # associated which have an impact on the error results.
+
+ # add operations
+
+ # mandatory attribute missing
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestobject," + self.base_dn,
+ "objectclass": "ipProtocol"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # inadequate but schema-valid attribute specified
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestobject," + self.base_dn,
+ "objectclass": "ipProtocol",
+ "ipProtocolNumber": "1",
+ "uid": "0"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestobject," + self.base_dn,
+ "objectclass": "ipProtocol",
+ "ipProtocolNumber": "1"})
+
+ # modify operations
+
+ # inadequate but schema-valid attribute add trial
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestobject," + self.base_dn)
+ m["uid"] = MessageElement("0", FLAG_MOD_ADD, "uid")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # mandatory attribute delete trial
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestobject," + self.base_dn)
+ m["ipProtocolNumber"] = MessageElement([], FLAG_MOD_DELETE,
+ "ipProtocolNumber")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # mandatory attribute delete trial
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestobject," + self.base_dn)
+ m["ipProtocolNumber"] = MessageElement([], FLAG_MOD_REPLACE,
+ "ipProtocolNumber")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ delete_force(self.ldb, "cn=ldaptestobject," + self.base_dn)
+
+ def test_single_valued_attributes(self):
+ """Test single-valued attributes"""
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "sAMAccountName": ["nam1", "nam2"]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ 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["sAMAccountName"] = MessageElement(["nam1", "nam2"], FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testgroupXX", FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testgroupXX2", FLAG_MOD_ADD,
+ "sAMAccountName")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_single_valued_linked_attributes(self):
+ """Test managedBy, a single-valued linked attribute.
+
+ (The single-valuedness of this is enforced differently, in
+ repl_meta_data.c)
+ """
+ ou = 'OU=svla,%s' % (self.base_dn)
+
+ delete_force(self.ldb, ou, controls=['tree_delete:1'])
+
+ self.ldb.add({'objectclass': 'organizationalUnit',
+ 'dn': ou})
+
+ managers = []
+ for x in range(3):
+ m = "cn=manager%d,%s" % (x, ou)
+ self.ldb.add({
+ "dn": m,
+ "objectclass": "user"})
+ managers.append(m)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=group1," + ou,
+ "objectclass": "group",
+ "managedBy": managers
+ })
+ self.fail("failed to fail to add multiple managedBy attributes")
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ managee = "cn=group2," + ou
+ self.ldb.add({
+ "dn": managee,
+ "objectclass": "group",
+ "managedBy": [managers[0]]})
+
+ m = Message()
+ m.dn = Dn(ldb, managee)
+ m["managedBy"] = MessageElement(managers, FLAG_MOD_REPLACE,
+ "managedBy")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, managee)
+ m["managedBy"] = MessageElement(managers[1], FLAG_MOD_REPLACE,
+ "managedBy")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, managee)
+ m["managedBy"] = MessageElement(managers[2], FLAG_MOD_ADD,
+ "managedBy")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ self.ldb.delete(ou, ['tree_delete:1'])
+
+ def test_multivalued_attributes(self):
+ """Test multi-valued attributes"""
+ ou = 'OU=mvattr,%s' % (self.base_dn)
+ delete_force(self.ldb, ou, controls=['tree_delete:1'])
+ self.ldb.add({'objectclass': 'organizationalUnit',
+ 'dn': ou})
+
+ # beyond 1210, Win2012r2 gives LDAP_ADMIN_LIMIT_EXCEEDED
+ ranges = (3, 30, 300, 1210)
+
+ for n in ranges:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser%d,%s" % (n, ou),
+ "objectclass": "user",
+ "carLicense": ["car%d" % x for x in range(n)]})
+
+ # add some more
+ for n in ranges:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser%d,%s" % (n, ou))
+ m["carLicense"] = MessageElement(["another"],
+ FLAG_MOD_ADD,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser%d,%s" % (n, ou))
+ m["carLicense"] = MessageElement(["foo%d" % x for x in range(4)],
+ FLAG_MOD_ADD,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser%d,%s" % (n, ou))
+ m["carLicense"] = MessageElement(["bar%d" % x for x in range(40)],
+ FLAG_MOD_ADD,
+ "carLicense")
+ ldb.modify(m)
+
+ for n in ranges:
+ m = Message()
+ dn = "cn=ldaptestuser%d,%s" % (n, ou)
+ m.dn = Dn(ldb, dn)
+ m["carLicense"] = MessageElement(["replacement"],
+ FLAG_MOD_REPLACE,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, dn)
+ m["carLicense"] = MessageElement(["replacement%d" % x for x in range(n)],
+ FLAG_MOD_REPLACE,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, dn)
+ m["carLicense"] = MessageElement(["again%d" % x for x in range(n)],
+ FLAG_MOD_REPLACE,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, dn)
+ m["carLicense"] = MessageElement(["andagain%d" % x for x in range(n)],
+ FLAG_MOD_REPLACE,
+ "carLicense")
+ ldb.modify(m)
+
+ self.ldb.delete(ou, ['tree_delete:1'])
+
+ def test_attribute_ranges(self):
+ """Test attribute ranges"""
+ # Too short (min. 1)
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "sn": ""})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_ATTRIBUTE_SYNTAX)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person"})
+
+ # Too short (min. 1)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sn"] = MessageElement("", FLAG_MOD_REPLACE, "sn")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_ATTRIBUTE_SYNTAX)
+
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sn"] = MessageElement("x", FLAG_MOD_REPLACE, "sn")
+ ldb.modify(m)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_attribute_ranges_too_long(self):
+ """Test attribute ranges"""
+ # This is knownfail with the wrong error
+ # (INVALID_ATTRIBUTE_SYNTAX vs CONSTRAINT_VIOLATION per Windows)
+
+ # Too long (max. 64)
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "sn": "x" * 65 })
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person"})
+
+ # Too long (max. 64)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sn"] = MessageElement("x" * 66, FLAG_MOD_REPLACE, "sn")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ self.assertEqual(e.args[0], ERR_CONSTRAINT_VIOLATION)
+
+ def test_empty_messages(self):
+ """Test empty messages"""
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ try:
+ ldb.add(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_empty_attributes(self):
+ """Test empty attributes"""
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("group", FLAG_MOD_ADD, "objectClass")
+ m["description"] = MessageElement([], FLAG_MOD_ADD, "description")
+
+ try:
+ ldb.add(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ 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([], FLAG_MOD_ADD, "description")
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement([], FLAG_MOD_REPLACE, "description")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement([], FLAG_MOD_DELETE, "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_instanceType(self):
+ """Tests the 'instanceType' attribute"""
+ # The instance type is single-valued
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "instanceType": ["0", "1"]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # The head NC flag cannot be set without the write flag
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "instanceType": "1"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # We cannot manipulate NCs without the head NC flag
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "instanceType": "32"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ 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["instanceType"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "instanceType")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["instanceType"] = MessageElement([], FLAG_MOD_REPLACE,
+ "instanceType")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["instanceType"] = MessageElement([], FLAG_MOD_DELETE, "instanceType")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ # only write is allowed with NC_HEAD for originating updates
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "instanceType": "3"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+
+ def test_distinguished_name(self):
+ """Tests the 'distinguishedName' attribute"""
+ # The "dn" shortcut isn't supported
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("group", 0, "objectClass")
+ m["dn"] = MessageElement("cn=ldaptestgroup,cn=users," + self.base_dn, 0,
+ "dn")
+ try:
+ ldb.add(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # a wrong "distinguishedName" attribute is obviously tolerated
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "distinguishedName": "cn=ldaptest,cn=users," + self.base_dn})
+
+ # proof if the DN has been set correctly
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["distinguishedName"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("distinguishedName" in res[0])
+ self.assertTrue(Dn(ldb, str(res[0]["distinguishedName"][0]))
+ == Dn(ldb, "cn=ldaptestgroup, cn=users," + self.base_dn))
+
+ # The "dn" shortcut isn't supported
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["dn"] = MessageElement(
+ "cn=ldaptestgroup,cn=users," + self.base_dn, FLAG_MOD_REPLACE,
+ "dn")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["distinguishedName"] = MessageElement(
+ "cn=ldaptestuser,cn=users," + self.base_dn, FLAG_MOD_ADD,
+ "distinguishedName")
+
+ 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["distinguishedName"] = MessageElement(
+ "cn=ldaptestuser,cn=users," + self.base_dn, FLAG_MOD_REPLACE,
+ "distinguishedName")
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["distinguishedName"] = MessageElement(
+ "cn=ldaptestuser,cn=users," + self.base_dn, FLAG_MOD_DELETE,
+ "distinguishedName")
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_rdn_name(self):
+ """Tests the RDN"""
+ # Search
+
+ # empty RDN
+ try:
+ self.ldb.search("=,cn=users," + self.base_dn, scope=SCOPE_BASE)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # empty RDN name
+ try:
+ self.ldb.search("cn=,cn=users," + self.base_dn, scope=SCOPE_BASE)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ try:
+ self.ldb.search("=ldaptestgroup,cn=users," + self.base_dn, scope=SCOPE_BASE)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # Add
+
+ # empty RDN
+ try:
+ self.ldb.add({
+ "dn": "=,cn=users," + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # empty RDN name
+ try:
+ self.ldb.add({
+ "dn": "=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # empty RDN value
+ try:
+ self.ldb.add({
+ "dn": "cn=,cn=users," + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # a wrong RDN candidate
+ try:
+ self.ldb.add({
+ "dn": "description=xyz,cn=users," + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NAMING_VIOLATION)
+
+ delete_force(self.ldb, "description=xyz,cn=users," + self.base_dn)
+
+ # a wrong "name" attribute is obviously tolerated
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "name": "ldaptestgroupx"})
+
+ # proof if the name has been set correctly
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["name"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("name" in res[0])
+ self.assertTrue(str(res[0]["name"][0]) == "ldaptestgroup")
+
+ # Modify
+
+ # empty RDN value
+ m = Message()
+ m.dn = Dn(ldb, "cn=,cn=users," + self.base_dn)
+ m["description"] = "test"
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # Delete
+
+ # empty RDN value
+ try:
+ self.ldb.delete("cn=,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # Rename
+
+ # new empty RDN
+ try:
+ self.ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn,
+ "=,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # new empty RDN name
+ try:
+ self.ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn,
+ "=ldaptestgroup,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # new empty RDN value
+ try:
+ self.ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn,
+ "cn=,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NAMING_VIOLATION)
+
+ # new wrong RDN candidate
+ try:
+ self.ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn,
+ "description=xyz,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "description=xyz,cn=users," + self.base_dn)
+
+ # old empty RDN value
+ try:
+ self.ldb.rename("cn=,cn=users," + self.base_dn,
+ "cn=ldaptestgroup,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # names
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["name"] = MessageElement("cn=ldaptestuser", FLAG_MOD_REPLACE,
+ "name")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_RDN)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["cn"] = MessageElement("ldaptestuser",
+ FLAG_MOD_REPLACE, "cn")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_RDN)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ # this test needs to be disabled until we really understand
+ # what the rDN length constraints are
+
+ def DISABLED_test_largeRDN(self):
+ """Testing large rDN (limit 64 characters)"""
+ rdn = "CN=a012345678901234567890123456789012345678901234567890123456789012"
+ delete_force(self.ldb, "%s,%s" % (rdn, self.base_dn))
+ ldif = """
+dn: %s,%s""" % (rdn, self.base_dn) + """
+objectClass: container
+"""
+ self.ldb.add_ldif(ldif)
+ delete_force(self.ldb, "%s,%s" % (rdn, self.base_dn))
+
+ rdn = "CN=a0123456789012345678901234567890123456789012345678901234567890120"
+ delete_force(self.ldb, "%s,%s" % (rdn, self.base_dn))
+ try:
+ ldif = """
+dn: %s,%s""" % (rdn, self.base_dn) + """
+objectClass: container
+"""
+ self.ldb.add_ldif(ldif)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ delete_force(self.ldb, "%s,%s" % (rdn, self.base_dn))
+
+ def test_rename(self):
+ """Tests the rename operation"""
+ try:
+ # cannot rename to be a child of itself
+ ldb.rename(self.base_dn, "dc=test," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ # inexistent object
+ ldb.rename("cn=ldaptestuser2,cn=users," + self.base_dn, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ ldb.rename("cn=ldaptestuser2,cn=users," + self.base_dn, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ ldb.rename("cn=ldaptestuser2,cn=users," + self.base_dn, "cn=ldaptestuser3,cn=users," + self.base_dn)
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestUSER3,cn=users," + self.base_dn)
+
+ try:
+ # containment problem: a user entry cannot contain user entries
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestuser4,cn=ldaptestuser3,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NAMING_VIOLATION)
+
+ try:
+ # invalid parent
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestuser3,cn=people,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OTHER)
+
+ try:
+ # invalid target DN syntax
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, ",cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ try:
+ # invalid RDN name
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "ou=ldaptestuser3,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestuser3,cn=users," + self.base_dn)
+
+ # Performs some "systemFlags" testing
+
+ # Move failing since no "SYSTEM_FLAG_CONFIG_ALLOW_MOVE"
+ try:
+ ldb.rename("CN=DisplaySpecifiers," + self.configuration_dn, "CN=DisplaySpecifiers,CN=Services," + self.configuration_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Limited move failing since no "SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE"
+ try:
+ ldb.rename("CN=Directory Service,CN=Windows NT,CN=Services," + self.configuration_dn, "CN=Directory Service,CN=RRAS,CN=Services," + self.configuration_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Rename failing since no "SYSTEM_FLAG_CONFIG_ALLOW_RENAME"
+ try:
+ ldb.rename("CN=DisplaySpecifiers," + self.configuration_dn, "CN=DisplaySpecifiers2," + self.configuration_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # It's not really possible to test moves on the schema partition since
+ # there don't exist subcontainers on it.
+
+ # Rename failing since "SYSTEM_FLAG_SCHEMA_BASE_OBJECT"
+ try:
+ ldb.rename("CN=Top," + self.schema_dn, "CN=Top2," + self.schema_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Move failing since "SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE"
+ try:
+ ldb.rename("CN=Users," + self.base_dn, "CN=Users,CN=Computers," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Rename failing since "SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME"
+ try:
+ ldb.rename("CN=Users," + self.base_dn, "CN=Users2," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Performs some other constraints testing
+
+ try:
+ ldb.rename("CN=Policies,CN=System," + self.base_dn, "CN=Users2," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OTHER)
+
+ def test_rename_twice(self):
+ """Tests the rename operation twice - this corresponds to a past bug"""
+ self.ldb.add({
+ "dn": "cn=ldaptestuser5,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ ldb.rename("cn=ldaptestuser5,cn=users," + self.base_dn, "cn=ldaptestUSER5,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser5,cn=users," + self.base_dn)
+ self.ldb.add({
+ "dn": "cn=ldaptestuser5,cn=users," + self.base_dn,
+ "objectclass": "user"})
+ ldb.rename("cn=ldaptestuser5,cn=Users," + self.base_dn, "cn=ldaptestUSER5,cn=users," + self.base_dn)
+ res = ldb.search(expression="cn=ldaptestuser5")
+ self.assertEqual(len(res), 1, "Wrong number of hits for cn=ldaptestuser5")
+ res = ldb.search(expression="(&(cn=ldaptestuser5)(objectclass=user))")
+ self.assertEqual(len(res), 1, "Wrong number of hits for (&(cn=ldaptestuser5)(objectclass=user))")
+ delete_force(self.ldb, "cn=ldaptestuser5,cn=users," + self.base_dn)
+
+ def test_objectGUID(self):
+ """Test objectGUID behaviour"""
+ # The objectGUID cannot directly be set
+ try:
+ self.ldb.add_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+objectClass: container
+objectGUID: bd3480c9-58af-4cd8-92df-bc4a18b6e44d
+""")
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectClass": "container"})
+
+ # The objectGUID cannot directly be changed
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: objectGUID
+objectGUID: bd3480c9-58af-4cd8-92df-bc4a18b6e44d
+""")
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ def test_parentGUID(self):
+ """Test parentGUID behaviour"""
+ self.ldb.add({
+ "dn": "cn=parentguidtest,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "samaccountname": "parentguidtest"})
+ res1 = ldb.search(base="cn=parentguidtest,cn=users," + self.base_dn, scope=SCOPE_BASE,
+ attrs=["parentGUID", "samaccountname"])
+ res2 = ldb.search(base="cn=users," + self.base_dn, scope=SCOPE_BASE,
+ attrs=["objectGUID"])
+ res3 = ldb.search(base=self.base_dn, scope=SCOPE_BASE,
+ attrs=["parentGUID"])
+ res4 = ldb.search(base=self.configuration_dn, scope=SCOPE_BASE,
+ attrs=["parentGUID"])
+ res5 = ldb.search(base=self.schema_dn, scope=SCOPE_BASE,
+ attrs=["parentGUID"])
+
+ """Check if the parentGUID is valid """
+ self.assertEqual(res1[0]["parentGUID"], res2[0]["objectGUID"])
+
+ """Check if it returns nothing when there is no parent object - default NC"""
+ has_parentGUID = False
+ for key in res3[0].keys():
+ if key == "parentGUID":
+ has_parentGUID = True
+ break
+ self.assertFalse(has_parentGUID)
+
+ """Check if it returns nothing when there is no parent object - configuration NC"""
+ has_parentGUID = False
+ for key in res4[0].keys():
+ if key == "parentGUID":
+ has_parentGUID = True
+ break
+ self.assertFalse(has_parentGUID)
+
+ """Check if it returns nothing when there is no parent object - schema NC"""
+ has_parentGUID = False
+ for key in res5[0].keys():
+ if key == "parentGUID":
+ has_parentGUID = True
+ break
+ self.assertFalse(has_parentGUID)
+
+ """Ensures that if you look for another object attribute after the constructed
+ parentGUID, it will return correctly"""
+ has_another_attribute = False
+ for key in res1[0].keys():
+ if key == "sAMAccountName":
+ has_another_attribute = True
+ break
+ self.assertTrue(has_another_attribute)
+ self.assertTrue(len(res1[0]["samaccountname"]) == 1)
+ self.assertEqual(str(res1[0]["samaccountname"][0]), "parentguidtest")
+
+ # Testing parentGUID behaviour on rename\
+
+ self.ldb.add({
+ "dn": "cn=testotherusers," + self.base_dn,
+ "objectclass": "container"})
+ res1 = ldb.search(base="cn=testotherusers," + self.base_dn, scope=SCOPE_BASE,
+ attrs=["objectGUID"])
+ ldb.rename("cn=parentguidtest,cn=users," + self.base_dn,
+ "cn=parentguidtest,cn=testotherusers," + self.base_dn)
+ res2 = ldb.search(base="cn=parentguidtest,cn=testotherusers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["parentGUID"])
+ self.assertEqual(res1[0]["objectGUID"], res2[0]["parentGUID"])
+
+ delete_force(self.ldb, "cn=parentguidtest,cn=testotherusers," + self.base_dn)
+ delete_force(self.ldb, "cn=testotherusers," + self.base_dn)
+
+ def test_usnChanged(self):
+ """Test usnChanged behaviour"""
+
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectClass": "container"})
+
+ res = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["objectGUID", "uSNCreated", "uSNChanged", "whenCreated", "whenChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res[0])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("uSNCreated" in res[0])
+ self.assertTrue("uSNChanged" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertTrue("whenChanged" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ # All this attributes are specificable on add operations
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container",
+ "uSNCreated": "1",
+ "uSNChanged": "1",
+ "whenCreated": timestring(int(time.time())),
+ "whenChanged": timestring(int(time.time()))})
+
+ res = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["objectGUID", "uSNCreated", "uSNChanged", "whenCreated", "whenChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res[0])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("uSNCreated" in res[0])
+ self.assertFalse(res[0]["uSNCreated"][0] == "1") # these are corrected
+ self.assertTrue("uSNChanged" in res[0])
+ self.assertFalse(res[0]["uSNChanged"][0] == "1") # these are corrected
+ self.assertTrue("whenCreated" in res[0])
+ self.assertTrue("whenChanged" in res[0])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+""")
+
+ res2 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res2[0])
+ self.assertEqual(res[0]["usnCreated"], res2[0]["usnCreated"])
+ self.assertEqual(res[0]["usnCreated"], res2[0]["usnChanged"])
+ self.assertEqual(res[0]["usnChanged"], res2[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+description: test
+""")
+
+ res3 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res3[0])
+ self.assertEqual("test", str(res3[0]["description"][0]))
+ self.assertEqual(res[0]["usnCreated"], res3[0]["usnCreated"])
+ self.assertNotEqual(res[0]["usnCreated"], res3[0]["usnChanged"])
+ self.assertNotEqual(res[0]["usnChanged"], res3[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+description: test
+""")
+
+ res4 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res4[0])
+ self.assertEqual("test", str(res4[0]["description"][0]))
+ self.assertEqual(res[0]["usnCreated"], res4[0]["usnCreated"])
+ self.assertNotEqual(res3[0]["usnCreated"], res4[0]["usnChanged"])
+ self.assertEqual(res3[0]["usnChanged"], res4[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+description: test2
+""")
+
+ res5 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res5[0])
+ self.assertEqual("test2", str(res5[0]["description"][0]))
+ self.assertEqual(res[0]["usnCreated"], res5[0]["usnCreated"])
+ self.assertNotEqual(res3[0]["usnChanged"], res5[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+delete: description
+description: test2
+""")
+
+ res6 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res6[0])
+ self.assertEqual(res[0]["usnCreated"], res6[0]["usnCreated"])
+ self.assertNotEqual(res5[0]["usnChanged"], res6[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+add: description
+description: test3
+""")
+
+ res7 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res7[0])
+ self.assertEqual("test3", str(res7[0]["description"][0]))
+ self.assertEqual(res[0]["usnCreated"], res7[0]["usnCreated"])
+ self.assertNotEqual(res6[0]["usnChanged"], res7[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+delete: description
+""")
+
+ res8 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res8[0])
+ self.assertEqual(res[0]["usnCreated"], res8[0]["usnCreated"])
+ self.assertNotEqual(res7[0]["usnChanged"], res8[0]["usnChanged"])
+
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ def test_groupType_int32(self):
+ """Test groupType (int32) behaviour (should appear to be casted to a 32 bit signed integer before comparison)"""
+
+ res1 = ldb.search(base=self.base_dn, scope=SCOPE_SUBTREE,
+ attrs=["groupType"], expression="groupType=2147483653")
+
+ res2 = ldb.search(base=self.base_dn, scope=SCOPE_SUBTREE,
+ attrs=["groupType"], expression="groupType=-2147483643")
+
+ self.assertEqual(len(res1), len(res2))
+
+ self.assertTrue(res1.count > 0)
+
+ self.assertEqual(str(res1[0]["groupType"][0]), "-2147483643")
+
+ def test_linked_attributes(self):
+ """This tests the linked attribute behaviour"""
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ # This should not work since "memberOf" is linked to "member"
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "memberOf": "cn=ldaptestgroup,cn=users," + self.base_dn})
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ 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["memberOf"] = MessageElement("cn=ldaptestgroup,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "memberOf")
+ 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["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["memberOf"] = MessageElement("cn=ldaptestgroup,cn=users," + self.base_dn,
+ FLAG_MOD_REPLACE, "memberOf")
+ 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=ldaptestuser,cn=users," + self.base_dn)
+ m["memberOf"] = MessageElement("cn=ldaptestgroup,cn=users," + self.base_dn,
+ FLAG_MOD_DELETE, "memberOf")
+ 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["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_DELETE, "member")
+ ldb.modify(m)
+
+ # This should yield no results since the member attribute for
+ # "ldaptestuser" should have been deleted
+ res1 = ldb.search("cn=ldaptestgroup, cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ expression="(member=cn=ldaptestuser,cn=users," + self.base_dn + ")",
+ attrs=[])
+ self.assertTrue(len(res1) == 0)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "member": "cn=ldaptestuser,cn=users," + self.base_dn})
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Make sure that the "member" attribute for "ldaptestuser" has been
+ # removed
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["member"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("member" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_wkguid(self):
+ """Test Well known GUID behaviours (including DN+Binary)"""
+
+ res = self.ldb.search(base=("<WKGUID=ab1d30f3768811d1aded00c04fd8d5cd,%s>" % self.base_dn), scope=SCOPE_BASE, attrs=[])
+ self.assertEqual(len(res), 1)
+
+ res2 = self.ldb.search(scope=SCOPE_BASE, attrs=["wellKnownObjects"], expression=("wellKnownObjects=B:32:ab1d30f3768811d1aded00c04fd8d5cd:%s" % res[0].dn))
+ self.assertEqual(len(res2), 1)
+
+ # Prove that the matching rule is over the whole DN+Binary
+ res2 = self.ldb.search(scope=SCOPE_BASE, attrs=["wellKnownObjects"], expression=("wellKnownObjects=B:32:ab1d30f3768811d1aded00c04fd8d5cd"))
+ self.assertEqual(len(res2), 0)
+ # Prove that the matching rule is over the whole DN+Binary
+ res2 = self.ldb.search(scope=SCOPE_BASE, attrs=["wellKnownObjects"], expression=("wellKnownObjects=%s") % res[0].dn)
+ self.assertEqual(len(res2), 0)
+
+ def test_subschemasubentry(self):
+ """Test subSchemaSubEntry appears when requested, but not when not requested"""
+
+ res = self.ldb.search(base=self.base_dn, scope=SCOPE_BASE, attrs=["subSchemaSubEntry"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["subSchemaSubEntry"][0]), "CN=Aggregate," + self.schema_dn)
+
+ res = self.ldb.search(base=self.base_dn, scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("subScheamSubEntry" not in res[0])
+
+ def test_all(self):
+ """Basic tests"""
+
+ # Testing user add
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=uSers," + self.base_dn,
+ "objectclass": "user",
+ "cN": "LDAPtestUSER",
+ "givenname": "ldap",
+ "sn": "testy"})
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=uSers," + self.base_dn,
+ "objectclass": "group",
+ "member": "cn=ldaptestuser,cn=useRs," + self.base_dn})
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "cN": "LDAPtestCOMPUTER"})
+
+ ldb.add({"dn": "cn=ldaptest2computer,cn=computers," + self.base_dn,
+ "objectClass": "computer",
+ "cn": "LDAPtest2COMPUTER",
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT),
+ "displayname": "ldap testy"})
+
+ try:
+ ldb.add({"dn": "cn=ldaptestcomputer3,cn=computers," + self.base_dn,
+ "objectClass": "computer",
+ "cn": "LDAPtest2COMPUTER"
+ })
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ try:
+ ldb.add({"dn": "cn=ldaptestcomputer3,cn=computers," + self.base_dn,
+ "objectClass": "computer",
+ "cn": "ldaptestcomputer3",
+ "sAMAccountType": str(ATYPE_NORMAL_ACCOUNT)
+ })
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ ldb.add({"dn": "cn=ldaptestcomputer3,cn=computers," + self.base_dn,
+ "objectClass": "computer",
+ "cn": "LDAPtestCOMPUTER3"
+ })
+
+ # Testing ldb.search for (&(cn=ldaptestcomputer3)(objectClass=user))
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestcomputer3)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Found only %d for (&(cn=ldaptestcomputer3)(objectClass=user))" % len(res))
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestcomputer3,CN=Computers," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"][0]), "ldaptestcomputer3")
+ self.assertEqual(str(res[0]["name"][0]), "ldaptestcomputer3")
+ self.assertEqual(str(res[0]["objectClass"][0]), "top")
+ self.assertEqual(str(res[0]["objectClass"][1]), "person")
+ self.assertEqual(str(res[0]["objectClass"][2]), "organizationalPerson")
+ self.assertEqual(str(res[0]["objectClass"][3]), "user")
+ self.assertEqual(str(res[0]["objectClass"][4]), "computer")
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertEqual(str(res[0]["objectCategory"][0]), ("CN=Computer,%s" % ldb.get_schema_basedn()))
+ self.assertEqual(int(res[0]["primaryGroupID"][0]), DOMAIN_RID_DOMAIN_MEMBERS)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]), ATYPE_WORKSTATION_TRUST)
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_WORKSTATION_TRUST_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE)
+
+ delete_force(self.ldb, "cn=ldaptestcomputer3,cn=computers," + self.base_dn)
+
+ # Testing attribute or value exists behaviour
+ try:
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+replace: servicePrincipalName
+servicePrincipalName: host/ldaptest2computer
+servicePrincipalName: host/ldaptest2computer
+servicePrincipalName: cifs/ldaptest2computer
+""")
+ self.fail()
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+replace: servicePrincipalName
+servicePrincipalName: host/ldaptest2computer
+servicePrincipalName: cifs/ldaptest2computer
+""")
+ try:
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+add: servicePrincipalName
+servicePrincipalName: host/ldaptest2computer
+""")
+ self.fail()
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ # Testing ranged results
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+replace: servicePrincipalName
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+add: servicePrincipalName
+servicePrincipalName: host/ldaptest2computer0
+servicePrincipalName: host/ldaptest2computer1
+servicePrincipalName: host/ldaptest2computer2
+servicePrincipalName: host/ldaptest2computer3
+servicePrincipalName: host/ldaptest2computer4
+servicePrincipalName: host/ldaptest2computer5
+servicePrincipalName: host/ldaptest2computer6
+servicePrincipalName: host/ldaptest2computer7
+servicePrincipalName: host/ldaptest2computer8
+servicePrincipalName: host/ldaptest2computer9
+servicePrincipalName: host/ldaptest2computer10
+servicePrincipalName: host/ldaptest2computer11
+servicePrincipalName: host/ldaptest2computer12
+servicePrincipalName: host/ldaptest2computer13
+servicePrincipalName: host/ldaptest2computer14
+servicePrincipalName: host/ldaptest2computer15
+servicePrincipalName: host/ldaptest2computer16
+servicePrincipalName: host/ldaptest2computer17
+servicePrincipalName: host/ldaptest2computer18
+servicePrincipalName: host/ldaptest2computer19
+servicePrincipalName: host/ldaptest2computer20
+servicePrincipalName: host/ldaptest2computer21
+servicePrincipalName: host/ldaptest2computer22
+servicePrincipalName: host/ldaptest2computer23
+servicePrincipalName: host/ldaptest2computer24
+servicePrincipalName: host/ldaptest2computer25
+servicePrincipalName: host/ldaptest2computer26
+servicePrincipalName: host/ldaptest2computer27
+servicePrincipalName: host/ldaptest2computer28
+servicePrincipalName: host/ldaptest2computer29
+""")
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE,
+ attrs=["servicePrincipalName;range=0-*"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=0-*"]), 30)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=0-19"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=0-19"]), 20)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=0-30"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=0-*"]), 30)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=0-40"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=0-*"]), 30)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=30-40"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=30-*"]), 0)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=10-40"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=10-*"]), 20)
+ # pos_11 = res[0]["servicePrincipalName;range=10-*"][18]
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=11-40"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=11-*"]), 19)
+ # self.assertEqual((res[0]["servicePrincipalName;range=11-*"][18]), pos_11)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=11-15"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=11-15"]), 5)
+ # self.assertEqual(res[0]["servicePrincipalName;range=11-15"][4], pos_11)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName"]), 30)
+ # self.assertEqual(res[0]["servicePrincipalName"][18], pos_11)
+
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ ldb.add({
+ "dn": "cn=ldaptestuser2,cn=useRs," + self.base_dn,
+ "objectClass": "user",
+ "cn": "LDAPtestUSER2",
+ "givenname": "testy",
+ "sn": "ldap user2"})
+
+ # Testing Ambiguous Name Resolution
+ # Testing ldb.search for (&(anr=ldap testy)(objectClass=user))
+ res = ldb.search(expression="(&(anr=ldap testy)(objectClass=user))")
+ self.assertEqual(len(res), 3, "Found only %d of 3 for (&(anr=ldap testy)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr=testy ldap)(objectClass=user))
+ res = ldb.search(expression="(&(anr=testy ldap)(objectClass=user))")
+ self.assertEqual(len(res), 2, "Found only %d of 2 for (&(anr=testy ldap)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr=ldap)(objectClass=user))
+ res = ldb.search(expression="(&(anr=ldap)(objectClass=user))")
+ self.assertEqual(len(res), 4, "Found only %d of 4 for (&(anr=ldap)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr==ldap)(objectClass=user))
+ res = ldb.search(expression="(&(anr==ldap)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(anr==ldap)(objectClass=user)). Found only %d for (&(anr=ldap)(objectClass=user))" % len(res))
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"][0]), "ldaptestuser")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser")
+
+ # Testing ldb.search for (&(anr=testy)(objectClass=user))
+ res = ldb.search(expression="(&(anr=testy)(objectClass=user))")
+ self.assertEqual(len(res), 2, "Found only %d for (&(anr=testy)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr=testy ldap)(objectClass=user))
+ res = ldb.search(expression="(&(anr=testy ldap)(objectClass=user))")
+ self.assertEqual(len(res), 2, "Found only %d for (&(anr=testy ldap)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr==testy ldap)(objectClass=user))
+# this test disabled for the moment, as anr with == tests are not understood
+# res = ldb.search(expression="(&(anr==testy ldap)(objectClass=user))")
+# self.assertEqual(len(res), 1, "Found only %d for (&(anr==testy ldap)(objectClass=user))" % len(res))
+
+# self.assertEqual(str(res[0].dn), ("CN=ldaptestuser,CN=Users," + self.base_dn))
+# self.assertEqual(res[0]["cn"][0], "ldaptestuser")
+# self.assertEqual(res[0]["name"][0], "ldaptestuser")
+
+ # Testing ldb.search for (&(anr==testy ldap)(objectClass=user))
+# res = ldb.search(expression="(&(anr==testy ldap)(objectClass=user))")
+# self.assertEqual(len(res), 1, "Could not find (&(anr==testy ldap)(objectClass=user))")
+
+# self.assertEqual(str(res[0].dn), ("CN=ldaptestuser,CN=Users," + self.base_dn))
+# self.assertEqual(res[0]["cn"][0], "ldaptestuser")
+# self.assertEqual(res[0]["name"][0], "ldaptestuser")
+
+ # Testing ldb.search for (&(anr=testy ldap user)(objectClass=user))
+ res = ldb.search(expression="(&(anr=testy ldap user)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(anr=testy ldap user)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestuser2")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser2")
+
+ # Testing ldb.search for (&(anr==testy ldap user2)(objectClass=user))
+# res = ldb.search(expression="(&(anr==testy ldap user2)(objectClass=user))")
+# self.assertEqual(len(res), 1, "Could not find (&(anr==testy ldap user2)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestuser2")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser2")
+
+ # Testing ldb.search for (&(anr==ldap user2)(objectClass=user))
+# res = ldb.search(expression="(&(anr==ldap user2)(objectClass=user))")
+# self.assertEqual(len(res), 1, "Could not find (&(anr==ldap user2)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestuser2")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser2")
+
+ # Testing ldb.search for (&(anr==not ldap user2)(objectClass=user))
+# res = ldb.search(expression="(&(anr==not ldap user2)(objectClass=user))")
+# self.assertEqual(len(res), 0, "Must not find (&(anr==not ldap user2)(objectClass=user))")
+
+ # Testing ldb.search for (&(anr=not ldap user2)(objectClass=user))
+ res = ldb.search(expression="(&(anr=not ldap user2)(objectClass=user))")
+ self.assertEqual(len(res), 0, "Must not find (&(anr=not ldap user2)(objectClass=user))")
+
+ # Testing ldb.search for (&(anr="testy ldap")(objectClass=user)) (ie, with quotes)
+# res = ldb.search(expression="(&(anr==\"testy ldap\")(objectClass=user))")
+# self.assertEqual(len(res), 0, "Found (&(anr==\"testy ldap\")(objectClass=user))")
+
+ # Testing Renames
+
+ attrs = ["objectGUID", "objectSid"]
+ # Testing ldb.search for (&(cn=ldaptestUSer2)(objectClass=user))
+ res_user = ldb.search(self.base_dn, expression="(&(cn=ldaptestUSer2)(objectClass=user))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res_user), 1, "Could not find (&(cn=ldaptestUSer2)(objectClass=user))")
+
+ # Check rename works with extended/alternate DN forms
+ ldb.rename("<SID=" + get_string(ldb.schema_format_value("objectSID", res_user[0]["objectSID"][0])) + ">", "cn=ldaptestUSER3,cn=users," + self.base_dn)
+
+ # Testing ldb.search for (&(cn=ldaptestuser3)(objectClass=user))
+ res = ldb.search(expression="(&(cn=ldaptestuser3)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser3)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestUSER3,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestUSER3")
+ self.assertEqual(str(res[0]["name"]), "ldaptestUSER3")
+
+ #"Testing ldb.search for (&(&(cn=ldaptestuser3)(userAccountControl=*))(objectClass=user))"
+ res = ldb.search(expression="(&(&(cn=ldaptestuser3)(userAccountControl=*))(objectClass=user))")
+ self.assertEqual(len(res), 1, "(&(&(cn=ldaptestuser3)(userAccountControl=*))(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestUSER3,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestUSER3")
+ self.assertEqual(str(res[0]["name"]), "ldaptestUSER3")
+
+ #"Testing ldb.search for (&(&(cn=ldaptestuser3)(userAccountControl=546))(objectClass=user))"
+ res = ldb.search(expression="(&(&(cn=ldaptestuser3)(userAccountControl=546))(objectClass=user))")
+ self.assertEqual(len(res), 1, "(&(&(cn=ldaptestuser3)(userAccountControl=546))(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestUSER3,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestUSER3")
+ self.assertEqual(str(res[0]["name"]), "ldaptestUSER3")
+
+ #"Testing ldb.search for (&(&(cn=ldaptestuser3)(userAccountControl=547))(objectClass=user))"
+ res = ldb.search(expression="(&(&(cn=ldaptestuser3)(userAccountControl=547))(objectClass=user))")
+ self.assertEqual(len(res), 0, "(&(&(cn=ldaptestuser3)(userAccountControl=547))(objectClass=user))")
+
+ # Testing ldb.search for (dn=CN=ldaptestUSER3,CN=Users," + self.base_dn + ") - should not work
+ res = ldb.search(expression="(dn=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")")
+ self.assertEqual(len(res), 0, "Could find (dn=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")")
+
+ # Testing ldb.search for (distinguishedName=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")
+ res = ldb.search(expression="(distinguishedName=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")")
+ self.assertEqual(len(res), 1, "Could not find (distinguishedName=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")")
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestUSER3,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestUSER3")
+ self.assertEqual(str(res[0]["name"]), "ldaptestUSER3")
+
+ # ensure we cannot add it again
+ try:
+ ldb.add({"dn": "cn=ldaptestuser3,cn=userS," + self.base_dn,
+ "objectClass": "user",
+ "cn": "LDAPtestUSER3"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # rename back
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestuser2,cn=users," + self.base_dn)
+
+ # ensure we cannot rename it twice
+ try:
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn,
+ "cn=ldaptestuser2,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # ensure can now use that name
+ ldb.add({"dn": "cn=ldaptestuser3,cn=users," + self.base_dn,
+ "objectClass": "user",
+ "cn": "LDAPtestUSER3"})
+
+ # ensure we now cannot rename
+ try:
+ ldb.rename("cn=ldaptestuser2,cn=users," + self.base_dn, "cn=ldaptestuser3,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+ try:
+ ldb.rename("cn=ldaptestuser3,cn=users,%s" % self.base_dn, "cn=ldaptestuser3,%s" % ldb.get_config_basedn())
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertTrue(num in (71, 64))
+
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestuser5,cn=users," + self.base_dn)
+
+ ldb.delete("cn=ldaptestuser5,cn=users," + self.base_dn)
+
+ delete_force(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+
+ ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+
+ # Testing subtree renames
+
+ ldb.add({"dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectClass": "container"})
+
+ ldb.add({"dn": "CN=ldaptestuser4,CN=ldaptestcontainer," + self.base_dn,
+ "objectClass": "user",
+ "cn": "LDAPtestUSER4"})
+
+ # Here we don't enforce these hard "description" constraints
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+description: desc1
+description: desc2
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+add: member
+member: cn=ldaptestuser4,cn=ldaptestcontainer,""" + self.base_dn + """
+member: cn=ldaptestcomputer,cn=computers,""" + self.base_dn + """
+member: cn=ldaptestuser2,cn=users,""" + self.base_dn + """
+""")
+
+ # Testing ldb.rename of cn=ldaptestcontainer," + self.base_dn + " to cn=ldaptestcontainer2," + self.base_dn
+ ldb.rename("CN=ldaptestcontainer," + self.base_dn, "CN=ldaptestcontainer2," + self.base_dn)
+
+ # Testing ldb.search for (&(cn=ldaptestuser4)(objectClass=user))
+ res = ldb.search(expression="(&(cn=ldaptestuser4)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser4)(objectClass=user))")
+
+ # Testing subtree ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in (just renamed from) cn=ldaptestcontainer," + self.base_dn
+ try:
+ res = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ expression="(&(cn=ldaptestuser4)(objectClass=user))",
+ scope=SCOPE_SUBTREE)
+ self.fail(res)
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Testing one-level ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in (just renamed from) cn=ldaptestcontainer," + self.base_dn
+ try:
+ res = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ expression="(&(cn=ldaptestuser4)(objectClass=user))", scope=SCOPE_ONELEVEL)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Testing ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in renamed container"
+ res = ldb.search("cn=ldaptestcontainer2," + self.base_dn, expression="(&(cn=ldaptestuser4)(objectClass=user))", scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser4)(objectClass=user)) under cn=ldaptestcontainer2," + self.base_dn)
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn))
+ self.assertEqual(str(res[0]["memberOf"][0]).upper(), ("CN=ldaptestgroup2,CN=Users," + self.base_dn).upper())
+
+ time.sleep(4)
+
+ # Testing ldb.search for (&(member=CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn + ")(objectclass=group)) to check subtree renames and linked attributes"
+ res = ldb.search(self.base_dn, expression="(&(member=CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn + ")(objectclass=group))", scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1, "Could not find (&(member=CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn + ")(objectclass=group)), perhaps linked attributes are not consistent with subtree renames?")
+
+ # Testing ldb.rename (into itself) of cn=ldaptestcontainer2," + self.base_dn + " to cn=ldaptestcontainer,cn=ldaptestcontainer2," + self.base_dn
+ try:
+ ldb.rename("cn=ldaptestcontainer2," + self.base_dn, "cn=ldaptestcontainer,cn=ldaptestcontainer2," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Testing ldb.rename (into non-existent container) of cn=ldaptestcontainer2," + self.base_dn + " to cn=ldaptestcontainer,cn=ldaptestcontainer3," + self.base_dn
+ try:
+ ldb.rename("cn=ldaptestcontainer2," + self.base_dn, "cn=ldaptestcontainer,cn=ldaptestcontainer3," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertTrue(num in (ERR_UNWILLING_TO_PERFORM, ERR_OTHER))
+
+ # Testing delete (should fail, not a leaf node) of renamed cn=ldaptestcontainer2," + self.base_dn
+ try:
+ ldb.delete("cn=ldaptestcontainer2," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+
+ # Testing base ldb.search for CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn
+ res = ldb.search(expression="(objectclass=*)", base=("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn), scope=SCOPE_BASE)
+ self.assertEqual(len(res), 1)
+ res = ldb.search(expression="(cn=ldaptestuser40)", base=("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn), scope=SCOPE_BASE)
+ self.assertEqual(len(res), 0)
+
+ # Testing one-level ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in cn=ldaptestcontainer2," + self.base_dn
+ res = ldb.search(expression="(&(cn=ldaptestuser4)(objectClass=user))", base=("cn=ldaptestcontainer2," + self.base_dn), scope=SCOPE_ONELEVEL)
+ self.assertEqual(len(res), 1)
+
+ # Testing one-level ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in cn=ldaptestcontainer2," + self.base_dn
+ res = ldb.search(expression="(&(cn=ldaptestuser4)(objectClass=user))", base=("cn=ldaptestcontainer2," + self.base_dn), scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+
+ # Testing delete of subtree renamed "+("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn)
+ ldb.delete(("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn))
+ # Testing delete of renamed cn=ldaptestcontainer2," + self.base_dn
+ ldb.delete("cn=ldaptestcontainer2," + self.base_dn)
+
+ ldb.add({"dn": "cn=ldaptestutf8user èùéìòà,cn=users," + self.base_dn, "objectClass": "user"})
+
+ ldb.add({"dn": "cn=ldaptestutf8user2 èùéìòà,cn=users," + self.base_dn, "objectClass": "user"})
+
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestuser)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestuser")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser")
+ self.assertEqual(set(res[0]["objectClass"]), set([b"top", b"person", b"organizationalPerson", b"user"]))
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertEqual(str(res[0]["objectCategory"]), ("CN=Person,%s" % ldb.get_schema_basedn()))
+ self.assertEqual(int(res[0]["sAMAccountType"][0]), ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE)
+ self.assertEqual(str(res[0]["memberOf"][0]).upper(), ("CN=ldaptestgroup2,CN=Users," + self.base_dn).upper())
+ self.assertEqual(len(res[0]["memberOf"]), 1)
+
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectCategory=cn=person,%s))" % ldb.get_schema_basedn()
+ res2 = ldb.search(expression="(&(cn=ldaptestuser)(objectCategory=cn=person,%s))" % ldb.get_schema_basedn())
+ self.assertEqual(len(res2), 1, "Could not find (&(cn=ldaptestuser)(objectCategory=cn=person,%s))" % ldb.get_schema_basedn())
+
+ self.assertEqual(res[0].dn, res2[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectCategory=PerSon))"
+ res3 = ldb.search(expression="(&(cn=ldaptestuser)(objectCategory=PerSon))")
+ self.assertEqual(len(res3), 1, "Could not find (&(cn=ldaptestuser)(objectCategory=PerSon)): matched %d" % len(res3))
+
+ self.assertEqual(res[0].dn, res3[0].dn)
+
+ if gc_ldb is not None:
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectCategory=PerSon)) in Global Catalog"
+ res3gc = gc_ldb.search(expression="(&(cn=ldaptestuser)(objectCategory=PerSon))")
+ self.assertEqual(len(res3gc), 1)
+
+ self.assertEqual(res[0].dn, res3gc[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectCategory=PerSon)) in with 'phantom root' control"
+
+ if gc_ldb is not None:
+ res3control = gc_ldb.search(self.base_dn, expression="(&(cn=ldaptestuser)(objectCategory=PerSon))", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["search_options:1:2"])
+ self.assertEqual(len(res3control), 1, "Could not find (&(cn=ldaptestuser)(objectCategory=PerSon)) in Global Catalog")
+
+ self.assertEqual(res[0].dn, res3control[0].dn)
+
+ ldb.delete(res[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestcomputer)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestcomputer,CN=Computers," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestcomputer")
+ self.assertEqual(str(res[0]["name"]), "ldaptestcomputer")
+ self.assertEqual(set(res[0]["objectClass"]), set([b"top", b"person", b"organizationalPerson", b"user", b"computer"]))
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertEqual(str(res[0]["objectCategory"]), ("CN=Computer,%s" % ldb.get_schema_basedn()))
+ self.assertEqual(int(res[0]["primaryGroupID"][0]), DOMAIN_RID_DOMAIN_MEMBERS)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]), ATYPE_WORKSTATION_TRUST)
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_WORKSTATION_TRUST_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE)
+ self.assertEqual(str(res[0]["memberOf"][0]).upper(), ("CN=ldaptestgroup2,CN=Users," + self.base_dn).upper())
+ self.assertEqual(len(res[0]["memberOf"]), 1)
+
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s))" % ldb.get_schema_basedn()
+ res2 = ldb.search(expression="(&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s))" % ldb.get_schema_basedn())
+ self.assertEqual(len(res2), 1, "Could not find (&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s))" % ldb.get_schema_basedn())
+
+ self.assertEqual(res[0].dn, res2[0].dn)
+
+ if gc_ldb is not None:
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s)) in Global Catalog" % gc_ldb.get_schema_basedn()
+ res2gc = gc_ldb.search(expression="(&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s))" % gc_ldb.get_schema_basedn())
+ self.assertEqual(len(res2gc), 1, "Could not find (&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s)) In Global Catalog" % gc_ldb.get_schema_basedn())
+
+ self.assertEqual(res[0].dn, res2gc[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectCategory=compuTER))"
+ res3 = ldb.search(expression="(&(cn=ldaptestcomputer)(objectCategory=compuTER))")
+ self.assertEqual(len(res3), 1, "Could not find (&(cn=ldaptestcomputer)(objectCategory=compuTER))")
+
+ self.assertEqual(res[0].dn, res3[0].dn)
+
+ if gc_ldb is not None:
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectCategory=compuTER)) in Global Catalog"
+ res3gc = gc_ldb.search(expression="(&(cn=ldaptestcomputer)(objectCategory=compuTER))")
+ self.assertEqual(len(res3gc), 1, "Could not find (&(cn=ldaptestcomputer)(objectCategory=compuTER)) in Global Catalog")
+
+ self.assertEqual(res[0].dn, res3gc[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestcomp*r)(objectCategory=compuTER))"
+ res4 = ldb.search(expression="(&(cn=ldaptestcomp*r)(objectCategory=compuTER))")
+ self.assertEqual(len(res4), 1, "Could not find (&(cn=ldaptestcomp*r)(objectCategory=compuTER))")
+
+ self.assertEqual(res[0].dn, res4[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestcomput*)(objectCategory=compuTER))"
+ res5 = ldb.search(expression="(&(cn=ldaptestcomput*)(objectCategory=compuTER))")
+ self.assertEqual(len(res5), 1, "Could not find (&(cn=ldaptestcomput*)(objectCategory=compuTER))")
+
+ self.assertEqual(res[0].dn, res5[0].dn)
+
+ # Testing ldb.search for (&(cn=*daptestcomputer)(objectCategory=compuTER))"
+ res6 = ldb.search(expression="(&(cn=*daptestcomputer)(objectCategory=compuTER))")
+ self.assertEqual(len(res6), 1, "Could not find (&(cn=*daptestcomputer)(objectCategory=compuTER))")
+
+ self.assertEqual(res[0].dn, res6[0].dn)
+
+ ldb.delete("<GUID=" + get_string(ldb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + ">")
+
+ # Testing ldb.search for (&(cn=ldaptest2computer)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptest2computer)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptest2computer)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), "CN=ldaptest2computer,CN=Computers," + self.base_dn)
+ self.assertEqual(str(res[0]["cn"]), "ldaptest2computer")
+ self.assertEqual(str(res[0]["name"]), "ldaptest2computer")
+ self.assertEqual(list(res[0]["objectClass"]), [b"top", b"person", b"organizationalPerson", b"user", b"computer"])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertEqual(str(res[0]["objectCategory"][0]), "CN=Computer,%s" % ldb.get_schema_basedn())
+ self.assertEqual(int(res[0]["sAMAccountType"][0]), ATYPE_WORKSTATION_TRUST)
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_WORKSTATION_TRUST_ACCOUNT)
+
+ ldb.delete("<SID=" + get_string(ldb.schema_format_value("objectSID", res[0]["objectSID"][0])) + ">")
+
+ attrs = ["cn", "name", "objectClass", "objectGUID", "objectSID", "whenCreated", "nTSecurityDescriptor", "memberOf", "allowedAttributes", "allowedAttributesEffective"]
+ # Testing ldb.search for (&(cn=ldaptestUSer2)(objectClass=user))"
+ res_user = ldb.search(self.base_dn, expression="(&(cn=ldaptestUSer2)(objectClass=user))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res_user), 1, "Could not find (&(cn=ldaptestUSer2)(objectClass=user))")
+
+ self.assertEqual(str(res_user[0].dn), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res_user[0]["cn"]), "ldaptestuser2")
+ self.assertEqual(str(res_user[0]["name"]), "ldaptestuser2")
+ self.assertEqual(list(res_user[0]["objectClass"]), [b"top", b"person", b"organizationalPerson", b"user"])
+ self.assertTrue("objectSid" in res_user[0])
+ self.assertTrue("objectGUID" in res_user[0])
+ self.assertTrue("whenCreated" in res_user[0])
+ self.assertTrue("nTSecurityDescriptor" in res_user[0])
+ self.assertTrue("allowedAttributes" in res_user[0])
+ self.assertTrue("allowedAttributesEffective" in res_user[0])
+ self.assertEqual(str(res_user[0]["memberOf"][0]).upper(), ("CN=ldaptestgroup2,CN=Users," + self.base_dn).upper())
+
+ ldaptestuser2_sid = res_user[0]["objectSid"][0]
+ ldaptestuser2_guid = res_user[0]["objectGUID"][0]
+
+ attrs = ["cn", "name", "objectClass", "objectGUID", "objectSID", "whenCreated", "nTSecurityDescriptor", "member", "allowedAttributes", "allowedAttributesEffective"]
+ # Testing ldb.search for (&(cn=ldaptestgroup2)(objectClass=group))"
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestgroup2)(objectClass=group))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestgroup2)(objectClass=group))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestgroup2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestgroup2")
+ self.assertEqual(str(res[0]["name"]), "ldaptestgroup2")
+ self.assertEqual(list(res[0]["objectClass"]), [b"top", b"group"])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("objectSid" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ self.assertTrue("allowedAttributes" in res[0])
+ self.assertTrue("allowedAttributesEffective" in res[0])
+ memberUP = []
+ for m in res[0]["member"]:
+ memberUP.append(str(m).upper())
+ self.assertTrue(("CN=ldaptestuser2,CN=Users," + self.base_dn).upper() in memberUP)
+
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestgroup2)(objectClass=group))", scope=SCOPE_SUBTREE, attrs=attrs, controls=["extended_dn:1:1"])
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestgroup2)(objectClass=group))")
+
+ print(res[0]["member"])
+ memberUP = []
+ for m in res[0]["member"]:
+ memberUP.append(str(m).upper())
+ print(("<GUID=" + get_string(ldb.schema_format_value("objectGUID", ldaptestuser2_guid)) + ">;<SID=" + get_string(ldb.schema_format_value("objectSid", ldaptestuser2_sid)) + ">;CN=ldaptestuser2,CN=Users," + self.base_dn).upper())
+
+ self.assertTrue(("<GUID=" + get_string(ldb.schema_format_value("objectGUID", ldaptestuser2_guid)) + ">;<SID=" + get_string(ldb.schema_format_value("objectSid", ldaptestuser2_sid)) + ">;CN=ldaptestuser2,CN=Users," + self.base_dn).upper() in memberUP)
+
+ # Quicktest for linked attributes"
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+replace: member
+member: CN=ldaptestuser2,CN=Users,""" + self.base_dn + """
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ ldb.modify_ldif("""
+dn: <GUID=""" + get_string(ldb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + """>
+changetype: modify
+replace: member
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ ldb.modify_ldif("""
+dn: <SID=""" + get_string(ldb.schema_format_value("objectSid", res[0]["objectSid"][0])) + """>
+changetype: modify
+delete: member
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+add: member
+member: <GUID=""" + get_string(ldb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + """>
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+replace: member
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+add: member
+member: <SID=""" + get_string(ldb.schema_format_value("objectSid", res_user[0]["objectSid"][0])) + """>
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: member
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestgroup2)(objectClass=group))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestgroup2)(objectClass=group))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestgroup2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["member"][0]), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(len(res[0]["member"]), 1)
+
+ ldb.delete(("CN=ldaptestuser2,CN=Users," + self.base_dn))
+
+ time.sleep(4)
+
+ attrs = ["cn", "name", "objectClass", "objectGUID", "whenCreated", "nTSecurityDescriptor", "member"]
+ # Testing ldb.search for (&(cn=ldaptestgroup2)(objectClass=group)) to check linked delete"
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestgroup2)(objectClass=group))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestgroup2)(objectClass=group)) to check linked delete")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestgroup2,CN=Users," + self.base_dn))
+ self.assertTrue("member" not in res[0])
+
+ # Testing ldb.search for (&(cn=ldaptestutf8user ÈÙÉÌÒÀ)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestutf8user ÈÙÉÌÒÀ)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestutf8user ÈÙÉÌÒÀ)(objectClass=user))")
+ res = ldb.search(expression="(&(cn=ldaptestutf8user èùéìòà)(objectclass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestutf8user ÈÙÉÌÒÀ)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestutf8user èùéìòà,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestutf8user èùéìòà")
+ self.assertEqual(str(res[0]["name"]), "ldaptestutf8user èùéìòà")
+ self.assertEqual(list(res[0]["objectClass"]), [b"top", b"person", b"organizationalPerson", b"user"])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+
+ # delete "ldaptestutf8user"
+ ldb.delete(res[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestutf8user2*)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestutf8user2*)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestutf8user2*)(objectClass=user))")
+
+ # Testing ldb.search for (&(cn=ldaptestutf8user2 ÈÙÉÌÒÀ)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestutf8user2 ÈÙÉÌÒÀ)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestutf8user2 ÈÙÉÌÒÀ)(objectClass=user))")
+
+ # delete "ldaptestutf8user2 "
+ ldb.delete(res[0].dn)
+
+ ldb.delete(("CN=ldaptestgroup2,CN=Users," + self.base_dn))
+
+ # Testing that we can't get at the configuration DN from the main search base"
+ res = ldb.search(self.base_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertEqual(len(res), 0)
+
+ # Testing that we can get at the configuration DN from the main search base on the LDAP port with the 'phantom root' search_options control"
+ res = ldb.search(self.base_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["search_options:1:2"])
+ self.assertTrue(len(res) > 0)
+
+ if gc_ldb is not None:
+ # Testing that we can get at the configuration DN from the main search base on the GC port with the search_options control == 0"
+
+ res = gc_ldb.search(self.base_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["search_options:1:0"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing that we do find configuration elements in the global catlog"
+ res = gc_ldb.search(self.base_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing that we do find configuration elements and user elements at the same time"
+ res = gc_ldb.search(self.base_dn, expression="(|(objectClass=crossRef)(objectClass=person))", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing that we do find configuration elements in the global catlog, with the configuration basedn"
+ res = gc_ldb.search(self.configuration_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing that we can get at the configuration DN on the main LDAP port"
+ res = ldb.search(self.configuration_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing objectCategory canonacolisation"
+ res = ldb.search(self.configuration_dn, expression="objectCategory=ntDsDSA", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0, "Didn't find any records with objectCategory=ntDsDSA")
+ self.assertTrue(len(res) != 0)
+
+ res = ldb.search(self.configuration_dn, expression="objectCategory=CN=ntDs-DSA," + self.schema_dn, scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0, "Didn't find any records with objectCategory=CN=ntDs-DSA," + self.schema_dn)
+ self.assertTrue(len(res) != 0)
+
+ # Testing objectClass attribute order on "+ self.base_dn
+ res = ldb.search(expression="objectClass=domain", base=self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectClass"])
+ self.assertEqual(len(res), 1)
+
+ self.assertEqual(list(res[0]["objectClass"]), [b"top", b"domain", b"domainDNS"])
+
+ # check enumeration
+
+ # Testing ldb.search for objectCategory=person"
+ res = ldb.search(self.base_dn, expression="objectCategory=person", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=person with domain scope control"
+ res = ldb.search(self.base_dn, expression="objectCategory=person", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["domain_scope:1"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=user"
+ res = ldb.search(self.base_dn, expression="objectCategory=user", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=user with domain scope control"
+ res = ldb.search(self.base_dn, expression="objectCategory=user", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["domain_scope:1"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=group"
+ res = ldb.search(self.base_dn, expression="objectCategory=group", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=group with domain scope control"
+ res = ldb.search(self.base_dn, expression="objectCategory=group", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["domain_scope:1"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing creating a user with the posixAccount objectClass"
+ self.ldb.add_ldif("""dn: cn=posixuser,CN=Users,%s
+objectClass: top
+objectClass: person
+objectClass: posixAccount
+objectClass: user
+objectClass: organizationalPerson
+cn: posixuser
+uid: posixuser
+sn: posixuser
+uidNumber: 10126
+gidNumber: 10126
+homeDirectory: /home/posixuser
+loginShell: /bin/bash
+gecos: Posix User;;;
+description: A POSIX user""" % (self.base_dn))
+
+ # Testing removing the posixAccount objectClass from an existing user"
+ self.ldb.modify_ldif("""dn: cn=posixuser,CN=Users,%s
+changetype: modify
+delete: objectClass
+objectClass: posixAccount""" % (self.base_dn))
+
+ # Testing adding the posixAccount objectClass to an existing user"
+ self.ldb.modify_ldif("""dn: cn=posixuser,CN=Users,%s
+changetype: modify
+add: objectClass
+objectClass: posixAccount""" % (self.base_dn))
+
+ delete_force(self.ldb, "cn=posixuser,cn=users," + 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, "cn=ldaptestuser3,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser4,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser4,cn=ldaptestcontainer2," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser5,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)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptest2computer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer3,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestutf8user èùéìòà,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestutf8user2 èùéìòà,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer2," + self.base_dn)
+
+ def test_security_descriptor_add(self):
+ """ Testing ldb.add_ldif() for nTSecurityDescriptor """
+ user_name = "testdescriptoruser1"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+ #
+ # Test an empty security descriptor (naturally this shouldn't work)
+ #
+ delete_force(self.ldb, user_dn)
+ try:
+ self.ldb.add({"dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name,
+ "nTSecurityDescriptor": []})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test add_ldif() with SDDL security descriptor input
+ #
+ try:
+ sddl = "O:DUG:DUD:PAI(A;;RPWP;;;AU)S:PAI"
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name + """
+nTSecurityDescriptor: """ + sddl)
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test add_ldif() with BASE64 security descriptor
+ #
+ try:
+ sddl = "O:DUG:DUD:PAI(A;;RPWP;;;AU)S:PAI"
+ desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ desc_binary = ndr_pack(desc)
+ desc_base64 = base64.b64encode(desc_binary).decode('utf8')
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name + """
+nTSecurityDescriptor:: """ + desc_base64)
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+
+ def test_security_descriptor_add_neg(self):
+ """Test add_ldif() with BASE64 security descriptor input using WRONG domain SID
+ Negative test
+ """
+ user_name = "testdescriptoruser1"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+ delete_force(self.ldb, user_dn)
+ try:
+ sddl = "O:DUG:DUD:AI(A;;RPWP;;;AU)S:PAI"
+ desc = security.descriptor.from_sddl(sddl, security.dom_sid('S-1-5-21'))
+ desc_base64 = base64.b64encode(ndr_pack(desc)).decode('utf8')
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name + """
+nTSecurityDescriptor:: """ + desc_base64)
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertTrue("O:S-1-5-21-513G:S-1-5-21-513D:AI(A;;RPWP;;;AU)" in desc_sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+
+ def test_security_descriptor_modify(self):
+ """ Testing ldb.modify_ldif() for nTSecurityDescriptor """
+ user_name = "testdescriptoruser2"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+ #
+ # Test an empty security descriptor (naturally this shouldn't work)
+ #
+ delete_force(self.ldb, user_dn)
+ self.ldb.add({"dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name})
+
+ m = Message()
+ m.dn = Dn(ldb, user_dn)
+ m["nTSecurityDescriptor"] = MessageElement([], FLAG_MOD_ADD,
+ "nTSecurityDescriptor")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, user_dn)
+ m["nTSecurityDescriptor"] = MessageElement([], FLAG_MOD_REPLACE,
+ "nTSecurityDescriptor")
+ try:
+ self.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, user_dn)
+ m["nTSecurityDescriptor"] = MessageElement([], FLAG_MOD_DELETE,
+ "nTSecurityDescriptor")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, user_dn)
+ #
+ # Test modify_ldif() with SDDL security descriptor input
+ # Add ACE to the original descriptor test
+ #
+ try:
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name)
+ # Modify descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ sddl = desc_sddl[:desc_sddl.find("(")] + "(A;;RPWP;;;AU)" + desc_sddl[desc_sddl.find("("):]
+ mod = """
+dn: """ + user_dn + """
+changetype: modify
+replace: nTSecurityDescriptor
+nTSecurityDescriptor: """ + sddl
+ self.ldb.modify_ldif(mod)
+ # Read modified descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test modify_ldif() with SDDL security descriptor input
+ # New descriptor test
+ #
+ try:
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name)
+ # Modify descriptor
+ sddl = "O:DUG:DUD:PAI(A;;RPWP;;;AU)S:PAI"
+ mod = """
+dn: """ + user_dn + """
+changetype: modify
+replace: nTSecurityDescriptor
+nTSecurityDescriptor: """ + sddl
+ self.ldb.modify_ldif(mod)
+ # Read modified descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test modify_ldif() with BASE64 security descriptor input
+ # Add ACE to the original descriptor test
+ #
+ try:
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name)
+ # Modify descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ sddl = desc_sddl[:desc_sddl.find("(")] + "(A;;RPWP;;;AU)" + desc_sddl[desc_sddl.find("("):]
+ desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ desc_base64 = base64.b64encode(ndr_pack(desc)).decode('utf8')
+ mod = """
+dn: """ + user_dn + """
+changetype: modify
+replace: nTSecurityDescriptor
+nTSecurityDescriptor:: """ + desc_base64
+ self.ldb.modify_ldif(mod)
+ # Read modified descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test modify_ldif() with BASE64 security descriptor input
+ # New descriptor test
+ #
+ try:
+ delete_force(self.ldb, user_dn)
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name)
+ # Modify descriptor
+ sddl = "O:DUG:DUD:PAI(A;;RPWP;;;AU)S:PAI"
+ desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ desc_base64 = base64.b64encode(ndr_pack(desc)).decode('utf8')
+ mod = """
+dn: """ + user_dn + """
+changetype: modify
+replace: nTSecurityDescriptor
+nTSecurityDescriptor:: """ + desc_base64
+ self.ldb.modify_ldif(mod)
+ # Read modified descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+
+ def test_dsheuristics(self):
+ """Tests the 'dSHeuristics' attribute"""
+ # Tests the 'dSHeuristics' attribute"
+
+ # Get the current value to restore it later
+ dsheuristics = self.ldb.get_dsheuristics()
+ # Perform the length checks: for each decade (except the 0th) we need
+ # the first index to be the number. This goes till the 9th one, beyond
+ # there does not seem to be another limitation.
+ try:
+ dshstr = ""
+ for i in range(1, 11):
+ # This is in the range
+ self.ldb.set_dsheuristics(dshstr + "x")
+ self.ldb.set_dsheuristics(dshstr + "xxxxx")
+ dshstr = dshstr + "xxxxxxxxx"
+ if i < 10:
+ # Not anymore in the range, new decade specifier needed
+ try:
+ self.ldb.set_dsheuristics(dshstr + "x")
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ dshstr = dshstr + str(i)
+ else:
+ # There does not seem to be an upper limit
+ self.ldb.set_dsheuristics(dshstr + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
+ # apart from the above, all char values are accepted
+ self.ldb.set_dsheuristics("123ABC-+!1asdfg@#^")
+ self.assertEqual(self.ldb.get_dsheuristics(), b"123ABC-+!1asdfg@#^")
+ finally:
+ # restore old value
+ self.ldb.set_dsheuristics(dsheuristics)
+
+ def test_ldapControlReturn(self):
+ """Testing that if we request a control that return a control it
+ really return something"""
+ res = self.ldb.search(attrs=["cn"],
+ controls=["paged_results:1:10"])
+ self.assertEqual(len(res.controls), 1)
+ self.assertEqual(res.controls[0].oid, "1.2.840.113556.1.4.319")
+ s = str(res.controls[0])
+
+ def test_operational(self):
+ """Tests operational attributes"""
+ # Tests operational attributes"
+
+ res = self.ldb.search(self.base_dn, scope=SCOPE_BASE,
+ attrs=["createTimeStamp", "modifyTimeStamp",
+ "structuralObjectClass", "whenCreated",
+ "whenChanged"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("createTimeStamp" in res[0])
+ self.assertTrue("modifyTimeStamp" in res[0])
+ self.assertTrue("structuralObjectClass" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertTrue("whenChanged" in res[0])
+
+ def test_timevalues1(self):
+ """Tests possible syntax of time attributes"""
+
+ user_name = "testtimevaluesuser1"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+
+ delete_force(self.ldb, user_dn)
+ self.ldb.add({"dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name})
+
+ #
+ # We check the following values:
+ #
+ # 370101000000Z => 20370101000000.0Z
+ # 20370102000000.*Z => 20370102000000.0Z
+ #
+ ext = ["Z", ".0Z", ".Z", ".000Z", ".RandomIgnoredCharacters...987654321Z"]
+ for i in range(0, len(ext)):
+ v_raw = "203701%02d000000" % (i + 1)
+ if ext[i] == "Z":
+ v_set = v_raw[2:] + ext[i]
+ else:
+ v_set = v_raw + ext[i]
+ v_get = v_raw + ".0Z"
+
+ m = Message()
+ m.dn = Dn(ldb, user_dn)
+ m["msTSExpireDate"] = MessageElement([v_set],
+ FLAG_MOD_REPLACE,
+ "msTSExpireDate")
+ self.ldb.modify(m)
+
+ res = self.ldb.search(base=user_dn, scope=SCOPE_BASE, attrs=["msTSExpireDate"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("msTSExpireDate" in res[0])
+ self.assertTrue(len(res[0]["msTSExpireDate"]) == 1)
+ self.assertEqual(str(res[0]["msTSExpireDate"][0]), v_get)
+
+ def test_ldapSearchNoAttributes(self):
+ """Testing ldap search with no attributes"""
+
+ user_name = "testemptyattributesuser"
+ user_dn = "CN=%s,%s" % (user_name, self.base_dn)
+ delete_force(self.ldb, user_dn)
+
+ self.ldb.add({"dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name})
+
+ res = self.ldb.search(user_dn, scope=SCOPE_BASE, attrs=[])
+ delete_force(self.ldb, user_dn)
+
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]), 0)
+
+
+class BaseDnTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(BaseDnTests, self).setUp()
+ self.ldb = ldb
+
+ def test_rootdse_attrs(self):
+ """Testing for all rootDSE attributes"""
+ res = self.ldb.search("", scope=SCOPE_BASE, attrs=[])
+ self.assertEqual(len(res), 1)
+
+ def test_highestcommittedusn(self):
+ """Testing for highestCommittedUSN"""
+ res = self.ldb.search("", scope=SCOPE_BASE, attrs=["highestCommittedUSN"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue(int(res[0]["highestCommittedUSN"][0]) != 0)
+
+ def test_netlogon(self):
+ """Testing for netlogon via LDAP"""
+ res = self.ldb.search("", scope=SCOPE_BASE, attrs=["netlogon"])
+ self.assertEqual(len(res), 0)
+
+ def test_netlogon_highestcommitted_usn(self):
+ """Testing for netlogon and highestCommittedUSN via LDAP"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["netlogon", "highestCommittedUSN"])
+ self.assertEqual(len(res), 0)
+
+ def test_namingContexts(self):
+ """Testing for namingContexts in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["namingContexts", "defaultNamingContext", "schemaNamingContext", "configurationNamingContext"])
+ self.assertEqual(len(res), 1)
+
+ ncs = set([])
+ for nc in res[0]["namingContexts"]:
+ self.assertTrue(nc not in ncs)
+ ncs.add(nc)
+
+ self.assertTrue(res[0]["defaultNamingContext"][0] in ncs)
+ self.assertTrue(res[0]["configurationNamingContext"][0] in ncs)
+ self.assertTrue(res[0]["schemaNamingContext"][0] in ncs)
+
+ def test_serverPath(self):
+ """Testing the server paths in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["dsServiceName", "serverName"])
+ self.assertEqual(len(res), 1)
+
+ self.assertTrue("CN=Servers" in str(res[0]["dsServiceName"][0]))
+ self.assertTrue("CN=Sites" in str(res[0]["dsServiceName"][0]))
+ self.assertTrue("CN=NTDS Settings" in str(res[0]["dsServiceName"][0]))
+ self.assertTrue("CN=Servers" in str(res[0]["serverName"][0]))
+ self.assertTrue("CN=Sites" in str(res[0]["serverName"][0]))
+ self.assertFalse("CN=NTDS Settings" in str(res[0]["serverName"][0]))
+
+ def test_functionality(self):
+ """Testing the server paths in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["forestFunctionality", "domainFunctionality", "domainControllerFunctionality"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]["forestFunctionality"]), 1)
+ self.assertEqual(len(res[0]["domainFunctionality"]), 1)
+ self.assertEqual(len(res[0]["domainControllerFunctionality"]), 1)
+
+ self.assertTrue(int(res[0]["forestFunctionality"][0]) <= int(res[0]["domainFunctionality"][0]))
+ self.assertTrue(int(res[0]["domainControllerFunctionality"][0]) >= int(res[0]["domainFunctionality"][0]))
+
+ res2 = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["dsServiceName", "serverName"])
+ self.assertEqual(len(res2), 1)
+ self.assertEqual(len(res2[0]["dsServiceName"]), 1)
+
+ res3 = self.ldb.search(res2[0]["dsServiceName"][0], scope=SCOPE_BASE, attrs=["msDS-Behavior-Version"])
+ self.assertEqual(len(res3), 1)
+ self.assertEqual(len(res3[0]["msDS-Behavior-Version"]), 1)
+ self.assertEqual(int(res[0]["domainControllerFunctionality"][0]), int(res3[0]["msDS-Behavior-Version"][0]))
+
+ res4 = self.ldb.search(ldb.domain_dn(), scope=SCOPE_BASE, attrs=["msDS-Behavior-Version"])
+ self.assertEqual(len(res4), 1)
+ self.assertEqual(len(res4[0]["msDS-Behavior-Version"]), 1)
+ self.assertEqual(int(res[0]["domainFunctionality"][0]), int(res4[0]["msDS-Behavior-Version"][0]))
+
+ res5 = self.ldb.search("cn=partitions,%s" % ldb.get_config_basedn(), scope=SCOPE_BASE, attrs=["msDS-Behavior-Version"])
+ self.assertEqual(len(res5), 1)
+ self.assertEqual(len(res5[0]["msDS-Behavior-Version"]), 1)
+ self.assertEqual(int(res[0]["forestFunctionality"][0]), int(res5[0]["msDS-Behavior-Version"][0]))
+
+ def test_dnsHostname(self):
+ """Testing the DNS hostname in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["dnsHostName", "serverName"])
+ self.assertEqual(len(res), 1)
+
+ res2 = self.ldb.search(res[0]["serverName"][0], scope=SCOPE_BASE,
+ attrs=["dNSHostName"])
+ self.assertEqual(len(res2), 1)
+
+ self.assertEqual(res[0]["dnsHostName"][0], res2[0]["dNSHostName"][0])
+
+ def test_ldapServiceName(self):
+ """Testing the ldap service name in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["ldapServiceName", "dnsHostName"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("ldapServiceName" in res[0])
+ self.assertTrue("dnsHostName" in res[0])
+
+ (hostname, _, dns_domainname) = str(res[0]["dnsHostName"][0]).partition(".")
+
+ given = str(res[0]["ldapServiceName"][0])
+ expected = "%s:%s$@%s" % (dns_domainname.lower(), hostname.lower(), dns_domainname.upper())
+ self.assertEqual(given, expected)
+
+
+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)
+if "tdb://" not in host:
+ gc_ldb = Ldb("%s:3268" % host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+else:
+ gc_ldb = None
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ldap_modify_order.py b/source4/dsdb/tests/python/ldap_modify_order.py
new file mode 100644
index 0000000..80c4a3a
--- /dev/null
+++ b/source4/dsdb/tests/python/ldap_modify_order.py
@@ -0,0 +1,371 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008-2011
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import optparse
+import sys
+import os
+from itertools import permutations
+import traceback
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_BASE, LdbError
+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.tests import delete_force
+
+TEST_DATA_DIR = os.path.join(
+ os.path.dirname(__file__),
+ 'testdata')
+
+LDB_STRERR = {}
+def _build_ldb_strerr():
+ import ldb
+ for k, v in vars(ldb).items():
+ if k.startswith('ERR_') and isinstance(v, int):
+ LDB_STRERR[v] = k
+
+_build_ldb_strerr()
+
+
+class ModifyOrderTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.admin_dsdb = get_dsdb(admin_creds)
+ self.base_dn = self.admin_dsdb.domain_dn()
+
+ def delete_object(self, dn):
+ delete_force(self.admin_dsdb, dn)
+
+ def get_user_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def _test_modify_order(self,
+ start_attrs,
+ mod_attrs,
+ extra_search_attrs=(),
+ name=None):
+ if name is None:
+ name = traceback.extract_stack()[-2][2][5:]
+
+ if opts.normal_user:
+ name += '-non-admin'
+ username = "user123"
+ password = "pass123@#$@#"
+ self.admin_dsdb.newuser(username, password)
+ self.addCleanup(self.delete_object, self.get_user_dn(username))
+ mod_creds = self.insta_creds(template=admin_creds,
+ username=username,
+ userpass=password)
+ else:
+ mod_creds = admin_creds
+
+ mod_dsdb = get_dsdb(mod_creds)
+ sig = []
+ op_lut = ['', 'add', 'replace', 'delete']
+
+ search_attrs = set(extra_search_attrs)
+ lines = [name, "initial attrs:"]
+ for k, v in start_attrs:
+ lines.append("%20s: %r" % (k, v))
+ search_attrs.add(k)
+
+ for k, v, op in mod_attrs:
+ search_attrs.add(k)
+
+ search_attrs = sorted(search_attrs)
+ header = "\n".join(lines)
+ sig.append(header)
+
+ clusters = {}
+ for i, attrs in enumerate(permutations(mod_attrs)):
+ # for each permutation we construct a string describing the
+ # requested operations, and a string describing the result
+ # (which may be an exception). The we cluster the
+ # attribute strings by their results.
+ dn = "cn=ldaptest_%s_%d,cn=users,%s" % (name, i, self.base_dn)
+ m = Message()
+ m.dn = Dn(self.admin_dsdb, dn)
+
+ # We are using Message objects here for add (rather than the
+ # more convenient dict) because we maybe care about the order
+ # in which attributes are added.
+
+ for k, v in start_attrs:
+ m[k] = MessageElement(v, 0, k)
+
+ self.admin_dsdb.add(m)
+ self.addCleanup(self.delete_object, dn)
+
+ m = Message()
+ m.dn = Dn(mod_dsdb, dn)
+
+ attr_lines = []
+ for k, v, op in attrs:
+ if v is None:
+ v = dn
+ m[k] = MessageElement(v, op, k)
+ attr_lines.append("%16s %-8s %s" % (k, op_lut[op], v))
+
+ attr_str = '\n'.join(attr_lines)
+
+ try:
+ mod_dsdb.modify(m)
+ except LdbError as e:
+ err, _ = e.args
+ s = LDB_STRERR.get(err, "unknown error")
+ result_str = "%s (%d)" % (s, err)
+ else:
+ res = self.admin_dsdb.search(base=dn, scope=SCOPE_BASE,
+ attrs=search_attrs)
+
+ lines = []
+ for k, v in sorted(dict(res[0]).items()):
+ if k != "dn" or k in extra_search_attrs:
+ lines.append("%20s: %r" % (k, sorted(v)))
+
+ result_str = '\n'.join(lines)
+
+ clusters.setdefault(result_str, []).append(attr_str)
+
+ for s, attrs in sorted(clusters.items()):
+ sig.extend([
+ "== result ===[%3d]=======================" % len(attrs),
+ s,
+ "-- operations ---------------------------"])
+ for a in attrs:
+ sig.append(a)
+ sig.append("-" * 34)
+
+ sig = '\n'.join(sig).replace(self.base_dn, "{base dn}")
+
+ if opts.verbose:
+ print(sig)
+
+ if opts.rewrite_ground_truth:
+ f = open(os.path.join(TEST_DATA_DIR, name + '.expected'), 'w')
+ f.write(sig)
+ f.close()
+ f = open(os.path.join(TEST_DATA_DIR, name + '.expected'))
+ expected = f.read()
+ f.close()
+
+ self.assertStringsEqual(sig, expected)
+
+ def test_modify_order_mixed(self):
+ start_attrs = [("objectclass", "user"),
+ ("carLicense", ["1", "2", "3"]),
+ ("otherTelephone", "123")]
+
+ mod_attrs = [("carLicense", "3", FLAG_MOD_DELETE),
+ ("carLicense", "4", FLAG_MOD_ADD),
+ ("otherTelephone", "4", FLAG_MOD_REPLACE),
+ ("otherTelephone", "123", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_mixed2(self):
+ start_attrs = [("objectclass", "user"),
+ ("carLicense", ["1", "2", "3"]),
+ ("ipPhone", "123")]
+
+ mod_attrs = [("carLicense", "3", FLAG_MOD_DELETE),
+ ("carLicense", "4", FLAG_MOD_ADD),
+ ("ipPhone", "4", FLAG_MOD_REPLACE),
+ ("ipPhone", "123", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_telephone(self):
+ start_attrs = [("objectclass", "user"),
+ ("otherTelephone", "123")]
+
+ mod_attrs = [("carLicense", "3", FLAG_MOD_REPLACE),
+ ("carLicense", "4", FLAG_MOD_ADD),
+ ("otherTelephone", "4", FLAG_MOD_REPLACE),
+ ("otherTelephone", "4", FLAG_MOD_ADD),
+ ("otherTelephone", "123", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_telephone_delete_delete(self):
+ start_attrs = [("objectclass", "user"),
+ ("otherTelephone", "123")]
+
+ mod_attrs = [("carLicense", "3", FLAG_MOD_REPLACE),
+ ("carLicense", "4", FLAG_MOD_DELETE),
+ ("otherTelephone", "4", FLAG_MOD_REPLACE),
+ ("otherTelephone", "4", FLAG_MOD_DELETE),
+ ("otherTelephone", "123", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_objectclass(self):
+ start_attrs = [("objectclass", "user"),
+ ("otherTelephone", "123")]
+
+ mod_attrs = [("objectclass", "computer", FLAG_MOD_REPLACE),
+ ("objectclass", "user", FLAG_MOD_DELETE),
+ ("objectclass", "person", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_objectclass2(self):
+ start_attrs = [("objectclass", "user")]
+
+ mod_attrs = [("objectclass", "computer", FLAG_MOD_REPLACE),
+ ("objectclass", "user", FLAG_MOD_ADD),
+ ("objectclass", "attributeSchema", FLAG_MOD_REPLACE),
+ ("objectclass", "inetOrgPerson", FLAG_MOD_ADD),
+ ("objectclass", "person", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_singlevalue(self):
+ start_attrs = [("objectclass", "user"),
+ ("givenName", "a")]
+
+ mod_attrs = [("givenName", "a", FLAG_MOD_REPLACE),
+ ("givenName", ["b", "a"], FLAG_MOD_REPLACE),
+ ("givenName", "b", FLAG_MOD_DELETE),
+ ("givenName", "a", FLAG_MOD_DELETE),
+ ("givenName", "c", FLAG_MOD_ADD)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_inapplicable(self):
+ #attributes that don't go on a user
+ start_attrs = [("objectclass", "user"),
+ ("givenName", "a")]
+
+ mod_attrs = [("dhcpSites", "b", FLAG_MOD_REPLACE),
+ ("dhcpSites", "b", FLAG_MOD_DELETE),
+ ("dhcpSites", "c", FLAG_MOD_ADD)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_sometimes_inapplicable(self):
+ # attributes that don't go on a user, but do on a computer,
+ # which we sometimes change into.
+ start_attrs = [("objectclass", "user"),
+ ("givenName", "a")]
+
+ mod_attrs = [("objectclass", "computer", FLAG_MOD_REPLACE),
+ ("objectclass", "person", FLAG_MOD_DELETE),
+ ("dnsHostName", "b", FLAG_MOD_ADD),
+ ("dnsHostName", "c", FLAG_MOD_REPLACE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_account_locality_device(self):
+ # account, locality, and device all take l (locality name) but
+ # only device takes owner. We shouldn't be able to change
+ # objectclass at all.
+ start_attrs = [("objectclass", "account"),
+ ("l", "a")]
+
+ mod_attrs = [("objectclass", ["device", "top"], FLAG_MOD_REPLACE),
+ ("l", "a", FLAG_MOD_DELETE),
+ ("owner", "c", FLAG_MOD_ADD)
+ ]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_container_flags_multivalue(self):
+ # account, locality, and device all take l (locality name)
+ # but only device takes owner
+ start_attrs = [("objectclass", "container"),
+ ("wWWHomePage", "a")]
+
+ mod_attrs = [("flags", ["0", "1"], FLAG_MOD_ADD),
+ ("flags", "65355", FLAG_MOD_ADD),
+ ("flags", "65355", FLAG_MOD_DELETE),
+ ("flags", ["2", "101"], FLAG_MOD_REPLACE),
+ ]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_container_flags(self):
+ #flags should be an integer
+ start_attrs = [("objectclass", "container")]
+
+ mod_attrs = [("flags", "0x6", FLAG_MOD_ADD),
+ ("flags", "5", FLAG_MOD_ADD),
+ ("flags", "101", FLAG_MOD_REPLACE),
+ ("flags", "c", FLAG_MOD_DELETE),
+ ]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_member(self):
+ name = "modify_order_member_other_group"
+
+ dn2 = "cn=%s,%s" % (name, self.base_dn)
+ m = Message()
+ m.dn = Dn(self.admin_dsdb, dn2)
+ self.admin_dsdb.add({"dn": dn2, "objectclass": "group"})
+ self.addCleanup(self.delete_object, dn2)
+
+ start_attrs = [("objectclass", "group"),
+ ("member", dn2)]
+
+ mod_attrs = [("member", None, FLAG_MOD_DELETE),
+ ("member", None, FLAG_MOD_REPLACE),
+ ("member", dn2, FLAG_MOD_DELETE),
+ ("member", None, FLAG_MOD_ADD),
+ ]
+ self._test_modify_order(start_attrs, mod_attrs, ["memberOf"])
+
+
+def get_dsdb(creds=None):
+ if creds is None:
+ creds = admin_creds
+ dsdb = SamDB(host,
+ credentials=creds,
+ session_info=system_session(lp),
+ lp=lp)
+ return dsdb
+
+
+parser = optparse.OptionParser("ldap_modify_order.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+parser.add_option("--rewrite-ground-truth", action="store_true",
+ help="write expected values")
+parser.add_option("-v", "--verbose", action="store_true")
+parser.add_option("--normal-user", action="store_true")
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+admin_creds = credopts.get_credentials(lp)
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ldap_schema.py b/source4/dsdb/tests/python/ldap_schema.py
new file mode 100755
index 0000000..b08aa7f
--- /dev/null
+++ b/source4/dsdb/tests/python/ldap_schema.py
@@ -0,0 +1,1601 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This is a port of the original in testprogs/ejs/ldap.js
+
+# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008-2011
+# Copyright (C) Catalyst.Net Ltd 2017
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import optparse
+import sys
+import time
+import random
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_ONELEVEL, SCOPE_BASE, LdbError
+from ldb import ERR_NO_SUCH_OBJECT
+from ldb import ERR_UNWILLING_TO_PERFORM
+from ldb import ERR_ENTRY_ALREADY_EXISTS
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_OBJECT_CLASS_VIOLATION
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE
+from samba.samdb import SamDB
+from samba.dsdb import DS_DOMAIN_FUNCTION_2003
+from samba.tests import delete_force
+from samba.ndr import ndr_unpack
+from samba.dcerpc import drsblobs
+
+parser = optparse.OptionParser("ldap_schema.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)
+
+
+class SchemaTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SchemaTests, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp, options=ldb_options)
+ self.base_dn = self.ldb.domain_dn()
+ self.schema_dn = self.ldb.get_schema_basedn().get_linearized()
+
+ def test_generated_schema(self):
+ """Testing we can read the generated schema via LDAP"""
+ res = self.ldb.search("cn=aggregate," + self.schema_dn, scope=SCOPE_BASE,
+ attrs=["objectClasses", "attributeTypes", "dITContentRules"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("dITContentRules" in res[0])
+ self.assertTrue("objectClasses" in res[0])
+ self.assertTrue("attributeTypes" in res[0])
+
+ def test_generated_schema_is_operational(self):
+ """Testing we don't get the generated schema via LDAP by default"""
+ # Must keep the "*" form
+ res = self.ldb.search("cn=aggregate," + self.schema_dn, scope=SCOPE_BASE,
+ attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertFalse("dITContentRules" in res[0])
+ self.assertFalse("objectClasses" in res[0])
+ self.assertFalse("attributeTypes" in res[0])
+
+ def test_schemaUpdateNow(self):
+ """Testing schemaUpdateNow"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.6.1.""" + rand + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+ # We must do a schemaUpdateNow otherwise it's not 100% sure that the schema
+ # will contain the new attribute
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ # Search for created attribute
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (attr_name, self.schema_dn), scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "schemaIDGUID", "msDS-IntID"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), attr_ldap_display_name)
+ self.assertTrue("schemaIDGUID" in res[0])
+ if "msDS-IntId" in res[0]:
+ msDS_IntId = int(res[0]["msDS-IntId"][0])
+ if msDS_IntId < 0:
+ msDS_IntId += (1 << 32)
+ else:
+ msDS_IntId = None
+
+ class_name = "test-Class" + time.strftime("%s", time.gmtime())
+ class_ldap_display_name = class_name.replace("-", "")
+
+ # First try to create a class with a wrong "defaultObjectCategory"
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+defaultObjectCategory: CN=_
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.6.1.""" + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemFlags: 16
+rDNAttID: cn
+systemMustContain: cn
+systemMustContain: """ + attr_ldap_display_name + """
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.6.2.""" + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemFlags: 16
+rDNAttID: cn
+systemMustContain: cn
+systemMustContain: """ + attr_ldap_display_name + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # Search for created objectclass
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (class_name, self.schema_dn), scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "defaultObjectCategory", "schemaIDGUID", "distinguishedName"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), class_ldap_display_name)
+ self.assertEqual(res[0]["defaultObjectCategory"][0], res[0]["distinguishedName"][0])
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ object_name = "obj" + time.strftime("%s", time.gmtime())
+
+ ldif = """
+dn: CN=%s,CN=Users,%s""" % (object_name, self.base_dn) + """
+objectClass: organizationalPerson
+objectClass: person
+objectClass: """ + class_ldap_display_name + """
+objectClass: top
+cn: """ + object_name + """
+instanceType: 4
+objectCategory: CN=%s,%s""" % (class_name, self.schema_dn) + """
+distinguishedName: CN=%s,CN=Users,%s""" % (object_name, self.base_dn) + """
+name: """ + object_name + """
+""" + attr_ldap_display_name + """: test
+"""
+ self.ldb.add_ldif(ldif)
+
+ # Search for created object
+ obj_res = self.ldb.search("cn=%s,cn=Users,%s" % (object_name, self.base_dn), scope=SCOPE_BASE, attrs=["replPropertyMetaData"])
+
+ self.assertEqual(len(obj_res), 1)
+ self.assertTrue("replPropertyMetaData" in obj_res[0])
+ val = obj_res[0]["replPropertyMetaData"][0]
+ repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, val)
+ obj = repl.ctr
+
+ # Windows 2000 functional level won't have this. It is too
+ # hard to work it out from the prefixmap however, so we skip
+ # this test in that case.
+ if msDS_IntId is not None:
+ found = False
+ for o in repl.ctr.array:
+ if o.attid == msDS_IntId:
+ found = True
+ break
+ self.assertTrue(found, "Did not find 0x%08x in replPropertyMetaData" % msDS_IntId)
+ # Delete the object
+ delete_force(self.ldb, "cn=%s,cn=Users,%s" % (object_name, self.base_dn))
+
+ def test_subClassOf(self):
+ """ Testing usage of custom child classSchema
+ """
+
+ class_name = "my-Class" + time.strftime("%s", time.gmtime())
+ class_ldap_display_name = class_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.6.3.""" + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalUnit
+systemFlags: 16
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # Search for created objectclass
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (class_name, self.schema_dn), scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "defaultObjectCategory",
+ "schemaIDGUID", "distinguishedName"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), class_ldap_display_name)
+ self.assertEqual(res[0]["defaultObjectCategory"][0], res[0]["distinguishedName"][0])
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ object_name = "org" + time.strftime("%s", time.gmtime())
+
+ ldif = """
+dn: OU=%s,%s""" % (object_name, self.base_dn) + """
+objectClass: """ + class_ldap_display_name + """
+ou: """ + object_name + """
+instanceType: 4
+"""
+ self.ldb.add_ldif(ldif)
+
+ # Search for created object
+ res = []
+ res = self.ldb.search("ou=%s,%s" % (object_name, self.base_dn), scope=SCOPE_BASE, attrs=["dn"])
+ self.assertEqual(len(res), 1)
+ # Delete the object
+ delete_force(self.ldb, "ou=%s,%s" % (object_name, self.base_dn))
+
+ def test_duplicate_attributeID(self):
+ """Testing creating a duplicate attribute"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.2." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate attributeID value")
+ except LdbError as e2:
+ (enum, estr) = e2.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_attributeID_governsID(self):
+ """Testing creating a duplicate attribute and class"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.3." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+governsId: """ + attributeID + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate governsID conflicting with attributeID value")
+ except LdbError as e3:
+ (enum, estr) = e3.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_cn(self):
+ """Testing creating a duplicate attribute"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.4." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add attribute with duplicate CN")
+ except LdbError as e4:
+ (enum, estr) = e4.args
+ self.assertEqual(enum, ERR_ENTRY_ALREADY_EXISTS)
+
+ def test_duplicate_implicit_ldapdisplayname(self):
+ """Testing creating a duplicate attribute ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.5." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add attribute with duplicate of the implicit ldapDisplayName")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_explicit_ldapdisplayname(self):
+ """Testing creating a duplicate attribute ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.6." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add attribute with duplicate ldapDisplayName")
+ except LdbError as e6:
+ (enum, estr) = e6.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_explicit_ldapdisplayname_with_class(self):
+ """Testing creating a duplicate attribute ldapdisplayname between
+ and attribute and a class"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.7." + rand
+ governsID = "1.3.6.1.4.1.7165.4.6.2.6.4." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """
+governsID: """ + governsID + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add class with duplicate ldapDisplayName")
+ except LdbError as e7:
+ (enum, estr) = e7.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_via_rename_ldapdisplayname(self):
+ """Testing creating a duplicate attribute ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.8." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """dup
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: ldapDisplayName
+ldapDisplayName: """ + attr_ldap_display_name + """
+-
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have attribute with duplicate ldapDisplayName")
+ except LdbError as e8:
+ (enum, estr) = e8.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_via_rename_attributeID(self):
+ """Testing creating a duplicate attributeID"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.9." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """dup
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: attributeId
+attributeId: """ + attributeID + """
+-
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have attribute with duplicate attributeID")
+ except LdbError as e9:
+ (enum, estr) = e9.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+ def test_remove_ldapdisplayname(self):
+ """Testing removing the ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.10." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: ldapDisplayName
+-
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to remove the ldapdisplayname")
+ except LdbError as e10:
+ (enum, estr) = e10.args
+ self.assertEqual(enum, ERR_OBJECT_CLASS_VIOLATION)
+
+ def test_rename_ldapdisplayname(self):
+ """Testing renaming ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.11." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: ldapDisplayName
+ldapDisplayName: """ + attr_ldap_display_name + """2
+-
+"""
+ self.ldb.modify_ldif(ldif)
+
+ def test_change_attributeID(self):
+ """Testing change the attributeID"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.12." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: attributeID
+attributeId: """ + attributeID + """.1
+
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have different attributeID")
+ except LdbError as e11:
+ (enum, estr) = e11.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+ def test_change_attributeID_same(self):
+ """Testing change the attributeID to the same value"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.13." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: attributeID
+attributeId: """ + attributeID + """
+
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have the same attributeID")
+ except LdbError as e12:
+ (enum, estr) = e12.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+ def test_generated_linkID(self):
+ """
+ Test that we automatically generate a linkID if the
+ OID "1.2.840.113556.1.2.50" is given as the linkID
+ of a new attribute, and that we don't get/can't add
+ duplicate linkIDs. Also test that we can add a backlink
+ by providing the attributeID or ldapDisplayName of
+ a forwards link in the linkID attribute.
+ """
+
+ # linkID generation isn't available before 2003
+ res = self.ldb.search(base="", expression="", scope=SCOPE_BASE,
+ attrs=["domainControllerFunctionality"])
+ self.assertEqual(len(res), 1)
+ dc_level = int(res[0]["domainControllerFunctionality"][0])
+ if dc_level < DS_DOMAIN_FUNCTION_2003:
+ return
+
+ rand = str(random.randint(1, 100000))
+
+ attr_name_1 = "test-generated-linkID" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_1 = attr_name_1.replace("-", "")
+ attributeID_1 = "1.3.6.1.4.1.7165.4.6.1.6.16." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_1, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_1 + """
+adminDisplayName: """ + attr_name_1 + """
+cn: """ + attr_name_1 + """
+attributeId: """ + attributeID_1 + """
+linkID: 1.2.840.113556.1.2.50
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_1 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e13:
+ (enum, estr) = e13.args
+ self.fail(estr)
+
+ attr_name_2 = "test-generated-linkID-2" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_2 = attr_name_2.replace("-", "")
+ attributeID_2 = "1.3.6.1.4.1.7165.4.6.1.6.17." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_2, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_2 + """
+adminDisplayName: """ + attr_name_2 + """
+cn: """ + attr_name_2 + """
+attributeId: """ + attributeID_2 + """
+linkID: 1.2.840.113556.1.2.50
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_2 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e14:
+ (enum, estr) = e14.args
+ self.fail(estr)
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_1, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["linkID"])
+ self.assertEqual(len(res), 1)
+ linkID_1 = int(res[0]["linkID"][0])
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_2, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["linkID"])
+ self.assertEqual(len(res), 1)
+ linkID_2 = int(res[0]["linkID"][0])
+
+ # 0 should never be generated as a linkID
+ self.assertFalse(linkID_1 == 0)
+ self.assertFalse(linkID_2 == 0)
+
+ # The generated linkID should always be even, because
+ # it should assume we're adding a forward link.
+ self.assertTrue(linkID_1 % 2 == 0)
+ self.assertTrue(linkID_2 % 2 == 0)
+
+ self.assertFalse(linkID_1 == linkID_2)
+
+ # This is only necessary against Windows, since we depend
+ # on the previously added links in the next ones and Windows
+ # won't refresh the schema as we add them.
+ ldif = """
+dn:
+changetype: modify
+replace: schemaupdatenow
+schemaupdatenow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ # If we add a new link with the same linkID, it should fail
+ attr_name = "test-generated-linkID-duplicate" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.18." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+linkID: """ + str(linkID_1) + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate linkID value")
+ except LdbError as e15:
+ (enum, estr) = e15.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ # If we add another attribute with the attributeID or lDAPDisplayName
+ # of a forward link in its linkID field, it should add as a backlink
+
+ attr_name_3 = "test-generated-linkID-backlink" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_3 = attr_name_3.replace("-", "")
+ attributeID_3 = "1.3.6.1.4.1.7165.4.6.1.6.19." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_3, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_3 + """
+adminDisplayName: """ + attr_name_3 + """
+cn: """ + attr_name_3 + """
+attributeId: """ + attributeID_3 + """
+linkID: """ + str(linkID_1 + 1) + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_3 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e16:
+ (enum, estr) = e16.args
+ self.fail(estr)
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_3, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["linkID"])
+ self.assertEqual(len(res), 1)
+ linkID = int(res[0]["linkID"][0])
+ self.assertEqual(linkID, linkID_1 + 1)
+
+ attr_name_4 = "test-generated-linkID-backlink-2" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_4 = attr_name_4.replace("-", "")
+ attributeID_4 = "1.3.6.1.4.1.7165.4.6.1.6.20." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_4, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_4 + """
+adminDisplayName: """ + attr_name_4 + """
+cn: """ + attr_name_4 + """
+attributeId: """ + attributeID_4 + """
+linkID: """ + attr_ldap_display_name_2 + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_4 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e17:
+ (enum, estr) = e17.args
+ self.fail(estr)
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_4, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["linkID"])
+ self.assertEqual(len(res), 1)
+ linkID = int(res[0]["linkID"][0])
+ self.assertEqual(linkID, linkID_2 + 1)
+
+ # If we then try to add another backlink in the same way
+ # for the same forwards link, we should fail.
+
+ attr_name = "test-generated-linkID-backlink-duplicate" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.21." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+linkID: """ + attributeID_1 + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate backlink")
+ except LdbError as e18:
+ (enum, estr) = e18.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ # If we try to supply the attributeID or ldapDisplayName
+ # of an existing backlink in the linkID field of a new link,
+ # it should fail.
+
+ attr_name = "test-generated-linkID-backlink-invalid" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.22." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+linkID: """ + attributeID_3 + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add backlink of backlink")
+ except LdbError as e19:
+ (enum, estr) = e19.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ attr_name = "test-generated-linkID-backlink-invalid-2" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.23." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+linkID: """ + attr_ldap_display_name_4 + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add backlink of backlink")
+ except LdbError as e20:
+ (enum, estr) = e20.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_generated_mAPIID(self):
+ """
+ Test that we automatically generate a mAPIID if the
+ OID "1.2.840.113556.1.2.49" is given as the mAPIID
+ of a new attribute, and that we don't get/can't add
+ duplicate mAPIIDs.
+ """
+
+ rand = str(random.randint(1, 100000))
+
+ attr_name_1 = "test-generated-mAPIID" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_1 = attr_name_1.replace("-", "")
+ attributeID_1 = "1.3.6.1.4.1.7165.4.6.1.6.24." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_1, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_1 + """
+adminDisplayName: """ + attr_name_1 + """
+cn: """ + attr_name_1 + """
+attributeId: """ + attributeID_1 + """
+mAPIID: 1.2.840.113556.1.2.49
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_1 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e21:
+ (enum, estr) = e21.args
+ self.fail(estr)
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_1, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["mAPIID"])
+ self.assertEqual(len(res), 1)
+ mAPIID_1 = int(res[0]["mAPIID"][0])
+
+ ldif = """
+dn:
+changetype: modify
+replace: schemaupdatenow
+schemaupdatenow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ # If we add a new attribute with the same mAPIID, it should fail
+ attr_name = "test-generated-mAPIID-duplicate" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.25." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+mAPIID: """ + str(mAPIID_1) + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate mAPIID value")
+ except LdbError as e22:
+ (enum, estr) = e22.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_change_governsID(self):
+ """Testing change the governsID"""
+ rand = str(random.randint(1, 100000))
+ class_name = "test-Class" + time.strftime("%s", time.gmtime()) + "-" + rand
+ class_ldap_display_name = class_name.replace("-", "")
+ governsID = "1.3.6.1.4.1.7165.4.6.2.6.5." + rand
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: """ + governsID + """
+ldapDisplayName: """ + class_ldap_display_name + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+changetype: modify
+replace: governsID
+governsId: """ + governsID + """.1
+
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have different governsID")
+ except LdbError as e23:
+ (enum, estr) = e23.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+ def test_change_governsID_same(self):
+ """Testing change the governsID"""
+ rand = str(random.randint(1, 100000))
+ class_name = "test-Class" + time.strftime("%s", time.gmtime()) + "-" + rand
+ class_ldap_display_name = class_name.replace("-", "")
+ governsID = "1.3.6.1.4.1.7165.4.6.2.6.6." + rand
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: """ + governsID + """
+ldapDisplayName: """ + class_ldap_display_name + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+changetype: modify
+replace: governsID
+governsId: """ + governsID + """.1
+
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have the same governsID")
+ except LdbError as e24:
+ (enum, estr) = e24.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+
+class SchemaTests_msDS_IntId(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SchemaTests_msDS_IntId, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp, options=ldb_options)
+ res = self.ldb.search(base="", expression="", scope=SCOPE_BASE,
+ attrs=["schemaNamingContext", "defaultNamingContext",
+ "forestFunctionality"])
+ self.assertEqual(len(res), 1)
+ self.schema_dn = res[0]["schemaNamingContext"][0]
+ self.base_dn = res[0]["defaultNamingContext"][0]
+ self.forest_level = int(res[0]["forestFunctionality"][0])
+
+ def _ldap_schemaUpdateNow(self):
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ def _make_obj_names(self, prefix):
+ class_name = prefix + time.strftime("%s", time.gmtime())
+ class_ldap_name = class_name.replace("-", "")
+ class_dn = "CN=%s,%s" % (class_name, self.schema_dn)
+ return (class_name, class_ldap_name, class_dn)
+
+ def _is_schema_base_object(self, ldb_msg):
+ """Test systemFlags for SYSTEM_FLAG_SCHEMA_BASE_OBJECT (16)"""
+ systemFlags = 0
+ if "systemFlags" in ldb_msg:
+ systemFlags = int(ldb_msg["systemFlags"][0])
+ return (systemFlags & 16) != 0
+
+ def _make_attr_ldif(self, attr_name, attr_dn):
+ ldif = """
+dn: """ + attr_dn + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.6.14.""" + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ return ldif
+
+ def test_msDS_IntId_on_attr(self):
+ """Testing msDs-IntId creation for Attributes.
+ See MS-ADTS - 3.1.1.Attributes
+
+ This test should verify that:
+ - Creating attribute with 'msDS-IntId' fails with ERR_UNWILLING_TO_PERFORM
+ - Adding 'msDS-IntId' on existing attribute fails with ERR_CONSTRAINT_VIOLATION
+ - Creating attribute with 'msDS-IntId' set and FLAG_SCHEMA_BASE_OBJECT flag
+ set fails with ERR_UNWILLING_TO_PERFORM
+ - Attributes created with FLAG_SCHEMA_BASE_OBJECT not set have
+ 'msDS-IntId' attribute added internally
+ """
+
+ # 1. Create attribute without systemFlags
+ # msDS-IntId should be created if forest functional
+ # level is >= DS_DOMAIN_FUNCTION_2003
+ # and missing otherwise
+ (attr_name, attr_ldap_name, attr_dn) = self._make_obj_names("msDS-IntId-Attr-1-")
+ ldif = self._make_attr_ldif(attr_name, attr_dn)
+
+ # try to add msDS-IntId during Attribute creation
+ ldif_fail = ldif + "msDS-IntId: -1993108831\n"
+ try:
+ self.ldb.add_ldif(ldif_fail)
+ self.fail("Adding attribute with preset msDS-IntId should fail")
+ except LdbError as e25:
+ (num, _) = e25.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # add the new attribute and update schema
+ self.ldb.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+
+ # Search for created attribute
+ res = []
+ res = self.ldb.search(attr_dn, scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "msDS-IntId", "systemFlags"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), attr_ldap_name)
+ if self.forest_level >= DS_DOMAIN_FUNCTION_2003:
+ if self._is_schema_base_object(res[0]):
+ self.assertTrue("msDS-IntId" not in res[0])
+ else:
+ self.assertTrue("msDS-IntId" in res[0])
+ else:
+ self.assertTrue("msDS-IntId" not in res[0])
+
+ msg = Message()
+ msg.dn = Dn(self.ldb, attr_dn)
+ msg["msDS-IntId"] = MessageElement("-1993108831", FLAG_MOD_REPLACE, "msDS-IntId")
+ try:
+ self.ldb.modify(msg)
+ self.fail("Modifying msDS-IntId should return error")
+ except LdbError as e26:
+ (num, _) = e26.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # 2. Create attribute with systemFlags = FLAG_SCHEMA_BASE_OBJECT
+ # msDS-IntId should be created if forest functional
+ # level is >= DS_DOMAIN_FUNCTION_2003
+ # and missing otherwise
+ (attr_name, attr_ldap_name, attr_dn) = self._make_obj_names("msDS-IntId-Attr-2-")
+ ldif = self._make_attr_ldif(attr_name, attr_dn)
+ ldif += "systemFlags: 16\n"
+
+ # try to add msDS-IntId during Attribute creation
+ ldif_fail = ldif + "msDS-IntId: -1993108831\n"
+ try:
+ self.ldb.add_ldif(ldif_fail)
+ self.fail("Adding attribute with preset msDS-IntId should fail")
+ except LdbError as e27:
+ (num, _) = e27.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # add the new attribute and update schema
+ self.ldb.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+
+ # Search for created attribute
+ res = []
+ res = self.ldb.search(attr_dn, scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), attr_ldap_name)
+ if self.forest_level >= DS_DOMAIN_FUNCTION_2003:
+ if self._is_schema_base_object(res[0]):
+ self.assertTrue("msDS-IntId" not in res[0])
+ else:
+ self.assertTrue("msDS-IntId" in res[0])
+ else:
+ self.assertTrue("msDS-IntId" not in res[0])
+
+ msg = Message()
+ msg.dn = Dn(self.ldb, attr_dn)
+ msg["msDS-IntId"] = MessageElement("-1993108831", FLAG_MOD_REPLACE, "msDS-IntId")
+ try:
+ self.ldb.modify(msg)
+ self.fail("Modifying msDS-IntId should return error")
+ except LdbError as e28:
+ (num, _) = e28.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ def _make_class_ldif(self, class_dn, class_name, sub_oid):
+ ldif = """
+dn: """ + class_dn + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.6.%d.""" % sub_oid + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ return ldif
+
+ def test_msDS_IntId_on_class(self):
+ """Testing msDs-IntId creation for Class
+ Reference: MS-ADTS - 3.1.1.2.4.8 Class classSchema"""
+
+ # 1. Create Class without systemFlags
+ # msDS-IntId should be created if forest functional
+ # level is >= DS_DOMAIN_FUNCTION_2003
+ # and missing otherwise
+ (class_name, class_ldap_name, class_dn) = self._make_obj_names("msDS-IntId-Class-1-")
+ ldif = self._make_class_ldif(class_dn, class_name, 8)
+
+ # try to add msDS-IntId during Class creation
+ ldif_add = ldif + "msDS-IntId: -1993108831\n"
+ self.ldb.add_ldif(ldif_add)
+ self._ldap_schemaUpdateNow()
+
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["msDS-IntId"][0]), "-1993108831")
+
+ # add a new Class and update schema
+ (class_name, class_ldap_name, class_dn) = self._make_obj_names("msDS-IntId-Class-2-")
+ ldif = self._make_class_ldif(class_dn, class_name, 9)
+
+ self.ldb.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+
+ # Search for created Class
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertFalse("msDS-IntId" in res[0])
+
+ msg = Message()
+ msg.dn = Dn(self.ldb, class_dn)
+ msg["msDS-IntId"] = MessageElement("-1993108831", FLAG_MOD_REPLACE, "msDS-IntId")
+ try:
+ self.ldb.modify(msg)
+ self.fail("Modifying msDS-IntId should return error")
+ except LdbError as e29:
+ (num, _) = e29.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # 2. Create Class with systemFlags = FLAG_SCHEMA_BASE_OBJECT
+ # msDS-IntId should be created if forest functional
+ # level is >= DS_DOMAIN_FUNCTION_2003
+ # and missing otherwise
+ (class_name, class_ldap_name, class_dn) = self._make_obj_names("msDS-IntId-Class-3-")
+ ldif = self._make_class_ldif(class_dn, class_name, 10)
+ ldif += "systemFlags: 16\n"
+
+ # try to add msDS-IntId during Class creation
+ ldif_add = ldif + "msDS-IntId: -1993108831\n"
+ self.ldb.add_ldif(ldif_add)
+
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["msDS-IntId"][0]), "-1993108831")
+
+ # add the new Class and update schema
+ (class_name, class_ldap_name, class_dn) = self._make_obj_names("msDS-IntId-Class-4-")
+ ldif = self._make_class_ldif(class_dn, class_name, 11)
+ ldif += "systemFlags: 16\n"
+
+ self.ldb.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+
+ # Search for created Class
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertFalse("msDS-IntId" in res[0])
+
+ msg = Message()
+ msg.dn = Dn(self.ldb, class_dn)
+ msg["msDS-IntId"] = MessageElement("-1993108831", FLAG_MOD_REPLACE, "msDS-IntId")
+ try:
+ self.ldb.modify(msg)
+ self.fail("Modifying msDS-IntId should return error")
+ except LdbError as e30:
+ (num, _) = e30.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertFalse("msDS-IntId" in res[0])
+
+ def test_verify_msDS_IntId(self):
+ """Verify msDS-IntId exists only on attributes without FLAG_SCHEMA_BASE_OBJECT flag set"""
+ count = 0
+ res = self.ldb.search(self.schema_dn, scope=SCOPE_ONELEVEL,
+ expression="objectClass=attributeSchema",
+ attrs=["systemFlags", "msDS-IntId", "attributeID", "cn"])
+ self.assertTrue(len(res) > 1)
+ for ldb_msg in res:
+ if self.forest_level >= DS_DOMAIN_FUNCTION_2003:
+ if self._is_schema_base_object(ldb_msg):
+ self.assertTrue("msDS-IntId" not in ldb_msg)
+ else:
+ # don't assert here as there are plenty of
+ # attributes under w2k8 that are not part of
+ # Base Schema (SYSTEM_FLAG_SCHEMA_BASE_OBJECT flag not set)
+ # has not msDS-IntId attribute set
+ #self.assertTrue("msDS-IntId" in ldb_msg, "msDS-IntId expected on: %s" % ldb_msg.dn)
+ if "msDS-IntId" not in ldb_msg:
+ count = count + 1
+ print("%3d warning: msDS-IntId expected on: %-30s %s" % (count, ldb_msg["attributeID"], ldb_msg["cn"]))
+ else:
+ self.assertTrue("msDS-IntId" not in ldb_msg)
+
+
+class SchemaTests_msDS_isRODC(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SchemaTests_msDS_isRODC, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp, options=ldb_options)
+ res = self.ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["defaultNamingContext"])
+ self.assertEqual(len(res), 1)
+ self.base_dn = res[0]["defaultNamingContext"][0]
+
+ def test_objectClass_ntdsdsa(self):
+ res = self.ldb.search(self.base_dn, expression="objectClass=nTDSDSA",
+ attrs=["msDS-isRODC"], controls=["search_options:1:2"])
+ for ldb_msg in res:
+ self.assertTrue("msDS-isRODC" in ldb_msg)
+
+ def test_objectClass_server(self):
+ res = self.ldb.search(self.base_dn, expression="objectClass=server",
+ attrs=["msDS-isRODC"], controls=["search_options:1:2"])
+ for ldb_msg in res:
+ ntds_search_dn = "CN=NTDS Settings,%s" % ldb_msg['dn']
+ try:
+ res_check = self.ldb.search(ntds_search_dn, attrs=["objectCategory"])
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ print("Server entry %s doesn't have a NTDS settings object" % res[0]['dn'])
+ else:
+ self.assertTrue("objectCategory" in res_check[0])
+ self.assertTrue("msDS-isRODC" in ldb_msg)
+
+ def test_objectClass_computer(self):
+ res = self.ldb.search(self.base_dn, expression="objectClass=computer",
+ attrs=["serverReferenceBL", "msDS-isRODC"], controls=["search_options:1:2"])
+ for ldb_msg in res:
+ if "serverReferenceBL" not in ldb_msg:
+ print("Computer entry %s doesn't have a serverReferenceBL attribute" % ldb_msg['dn'])
+ else:
+ self.assertTrue("msDS-isRODC" in ldb_msg)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+ldb_options = []
+if host.startswith("ldap://"):
+ # user 'paged_search' module when connecting remotely
+ ldb_options = ["modules:paged_searches"]
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ldap_syntaxes.py b/source4/dsdb/tests/python/ldap_syntaxes.py
new file mode 100755
index 0000000..081c280
--- /dev/null
+++ b/source4/dsdb/tests/python/ldap_syntaxes.py
@@ -0,0 +1,388 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Tests for LDAP syntaxes
+
+import optparse
+import sys
+import time
+import random
+import uuid
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_BASE, SCOPE_SUBTREE, LdbError
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_INVALID_ATTRIBUTE_SYNTAX
+from ldb import ERR_ENTRY_ALREADY_EXISTS
+
+import samba.tests
+
+parser = optparse.OptionParser("ldap_syntaxes.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)
+
+
+class SyntaxTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SyntaxTests, self).setUp()
+ self.ldb = samba.tests.connect_samdb(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.schema_dn = self.ldb.get_schema_basedn().get_linearized()
+ self._setup_dn_string_test()
+ self._setup_dn_binary_test()
+
+ def _setup_dn_string_test(self):
+ """Testing DN+String syntax"""
+ attr_name = "test-Attr-DN-String" + time.strftime("%s", time.gmtime())
+ attr_ldap_display_name = attr_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+ldapDisplayName: """ + attr_ldap_display_name + """
+objectClass: top
+objectClass: attributeSchema
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.1.""" + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.14
+omSyntax: 127
+omObjectClass: \x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x0C
+isSingleValued: FALSE
+schemaIdGuid: """ + str(uuid.uuid4()) + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # search for created attribute
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (attr_name, self.schema_dn), scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0]["lDAPDisplayName"][0], attr_ldap_display_name)
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ class_name = "test-Class-DN-String" + time.strftime("%s", time.gmtime())
+ class_ldap_display_name = class_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.1.""" + str(random.randint(1, 100000)) + """
+schemaIdGuid: """ + str(uuid.uuid4()) + """
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemMayContain: """ + attr_ldap_display_name + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # search for created objectclass
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (class_name, self.schema_dn), scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0]["lDAPDisplayName"][0], class_ldap_display_name)
+ self.assertEqual(res[0]["defaultObjectCategory"][0], res[0]["distinguishedName"][0])
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ # store the class and the attribute
+ self.dn_string_class_ldap_display_name = class_ldap_display_name
+ self.dn_string_attribute = attr_ldap_display_name
+ self.dn_string_class_name = class_name
+
+ def _setup_dn_binary_test(self):
+ """Testing DN+Binary syntaxes"""
+ attr_name = "test-Attr-DN-Binary" + time.strftime("%s", time.gmtime())
+ attr_ldap_display_name = attr_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+ldapDisplayName: """ + attr_ldap_display_name + """
+objectClass: top
+objectClass: attributeSchema
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.2.""" + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.7
+omSyntax: 127
+omObjectClass: \x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x0B
+isSingleValued: FALSE
+schemaIdGuid: """ + str(uuid.uuid4()) + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # search for created attribute
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (attr_name, self.schema_dn), scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0]["lDAPDisplayName"][0], attr_ldap_display_name)
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ class_name = "test-Class-DN-Binary" + time.strftime("%s", time.gmtime())
+ class_ldap_display_name = class_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.2.""" + str(random.randint(1, 100000)) + """
+schemaIdGuid: """ + str(uuid.uuid4()) + """
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemMayContain: """ + attr_ldap_display_name + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # search for created objectclass
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (class_name, self.schema_dn), scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0]["lDAPDisplayName"][0], class_ldap_display_name)
+ self.assertEqual(res[0]["defaultObjectCategory"][0], res[0]["distinguishedName"][0])
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ # store the class and the attribute
+ self.dn_binary_class_ldap_display_name = class_ldap_display_name
+ self.dn_binary_attribute = attr_ldap_display_name
+ self.dn_binary_class_name = class_name
+
+ def _get_object_ldif(self, object_name, class_name, class_ldap_display_name, attr_name, attr_value):
+ # add object with correct syntax
+ ldif = """
+dn: CN=%s,CN=Users,%s""" % (object_name, self.base_dn) + """
+objectClass: organizationalPerson
+objectClass: person
+objectClass: """ + class_ldap_display_name + """
+objectClass: top
+cn: """ + object_name + """
+instanceType: 4
+objectCategory: CN=%s,%s""" % (class_name, self.schema_dn) + """
+distinguishedName: CN=%s,CN=Users,%s""" % (object_name, self.base_dn) + """
+name: """ + object_name + """
+""" + attr_name + attr_value + """
+"""
+ return ldif
+
+ def test_dn_string(self):
+ # add object with correct value
+ object_name1 = "obj-DN-String1" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:" + self.base_dn)
+ self.ldb.add_ldif(ldif)
+
+ # search by specifying the DN part only
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=%s)" % (self.dn_string_attribute, self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ # search by specifying the string part only
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=S:5:ABCDE)" % self.dn_string_attribute)
+ self.assertEqual(len(res), 0)
+
+ # search by DN+String
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=S:5:ABCDE:%s)" % (self.dn_string_attribute, self.base_dn))
+ self.assertEqual(len(res), 1)
+
+ # add object with wrong format
+ object_name2 = "obj-DN-String2" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name2, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCD:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_ATTRIBUTE_SYNTAX)
+
+ # add object with the same dn but with different string value in case
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:abcde:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different string value
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:FGHIJ:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different dn and string value
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:FGHIJ:" + self.schema_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different dn value
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:" + self.schema_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with GUID instead of DN
+ object_name3 = "obj-DN-String3" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name3, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:<GUID=%s>" % str(uuid.uuid4()))
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # add object with SID instead of DN
+ object_name4 = "obj-DN-String4" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name4, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:<SID=%s>" % self.ldb.get_domain_sid())
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # add object with random string instead of DN
+ object_name5 = "obj-DN-String5" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name5, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:randomSTRING")
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ def test_dn_binary(self):
+ # add object with correct value
+ object_name1 = "obj-DN-Binary1" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name1, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:" + self.base_dn)
+ self.ldb.add_ldif(ldif)
+
+ # search by specifyingthe DN part
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=%s)" % (self.dn_binary_attribute, self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ # search by specifying the binary part
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=B:4:1234)" % self.dn_binary_attribute)
+ self.assertEqual(len(res), 0)
+
+ # search by DN+Binary
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=B:4:1234:%s)" % (self.dn_binary_attribute, self.base_dn))
+ self.assertEqual(len(res), 1)
+
+ # add object with wrong format - 5 bytes instead of 4, 8, 16, 32...
+ object_name2 = "obj-DN-Binary2" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name2, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:5:67890:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.assertEqual(num, ERR_INVALID_ATTRIBUTE_SYNTAX)
+
+ # add object with the same dn but with different binary value
+ ldif = self._get_object_ldif(object_name1, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:5678:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e9:
+ (num, _) = e9.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different binary and dn value
+ ldif = self._get_object_ldif(object_name1, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:5678:" + self.schema_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different dn value
+ ldif = self._get_object_ldif(object_name1, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:" + self.schema_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e11:
+ (num, _) = e11.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with GUID instead of DN
+ object_name3 = "obj-DN-Binary3" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name3, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:<GUID=%s>" % str(uuid.uuid4()))
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e12:
+ (num, _) = e12.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # add object with SID instead of DN
+ object_name4 = "obj-DN-Binary4" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name4, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:<SID=%s>" % self.ldb.get_domain_sid())
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e13:
+ (num, _) = e13.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # add object with random string instead of DN
+ object_name5 = "obj-DN-Binary5" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name5, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:randomSTRING")
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/linked_attributes.py b/source4/dsdb/tests/python/linked_attributes.py
new file mode 100644
index 0000000..65a248a
--- /dev/null
+++ b/source4/dsdb/tests/python/linked_attributes.py
@@ -0,0 +1,734 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Originally based on ./sam.py
+import optparse
+import sys
+import os
+import itertools
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+from samba.dcerpc import misc
+
+parser = optparse.OptionParser("linked_attributes.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)
+
+parser.add_option('--delete-in-setup', action='store_true',
+ help="cleanup in setup")
+
+parser.add_option('--no-cleanup', action='store_true',
+ help="don't cleanup in teardown")
+
+parser.add_option('--no-reveal-internals', action='store_true',
+ help="Only use windows compatible ldap controls")
+
+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)
+
+
+class LATestException(Exception):
+ pass
+
+
+class LATests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(LATests, self).setUp()
+ self.samdb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.samdb.domain_dn()
+ self.ou = "OU=la,%s" % self.base_dn
+ if opts.delete_in_setup:
+ try:
+ self.samdb.delete(self.ou, ['tree_delete:1'])
+ except ldb.LdbError as e:
+ print("tried deleting %s, got error %s" % (self.ou, e))
+ self.samdb.add({'objectclass': 'organizationalUnit',
+ 'dn': self.ou})
+
+ def tearDown(self):
+ super(LATests, self).tearDown()
+ if not opts.no_cleanup:
+ self.samdb.delete(self.ou, ['tree_delete:1'])
+
+ def add_object(self, cn, objectclass, more_attrs={}):
+ dn = "CN=%s,%s" % (cn, self.ou)
+ attrs = {'cn': cn,
+ 'objectclass': objectclass,
+ 'dn': dn}
+ attrs.update(more_attrs)
+ self.samdb.add(attrs)
+
+ return dn
+
+ def add_objects(self, n, objectclass, prefix=None, more_attrs={}):
+ if prefix is None:
+ prefix = objectclass
+ dns = []
+ for i in range(n):
+ dns.append(self.add_object("%s%d" % (prefix, i + 1),
+ objectclass,
+ more_attrs=more_attrs))
+ return dns
+
+ def add_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_ADD, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def remove_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_DELETE, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def replace_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_REPLACE, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def attr_search(self, obj, attr, scope=ldb.SCOPE_BASE, **controls):
+ if opts.no_reveal_internals:
+ if 'reveal_internals' in controls:
+ del controls['reveal_internals']
+
+ controls = ['%s:%d' % (k, int(v)) for k, v in controls.items()]
+
+ res = self.samdb.search(obj,
+ scope=scope,
+ attrs=[attr],
+ controls=controls)
+ return res
+
+ def assert_links(self, obj, expected, attr, msg='', **kwargs):
+ res = self.attr_search(obj, attr, **kwargs)
+
+ if len(expected) == 0:
+ if attr in res[0]:
+ self.fail("found attr '%s' in %s" % (attr, res[0]))
+ return
+
+ try:
+ results = [str(x) for x in res[0][attr]]
+ except KeyError:
+ self.fail("missing attr '%s' on %s" % (attr, obj))
+
+ expected = sorted(expected)
+ results = sorted(results)
+
+ if expected != results:
+ print(msg)
+ print("expected %s" % expected)
+ print("received %s" % results)
+
+ self.assertEqual(results, expected)
+
+ def assert_back_links(self, obj, expected, attr='memberOf', **kwargs):
+ self.assert_links(obj, expected, attr=attr,
+ msg='back links do not match', **kwargs)
+
+ def assert_forward_links(self, obj, expected, attr='member', **kwargs):
+ self.assert_links(obj, expected, attr=attr,
+ msg='forward links do not match', **kwargs)
+
+ def get_object_guid(self, dn):
+ res = self.samdb.search(dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['objectGUID'])
+ return str(misc.GUID(res[0]['objectGUID'][0]))
+
+ def _test_la_backlinks(self, reveal=False):
+ tag = 'backlinks'
+ kwargs = {}
+ if reveal:
+ tag += '_reveal'
+ kwargs = {'reveal_internals': 0}
+
+ u1, u2 = self.add_objects(2, 'user', 'u_%s' % tag)
+ g1, g2 = self.add_objects(2, 'group', 'g_%s' % tag)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.assert_back_links(u1, [g1, g2], **kwargs)
+ self.assert_back_links(u2, [g2], **kwargs)
+
+ def test_la_backlinks(self):
+ self._test_la_backlinks()
+
+ def test_la_backlinks_reveal(self):
+ if opts.no_reveal_internals:
+ print('skipping because --no-reveal-internals')
+ return
+ self._test_la_backlinks(True)
+
+ def _test_la_backlinks_delete_group(self, reveal=False):
+ tag = 'del_group'
+ kwargs = {}
+ if reveal:
+ tag += '_reveal'
+ kwargs = {'reveal_internals': 0}
+
+ u1, u2 = self.add_objects(2, 'user', 'u_' + tag)
+ g1, g2 = self.add_objects(2, 'group', 'g_' + tag)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.samdb.delete(g2, ['tree_delete:1'])
+
+ self.assert_back_links(u1, [g1], **kwargs)
+ self.assert_back_links(u2, set(), **kwargs)
+
+ def test_la_backlinks_delete_group(self):
+ self._test_la_backlinks_delete_group()
+
+ def test_la_backlinks_delete_group_reveal(self):
+ if opts.no_reveal_internals:
+ print('skipping because --no-reveal-internals')
+ return
+ self._test_la_backlinks_delete_group(True)
+
+ def test_links_all_delete_group(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_all_del_group')
+ g1, g2 = self.add_objects(2, 'group', 'g_all_del_group')
+ g2guid = self.get_object_guid(g2)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.samdb.delete(g2)
+ self.assert_back_links(u1, [g1], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0)
+ self.assert_back_links(u2, set(), show_deleted=1, show_recycled=1,
+ show_deactivated_link=0)
+ self.assert_forward_links(g1, [u1], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0)
+ self.assert_forward_links('<GUID=%s>' % g2guid,
+ [], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0)
+
+ def test_links_all_delete_group_reveal(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_all_del_group_reveal')
+ g1, g2 = self.add_objects(2, 'group', 'g_all_del_group_reveal')
+ g2guid = self.get_object_guid(g2)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.samdb.delete(g2)
+ self.assert_back_links(u1, [g1], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+ self.assert_back_links(u2, set(), show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+ self.assert_forward_links(g1, [u1], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+ self.assert_forward_links('<GUID=%s>' % g2guid,
+ [], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+
+ def test_la_links_delete_link(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_del_link')
+ g1, g2 = self.add_objects(2, 'group', 'g_del_link')
+
+ res = self.samdb.search(g1, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ old_usn1 = int(res[0]['uSNChanged'][0])
+
+ self.add_linked_attribute(g1, u1)
+
+ res = self.samdb.search(g1, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ new_usn1 = int(res[0]['uSNChanged'][0])
+
+ self.assertNotEqual(old_usn1, new_usn1, "USN should have incremented")
+
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ res = self.samdb.search(g2, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ old_usn2 = int(res[0]['uSNChanged'][0])
+
+ self.remove_linked_attribute(g2, u1)
+
+ res = self.samdb.search(g2, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ new_usn2 = int(res[0]['uSNChanged'][0])
+
+ self.assertNotEqual(old_usn2, new_usn2, "USN should have incremented")
+
+ self.assert_forward_links(g1, [u1])
+ self.assert_forward_links(g2, [u2])
+
+ self.add_linked_attribute(g2, u1)
+ self.assert_forward_links(g2, [u1, u2])
+ self.remove_linked_attribute(g2, u2)
+ self.assert_forward_links(g2, [u1])
+ self.remove_linked_attribute(g2, u1)
+ self.assert_forward_links(g2, [])
+ self.remove_linked_attribute(g1, [])
+ self.assert_forward_links(g1, [])
+
+ # removing a duplicate link in the same message should fail
+ self.add_linked_attribute(g2, [u1, u2])
+ self.assertRaises(ldb.LdbError,
+ self.remove_linked_attribute, g2, [u1, u1])
+
+ def _test_la_links_delete_link_reveal(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_del_link_reveal')
+ g1, g2 = self.add_objects(2, 'group', 'g_del_link_reveal')
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.remove_linked_attribute(g2, u1)
+
+ self.assert_forward_links(g2, [u1, u2], show_deleted=1,
+ show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0
+ )
+
+ def test_la_links_delete_link_reveal(self):
+ if opts.no_reveal_internals:
+ print('skipping because --no-reveal-internals')
+ return
+ self._test_la_links_delete_link_reveal()
+
+ def test_la_links_delete_user(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_del_user')
+ g1, g2 = self.add_objects(2, 'group', 'g_del_user')
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ res = self.samdb.search(g1, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ old_usn1 = int(res[0]['uSNChanged'][0])
+
+ res = self.samdb.search(g2, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ old_usn2 = int(res[0]['uSNChanged'][0])
+
+ self.samdb.delete(u1)
+
+ self.assert_forward_links(g1, [])
+ self.assert_forward_links(g2, [u2])
+
+ res = self.samdb.search(g1, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ new_usn1 = int(res[0]['uSNChanged'][0])
+
+ res = self.samdb.search(g2, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ new_usn2 = int(res[0]['uSNChanged'][0])
+
+ # Assert the USN on the alternate object is unchanged
+ self.assertEqual(old_usn1, new_usn1)
+ self.assertEqual(old_usn2, new_usn2)
+
+ def test_la_links_delete_user_reveal(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_del_user_reveal')
+ g1, g2 = self.add_objects(2, 'group', 'g_del_user_reveal')
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.samdb.delete(u1)
+
+ self.assert_forward_links(g2, [u2],
+ show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+ self.assert_forward_links(g1, [],
+ show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+
+ def test_multiple_links(self):
+ u1, u2, u3, u4 = self.add_objects(4, 'user', 'u_multiple_links')
+ g1, g2, g3, g4 = self.add_objects(4, 'group', 'g_multiple_links')
+
+ self.add_linked_attribute(g1, [u1, u2, u3, u4])
+ self.add_linked_attribute(g2, [u3, u1])
+ self.add_linked_attribute(g3, u2)
+
+ self.assertRaisesLdbError(ldb.ERR_ENTRY_ALREADY_EXISTS,
+ "adding duplicate values",
+ self.add_linked_attribute, g2,
+ [u1, u2, u3, u2])
+
+ self.assert_forward_links(g1, [u1, u2, u3, u4])
+ self.assert_forward_links(g2, [u3, u1])
+ self.assert_forward_links(g3, [u2])
+ self.assert_back_links(u1, [g2, g1])
+ self.assert_back_links(u2, [g3, g1])
+ self.assert_back_links(u3, [g2, g1])
+ self.assert_back_links(u4, [g1])
+
+ self.remove_linked_attribute(g2, [u1, u3])
+ self.remove_linked_attribute(g1, [u1, u3])
+
+ self.assert_forward_links(g1, [u2, u4])
+ self.assert_forward_links(g2, [])
+ self.assert_forward_links(g3, [u2])
+ self.assert_back_links(u1, [])
+ self.assert_back_links(u2, [g3, g1])
+ self.assert_back_links(u3, [])
+ self.assert_back_links(u4, [g1])
+
+ self.add_linked_attribute(g1, [u1, u3])
+ self.add_linked_attribute(g2, [u3, u1])
+ self.add_linked_attribute(g3, [u1, u3])
+
+ self.assert_forward_links(g1, [u1, u2, u3, u4])
+ self.assert_forward_links(g2, [u1, u3])
+ self.assert_forward_links(g3, [u1, u2, u3])
+ self.assert_back_links(u1, [g1, g2, g3])
+ self.assert_back_links(u2, [g3, g1])
+ self.assert_back_links(u3, [g3, g2, g1])
+ self.assert_back_links(u4, [g1])
+
+ def test_la_links_replace(self):
+ u1, u2, u3, u4 = self.add_objects(4, 'user', 'u_replace')
+ g1, g2, g3, g4 = self.add_objects(4, 'group', 'g_replace')
+
+ self.add_linked_attribute(g1, [u1, u2])
+ self.add_linked_attribute(g2, [u1, u3])
+ self.add_linked_attribute(g3, u1)
+
+ self.replace_linked_attribute(g1, [u2])
+ self.replace_linked_attribute(g2, [u2, u3])
+ self.replace_linked_attribute(g3, [u1, u3])
+ self.replace_linked_attribute(g4, [u4])
+
+ self.assert_forward_links(g1, [u2])
+ self.assert_forward_links(g2, [u3, u2])
+ self.assert_forward_links(g3, [u3, u1])
+ self.assert_forward_links(g4, [u4])
+ self.assert_back_links(u1, [g3])
+ self.assert_back_links(u2, [g1, g2])
+ self.assert_back_links(u3, [g2, g3])
+ self.assert_back_links(u4, [g4])
+
+ self.replace_linked_attribute(g1, [u1, u2, u3])
+ self.replace_linked_attribute(g2, [u1])
+ self.replace_linked_attribute(g3, [u2])
+ self.replace_linked_attribute(g4, [])
+
+ self.assert_forward_links(g1, [u1, u2, u3])
+ self.assert_forward_links(g2, [u1])
+ self.assert_forward_links(g3, [u2])
+ self.assert_forward_links(g4, [])
+ self.assert_back_links(u1, [g1, g2])
+ self.assert_back_links(u2, [g1, g3])
+ self.assert_back_links(u3, [g1])
+ self.assert_back_links(u4, [])
+
+ self.assertRaisesLdbError(ldb.ERR_ENTRY_ALREADY_EXISTS,
+ "replacing duplicate values",
+ self.replace_linked_attribute, g2,
+ [u1, u2, u3, u2])
+
+ def test_la_links_replace2(self):
+ users = self.add_objects(12, 'user', 'u_replace2')
+ g1, = self.add_objects(1, 'group', 'g_replace2')
+
+ self.add_linked_attribute(g1, users[:6])
+ self.assert_forward_links(g1, users[:6])
+ self.replace_linked_attribute(g1, users)
+ self.assert_forward_links(g1, users)
+ self.replace_linked_attribute(g1, users[6:])
+ self.assert_forward_links(g1, users[6:])
+ self.remove_linked_attribute(g1, users[6:9])
+ self.assert_forward_links(g1, users[9:])
+ self.remove_linked_attribute(g1, users[9:])
+ self.assert_forward_links(g1, [])
+
+ def test_la_links_permutations(self):
+ """Make sure the order in which we add links doesn't matter."""
+ users = self.add_objects(3, 'user', 'u_permutations')
+ groups = self.add_objects(6, 'group', 'g_permutations')
+
+ for g, p in zip(groups, itertools.permutations(users)):
+ self.add_linked_attribute(g, p)
+
+ # everyone should be in every group
+ for g in groups:
+ self.assert_forward_links(g, users)
+
+ for u in users:
+ self.assert_back_links(u, groups)
+
+ for g, p in zip(groups[::-1], itertools.permutations(users)):
+ self.replace_linked_attribute(g, p)
+
+ for g in groups:
+ self.assert_forward_links(g, users)
+
+ for u in users:
+ self.assert_back_links(u, groups)
+
+ for g, p in zip(groups, itertools.permutations(users)):
+ self.remove_linked_attribute(g, p)
+
+ for g in groups:
+ self.assert_forward_links(g, [])
+
+ for u in users:
+ self.assert_back_links(u, [])
+
+ def test_la_links_relaxed(self):
+ """Check that the relax control doesn't mess with linked attributes."""
+ relax_control = ['relax:0']
+
+ users = self.add_objects(10, 'user', 'u_relax')
+ groups = self.add_objects(3, 'group', 'g_relax',
+ more_attrs={'member': users[:2]})
+ g_relax1, g_relax2, g_uptight = groups
+
+ # g_relax1 has all users added at once
+ # g_relax2 gets them one at a time in reverse order
+ # g_uptight never relaxes
+
+ self.add_linked_attribute(g_relax1, users[2:5], controls=relax_control)
+
+ for u in reversed(users[2:5]):
+ self.add_linked_attribute(g_relax2, u, controls=relax_control)
+ self.add_linked_attribute(g_uptight, u)
+
+ for g in groups:
+ self.assert_forward_links(g, users[:5])
+
+ self.add_linked_attribute(g, users[5:7])
+ self.assert_forward_links(g, users[:7])
+
+ for u in users[7:]:
+ self.add_linked_attribute(g, u)
+
+ self.assert_forward_links(g, users)
+
+ for u in users:
+ self.assert_back_links(u, groups)
+
+ # try some replacement permutations
+ import random
+ random.seed(1)
+ users2 = users[:]
+ for i in range(5):
+ random.shuffle(users2)
+ self.replace_linked_attribute(g_relax1, users2,
+ controls=relax_control)
+
+ self.assert_forward_links(g_relax1, users)
+
+ for i in range(5):
+ random.shuffle(users2)
+ self.remove_linked_attribute(g_relax2, users2,
+ controls=relax_control)
+ self.remove_linked_attribute(g_uptight, users2)
+
+ self.replace_linked_attribute(g_relax1, [], controls=relax_control)
+
+ random.shuffle(users2)
+ self.add_linked_attribute(g_relax2, users2,
+ controls=relax_control)
+ self.add_linked_attribute(g_uptight, users2)
+ self.replace_linked_attribute(g_relax1, users2,
+ controls=relax_control)
+
+ self.assert_forward_links(g_relax1, users)
+ self.assert_forward_links(g_relax2, users)
+ self.assert_forward_links(g_uptight, users)
+
+ for u in users:
+ self.assert_back_links(u, groups)
+
+ def test_add_all_at_once(self):
+ """All these other tests are creating linked attributes after the
+ objects are there. We want to test creating them all at once
+ using LDIF.
+ """
+ users = self.add_objects(7, 'user', 'u_all_at_once')
+ g1, g3 = self.add_objects(2, 'group', 'g_all_at_once',
+ more_attrs={'member': users})
+ (g2,) = self.add_objects(1, 'group', 'g_all_at_once2',
+ more_attrs={'member': users[:5]})
+
+ self.assertRaisesLdbError(ldb.ERR_ENTRY_ALREADY_EXISTS,
+ "adding multiple duplicate values",
+ self.add_objects, 1, 'group',
+ 'g_with_duplicate_links',
+ more_attrs={'member': users[:5] + users[1:2]})
+
+ self.assert_forward_links(g1, users)
+ self.assert_forward_links(g2, users[:5])
+ self.assert_forward_links(g3, users)
+ for u in users[:5]:
+ self.assert_back_links(u, [g1, g2, g3])
+ for u in users[5:]:
+ self.assert_back_links(u, [g1, g3])
+
+ self.remove_linked_attribute(g2, users[0])
+ self.remove_linked_attribute(g2, users[1])
+ self.add_linked_attribute(g2, users[1])
+ self.add_linked_attribute(g2, users[5])
+ self.add_linked_attribute(g2, users[6])
+
+ self.assert_forward_links(g1, users)
+ self.assert_forward_links(g2, users[1:])
+
+ for u in users[1:]:
+ self.remove_linked_attribute(g2, u)
+ self.remove_linked_attribute(g1, users)
+
+ for u in users:
+ self.samdb.delete(u)
+
+ self.assert_forward_links(g1, [])
+ self.assert_forward_links(g2, [])
+ self.assert_forward_links(g3, [])
+
+ def test_one_way_attributes(self):
+ e1, e2 = self.add_objects(2, 'msExchConfigurationContainer',
+ 'e_one_way')
+ guid = self.get_object_guid(e2)
+
+ self.add_linked_attribute(e1, e2, attr="addressBookRoots")
+ self.assert_forward_links(e1, [e2], attr='addressBookRoots')
+
+ self.samdb.delete(e2)
+
+ res = self.samdb.search("<GUID=%s>" % guid,
+ scope=ldb.SCOPE_BASE,
+ controls=['show_deleted:1',
+ 'show_recycled:1'])
+
+ new_dn = str(res[0].dn)
+ self.assert_forward_links(e1, [new_dn], attr='addressBookRoots')
+ self.assert_forward_links(e1, [new_dn],
+ attr='addressBookRoots',
+ show_deactivated_link=0)
+
+ def test_one_way_attributes_delete_link(self):
+ e1, e2 = self.add_objects(2, 'msExchConfigurationContainer',
+ 'e_one_way')
+ guid = self.get_object_guid(e2)
+
+ self.add_linked_attribute(e1, e2, attr="addressBookRoots")
+ self.assert_forward_links(e1, [e2], attr='addressBookRoots')
+
+ self.remove_linked_attribute(e1, e2, attr="addressBookRoots")
+
+ self.assert_forward_links(e1, [], attr='addressBookRoots')
+ self.assert_forward_links(e1, [], attr='addressBookRoots',
+ show_deactivated_link=0)
+
+ def test_pretend_one_way_attributes(self):
+ e1, e2 = self.add_objects(2, 'msExchConfigurationContainer',
+ 'e_one_way')
+ guid = self.get_object_guid(e2)
+
+ self.add_linked_attribute(e1, e2, attr="addressBookRoots2")
+ self.assert_forward_links(e1, [e2], attr='addressBookRoots2')
+
+ self.samdb.delete(e2)
+ res = self.samdb.search("<GUID=%s>" % guid,
+ scope=ldb.SCOPE_BASE,
+ controls=['show_deleted:1',
+ 'show_recycled:1'])
+
+ new_dn = str(res[0].dn)
+
+ self.assert_forward_links(e1, [], attr='addressBookRoots2')
+ self.assert_forward_links(e1, [], attr='addressBookRoots2',
+ show_deactivated_link=0)
+
+ def test_pretend_one_way_attributes_delete_link(self):
+ e1, e2 = self.add_objects(2, 'msExchConfigurationContainer',
+ 'e_one_way')
+ guid = self.get_object_guid(e2)
+
+ self.add_linked_attribute(e1, e2, attr="addressBookRoots2")
+ self.assert_forward_links(e1, [e2], attr='addressBookRoots2')
+
+ self.remove_linked_attribute(e1, e2, attr="addressBookRoots2")
+
+ self.assert_forward_links(e1, [], attr='addressBookRoots2')
+ self.assert_forward_links(e1, [], attr='addressBookRoots2',
+ show_deactivated_link=0)
+
+
+ def test_self_link(self):
+ e1, = self.add_objects(1, 'group',
+ 'e_self_link')
+
+ guid = self.get_object_guid(e1)
+ self.add_linked_attribute(e1, e1, attr="member")
+ self.assert_forward_links(e1, [e1], attr='member')
+ self.assert_back_links(e1, [e1], attr='memberOf')
+
+ try:
+ self.samdb.delete(e1)
+ except ldb.LdbError:
+ # Cope with the current bug to make this a failure
+ self.remove_linked_attribute(e1, e1, attr="member")
+ self.samdb.delete(e1)
+ self.fail("could not delete object with link to itself")
+
+ self.assert_forward_links('<GUID=%s>' % guid, [], attr='member',
+ show_deleted=1)
+ self.assert_forward_links('<GUID=%s>' % guid, [], attr='member',
+ show_deactivated_link=0,
+ show_deleted=1)
+ self.assert_back_links('<GUID=%s>' % guid, [], attr='memberOf',
+ show_deleted=1)
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/login_basics.py b/source4/dsdb/tests/python/login_basics.py
new file mode 100755
index 0000000..19cdf3e
--- /dev/null
+++ b/source4/dsdb/tests/python/login_basics.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Basic sanity-checks of user login. This sanity-checks that a user can login
+# over both NTLM and Kerberos, that incorrect passwords are rejected, and that
+# the user can change their password successfully.
+#
+# Copyright Andrew Bartlett 2018
+#
+import optparse
+import sys
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+import samba.getopt as options
+from samba.auth import system_session
+from samba.credentials import MUST_USE_KERBEROS
+from samba.dsdb import UF_NORMAL_ACCOUNT
+from samba.samdb import SamDB
+from password_lockout_base import BasePasswordTestCase
+
+sys.path.insert(0, "bin/python")
+
+parser = optparse.OptionParser("login_basics.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()
+global_creds = credopts.get_credentials(lp)
+
+
+#
+# Tests start here
+#
+class BasicUserAuthTests(BasePasswordTestCase):
+
+ def setUp(self):
+ self.host = host
+ self.host_url = "ldap://%s" % host
+ self.host_url_ldaps = "ldaps://%s" % host
+ self.lp = lp
+ self.global_creds = global_creds
+ self.ldb = SamDB(url=self.host_url, credentials=self.global_creds,
+ session_info=system_session(self.lp), lp=self.lp)
+ super(BasicUserAuthTests, self).setUp()
+
+ def _test_login_basics(self, creds, simple=False):
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ ldap_url = self.host_url
+ print("Performs a lockout attempt against LDAP using Kerberos")
+ elif simple:
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+ ldap_url = self.host_url_ldaps
+ print("Performs a lockout attempt against LDAP using Simple")
+ else:
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+ ldap_url = self.host_url
+ print("Performs a lockout attempt against LDAP using NTLM")
+
+ # get the initial logon values for this user
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=("greater", 0),
+ lastLogonTimestamp=("greater", 0),
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Initial test setup...')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+
+ test_creds = self.insta_creds(creds)
+
+ # check logging in with the wrong password fails
+ test_creds.set_password("thatsAcomplPASS1xBAD")
+ self.assertLoginFailure(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test login with wrong password')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # check logging in with the correct password succeeds
+ test_creds.set_password(userpass)
+ user_ldb = self.assertLoginSuccess(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test login with correct password')
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ # check that the user can change its password
+ new_password = "thatsAcomplPASS2"
+ user_ldb.modify_ldif("""
+dn: %s
+changetype: modify
+delete: userPassword
+userPassword: %s
+add: userPassword
+userPassword: %s
+""" % (userdn, userpass, new_password))
+
+ # discard the old creds (i.e. get rid of our valid Kerberos ticket)
+ del test_creds
+ test_creds = self.insta_creds(creds)
+ test_creds.set_password(userpass)
+
+ # for Kerberos, logging in with the old password fails
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ self.assertLoginFailure(ldap_url, test_creds, self.lp)
+ info_msg = 'Test Kerberos login with old password fails'
+ expectBadPwdTime = ("greater", badPasswordTime)
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=expectBadPwdTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg=info_msg)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ else:
+ # for NTLM, logging in with the old password succeeds
+ user_ldb = self.assertLoginSuccess(ldap_url, test_creds, self.lp)
+ if simple:
+ info_msg = 'Test simple-bind login with old password succeeds'
+ else:
+ info_msg = 'Test NTLM login with old password succeeds'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg=info_msg)
+
+ # check logging in with the new password succeeds
+ test_creds.set_password(new_password)
+ user_ldb = self.assertLoginSuccess(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test login with new password succeeds')
+
+ def test_login_basics_krb5(self):
+ self._test_login_basics(self.lockout1krb5_creds)
+
+ def test_login_basics_ntlm(self):
+ self._test_login_basics(self.lockout1ntlm_creds)
+
+ def test_login_basics_simple(self):
+ self._test_login_basics(self.lockout1simple_creds, simple=True)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ndr_pack_performance.py b/source4/dsdb/tests/python/ndr_pack_performance.py
new file mode 100644
index 0000000..3b31a3a
--- /dev/null
+++ b/source4/dsdb/tests/python/ndr_pack_performance.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import time
+import gzip
+
+from samba.netcmd.main import cmd_sambatool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import security
+from samba.dcerpc import drsuapi
+
+parser = optparse.OptionParser("ndr_pack_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+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)
+
+random.seed(1)
+
+
+BIG_SD_SDDL = ''.join(
+ """O:S-1-5-21-3328325300-3937145445-4190589019-512G:S-1-5-2
+1-3328325300-3937145445-4190589019-512D:AI(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;S-
+1-5-21-3328325300-3937145445-4190589019-512)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;
+SY)(A;;RPLCLORC;;;AU)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;AO)(A;;RPLCLORC;;;PS)(O
+A;;CR;ab721a55-1e2f-11d0-9819-00aa0040529b;;AU)(OA;;RP;46a9b11d-60ae-405a-b7e
+8-ff8a58d456d2;;S-1-5-32-560)(OA;CIIOID;RP;4c164200-20c0-11d0-a768-00aa006e05
+29;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;4c164200-20c0-11d0-a
+768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;5f2020
+10-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CI
+IOID;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa0030
+49e2;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc
+-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf96
+7aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;59ba2f42-79a2-11d0-9020-00c
+04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;59ba2f42-79a2
+-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP
+;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU
+)(OA;CIIOID;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-0
+0aa003049e2;RU)(OA;CIIOID;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0d
+e6-11d0-a285-00aa003049e2;ED)(OA;CIID;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608
+;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIOID;RP;b7c69e6d-2cc7-11d2-854
+e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIOID;RPLCLORC;;4
+828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIID;RPLCLORC;;bf967a9c-0de6-11d0-
+a285-00aa003049e2;RU)(OA;CIIOID;RPLCLORC;;bf967aba-0de6-11d0-a285-00aa003049e
+2;RU)(OA;CIID;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)(A;CIID;RPWPCRC
+CDCLCLORCWOWDSDDTSW;;;S-1-5-21-3328325300-3937145445-4190589019-519)(A;CIID;L
+C;;;RU)(A;CIID;RPWPCRCCLCLORCWOWDSDSW;;;BA)(OA;CIIOID;RP;4c164200-20c0-11d0-a
+768-00aa006e0529;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;4c1642
+00-20c0-11d0-a768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CI
+IOID;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e
+5f28;RU)(OA;CIIOID;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0
+-a285-00aa003049e2;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828
+cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c
+04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;59ba2f42-79a2
+-11d0-9020-00c04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP
+;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU
+)(OA;CIIOID;RP;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-a
+d6f015e5f28;RU)(OA;CIIOID;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0d
+e6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f6
+08;bf967a86-0de6-11d0-a285-00aa003049e2;ED)(OA;CIID;RP;b7c69e6d-2cc7-11d2-854
+e-00a0c983f608;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIOID;RP;b7c69e6d
+-2cc7-11d2-854e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIO
+ID;RPLCLORC;;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIID;RPLCLORC;;bf967
+a9c-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RPLCLORC;;bf967aba-0de6-11d0-a2
+85-00aa003049e2;RU)(OA;CIID;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)(
+A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;S-1-5-21-3328325300-3937145445-4190589019
+-519)(A;CIID;LC;;;RU)(A;CIID;RPWPCRCCLCLORCWOWDSDSW;;;BA)S:AI(OU;CIIOIDSA;WP;
+f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)
+(OU;CIIOIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-
+00aa003049e2;WD)(OU;CIIOIDSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5
+-0de6-11d0-a285-00aa003049e2;WD)(OU;CIIOIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f
+80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)""".split())
+
+LITTLE_SD_SDDL = ''.join(
+ """O:S-1-5-21-3328325300-3937145445-4190589019-512G:S-1-5-2
+1-3328325300-3937145445-4190589019-512D:AI(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;S-
+1-5-21-3328325300-3937145445-4190589019-512)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;
+SY)(A;;RPLCLORC;;;AU)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;AO)(A;;RPLCLORC;;;PS)(O
+A;;CR;ab721a55-1e2f-11d0-9819-00aa0040529b;;AU)(OA;;RP;46a9b11d-60ae-405a-b7e
+8-ff8a58d456d2;;S-1-5-32-560)(OA;CIIOID;RP;4c164200-20c0-11d0-a768-00aa006e05
+29;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;4c164200-20c0-11d0-a
+768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;5f2020
+10-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CI
+IOID;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa0030
+49e2;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc
+-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf96
+7aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;59ba2f42-79a2-11d0-9020-00c
+04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;59ba2f42-79a2
+-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP
+;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU
+)(OA;CIIOID;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-0
+0aa003049e2;RU)(OA;CIIOID;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0d
+e6-11d0-a285-00aa003049e2;ED)""".split())
+
+# set SCALE = 100 for normal test, or 1 for testing the test.
+SCALE = 100
+
+
+class UserTests(samba.tests.TestCase):
+
+ def get_file_blob(self, filename):
+ if filename.endswith('.gz'):
+ f = gzip.open(filename)
+ else:
+ f = open(filename)
+ return f.read()
+
+ def get_desc(self, sddl):
+ dummy_sid = security.dom_sid("S-2-0-0")
+ return security.descriptor.from_sddl(sddl, dummy_sid)
+
+ def get_blob(self, sddl):
+ return ndr_pack(self.get_desc(sddl))
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def _test_pack(self, unpacked, cycles=10000):
+ for i in range(SCALE * cycles):
+ ndr_pack(unpacked)
+
+ def _test_unpack(self, blob, cycles=10000, cls=security.descriptor):
+ for i in range(SCALE * cycles):
+ ndr_unpack(cls, blob)
+
+ def _test_pack_unpack(self, desc, cycles=5000, cls=security.descriptor):
+ blob2 = ndr_pack(desc)
+
+ for i in range(SCALE * cycles):
+ blob = ndr_pack(desc)
+ desc = ndr_unpack(cls, blob)
+
+ self.assertEqual(blob, blob2)
+
+ def test_pack_big_sd(self):
+ unpacked = self.get_desc(BIG_SD_SDDL)
+ self._test_pack(unpacked)
+
+ def test_unpack_big_sd(self):
+ blob = self.get_blob(BIG_SD_SDDL)
+ self._test_unpack(blob)
+
+ def test_pack_unpack_big_sd(self):
+ unpacked = self.get_desc(BIG_SD_SDDL)
+ self._test_pack_unpack(unpacked)
+
+ def test_pack_little_sd(self):
+ unpacked = self.get_desc(LITTLE_SD_SDDL)
+ self._test_pack(unpacked)
+
+ def test_unpack_little_sd(self):
+ blob = self.get_blob(LITTLE_SD_SDDL)
+ self._test_unpack(blob)
+
+ def test_pack_unpack_little_sd(self):
+ unpacked = self.get_desc(LITTLE_SD_SDDL)
+ self._test_pack_unpack(unpacked)
+
+ def test_unpack_repl_sample(self):
+ blob = self.get_file_blob('testdata/replication-ndrpack-example.gz')
+ self._test_unpack(blob, cycles=20, cls=drsuapi.DsGetNCChangesCtr6)
+
+ def test_pack_repl_sample(self):
+ blob = self.get_file_blob('testdata/replication-ndrpack-example.gz')
+ desc = ndr_unpack(drsuapi.DsGetNCChangesCtr6, blob)
+ self._test_pack(desc, cycles=20)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.makeSuite(UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/notification.py b/source4/dsdb/tests/python/notification.py
new file mode 100755
index 0000000..b9e1032
--- /dev/null
+++ b/source4/dsdb/tests/python/notification.py
@@ -0,0 +1,372 @@
+#!/usr/bin/env python3
+#
+# Unit tests for the notification control
+# Copyright (C) Stefan Metzmacher 2016
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import optparse
+import sys
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba import ldb
+from samba.samdb import SamDB
+from samba.ndr import ndr_unpack
+from samba import gensec
+from samba.credentials import Credentials
+import samba.tests
+
+from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHENTICATED, AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
+
+from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, LdbError
+from ldb import ERR_TIME_LIMIT_EXCEEDED, ERR_ADMIN_LIMIT_EXCEEDED, ERR_UNWILLING_TO_PERFORM
+from ldb import Message
+
+parser = optparse.OptionParser("notification.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)
+
+url = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class LDAPNotificationTest(samba.tests.TestCase):
+
+ def setUp(self):
+ super(LDAPNotificationTest, self).setUp()
+ self.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ self.user_sid_dn = "<SID=%s>" % str(ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["tokenGroups"][0]))
+
+ def test_simple_search(self):
+ """Testing a notification with an modify and a timeout"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ msg1 = None
+ search1 = self.ldb.search_iterator(base=self.user_sid_dn,
+ expression="(objectClass=*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name", "objectGUID", "displayName"])
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ self.assertIsNone(msg1)
+ msg1 = reply
+ res1 = search1.result()
+
+ search2 = self.ldb.search_iterator(base=self.base_dn,
+ expression="(objectClass=*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name", "objectGUID", "displayName"])
+ refs2 = 0
+ msg2 = None
+ for reply in search2:
+ if isinstance(reply, str):
+ refs2 += 1
+ continue
+ self.assertIsInstance(reply, ldb.Message)
+ if reply["objectGUID"][0] == msg1["objectGUID"][0]:
+ self.assertIsNone(msg2)
+ msg2 = reply
+ self.assertEqual(msg1.dn, msg2.dn)
+ self.assertEqual(len(msg1), len(msg2))
+ self.assertEqual(msg1["name"], msg2["name"])
+ #self.assertEqual(msg1["displayName"], msg2["displayName"])
+ res2 = search2.result()
+
+ self.ldb.modify_ldif("""
+dn: """ + self.user_sid_dn + """
+changetype: modify
+replace: otherLoginWorkstations
+otherLoginWorkstations: BEFORE"
+""")
+ notify1 = self.ldb.search_iterator(base=self.base_dn,
+ expression="(objectClass=*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name", "objectGUID", "displayName"],
+ controls=["notification:1"],
+ timeout=1)
+
+ self.ldb.modify_ldif("""
+dn: """ + self.user_sid_dn + """
+changetype: modify
+replace: otherLoginWorkstations
+otherLoginWorkstations: AFTER"
+""")
+
+ msg3 = None
+ for reply in notify1:
+ self.assertIsInstance(reply, ldb.Message)
+ if reply["objectGUID"][0] == msg1["objectGUID"][0]:
+ self.assertIsNone(msg3)
+ msg3 = reply
+ self.assertEqual(msg1.dn, msg3.dn)
+ self.assertEqual(len(msg1), len(msg3))
+ self.assertEqual(msg1["name"], msg3["name"])
+ #self.assertEqual(msg1["displayName"], msg3["displayName"])
+ try:
+ res = notify1.result()
+ self.fail()
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_TIME_LIMIT_EXCEEDED)
+ self.assertIsNotNone(msg3)
+
+ self.ldb.modify_ldif("""
+dn: """ + self.user_sid_dn + """
+changetype: delete
+delete: otherLoginWorkstations
+""")
+
+ def test_max_search(self):
+ """Testing the max allowed notifications"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ max_notifications = 5
+
+ notifies = [None] * (max_notifications + 1)
+ for i in range(0, max_notifications + 1):
+ notifies[i] = self.ldb.search_iterator(base=self.base_dn,
+ expression="(objectClass=*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=1)
+ num_admin_limit = 0
+ num_time_limit = 0
+ for i in range(0, max_notifications + 1):
+ try:
+ for msg in notifies[i]:
+ continue
+ res = notifies[i].result()
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ if num == ERR_ADMIN_LIMIT_EXCEEDED:
+ num_admin_limit += 1
+ continue
+ if num == ERR_TIME_LIMIT_EXCEEDED:
+ num_time_limit += 1
+ continue
+ raise
+ self.assertEqual(num_admin_limit, 1)
+ self.assertEqual(num_time_limit, max_notifications)
+
+ def test_invalid_filter(self):
+ """Testing invalid filters for notifications"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ valid_attrs = ["objectClass", "objectGUID", "distinguishedName", "name"]
+
+ for va in valid_attrs:
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=*)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=1)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_TIME_LIMIT_EXCEEDED)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(|(%s=*)(%s=value))" % (va, va),
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=1)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_TIME_LIMIT_EXCEEDED)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(&(%s=*)(%s=value))" % (va, va),
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=value)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s>=value)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s<=value)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=*value*)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(!(%s=*))" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ res = self.ldb.search(base=self.ldb.get_schema_basedn(),
+ expression="(objectClass=attributeSchema)",
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=["lDAPDisplayName"],
+ controls=["paged_results:1:2500"])
+ for msg in res:
+ va = str(msg["lDAPDisplayName"][0])
+ if va in valid_attrs:
+ continue
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=*)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e9:
+ (num, _) = e9.args
+ if num != ERR_UNWILLING_TO_PERFORM:
+ print("va[%s]" % va)
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ va = "noneAttributeName"
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=*)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e11:
+ (num, _) = e11.args
+ if num != ERR_UNWILLING_TO_PERFORM:
+ print("va[%s]" % va)
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+
+if "://" not in url:
+ if os.path.isfile(url):
+ url = "tdb://%s" % url
+ else:
+ url = "ldap://%s" % url
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/password_lockout.py b/source4/dsdb/tests/python/password_lockout.py
new file mode 100755
index 0000000..ed05029
--- /dev/null
+++ b/source4/dsdb/tests/python/password_lockout.py
@@ -0,0 +1,1708 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This tests the password lockout behavior for AD implementations
+#
+# Copyright Matthias Dieter Wallnoefer 2010
+# Copyright Andrew Bartlett 2013
+# Copyright Stefan Metzmacher 2014
+#
+
+import optparse
+import sys
+import base64
+import time
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+from samba.netcmd.main import cmd_sambatool
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS
+from ldb import SCOPE_BASE, LdbError
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_INVALID_CREDENTIALS
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE
+from samba import gensec, dsdb
+from samba.samdb import SamDB
+import samba.tests
+from samba.tests import delete_force
+from samba.dcerpc import security, samr
+from samba.ndr import ndr_unpack
+from samba.tests.pso import PasswordSettings
+from samba.net import Net
+from samba import NTSTATUSError, ntstatus
+import ctypes
+
+parser = optparse.OptionParser("password_lockout.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()
+global_creds = credopts.get_credentials(lp)
+
+import password_lockout_base
+
+#
+# Tests start here
+#
+
+
+class PasswordTests(password_lockout_base.BasePasswordTestCase):
+ def setUp(self):
+ self.host = host
+ self.host_url = "ldap://%s" % host
+ self.host_url_ldaps = "ldaps://%s" % host
+ self.lp = lp
+ self.global_creds = global_creds
+ self.ldb = SamDB(url=self.host_url, session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+ super(PasswordTests, self).setUp()
+
+ self.lockout2krb5_creds = self.insta_creds(self.template_creds,
+ username="lockout2krb5",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=MUST_USE_KERBEROS)
+ self._readd_user(self.lockout2krb5_creds,
+ lockOutObservationWindow=self.lockout_observation_window)
+ self.lockout2krb5_ldb = SamDB(url=self.host_url,
+ credentials=self.lockout2krb5_creds,
+ lp=lp)
+
+ self.lockout2ntlm_creds = self.insta_creds(self.template_creds,
+ username="lockout2ntlm",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._readd_user(self.lockout2ntlm_creds,
+ lockOutObservationWindow=self.lockout_observation_window)
+ self.lockout2ntlm_ldb = SamDB(url=self.host_url,
+ credentials=self.lockout2ntlm_creds,
+ lp=lp)
+
+
+ def use_pso_lockout_settings(self, creds):
+
+ # create a PSO with the lockout settings the test cases normally expect
+ #
+ # Some test cases sleep() for self.account_lockout_duration
+ pso = PasswordSettings("lockout-PSO", self.ldb, lockout_attempts=3,
+ lockout_duration=self.account_lockout_duration)
+ self.addCleanup(self.ldb.delete, pso.dn)
+
+ userdn = "cn=%s,cn=users,%s" % (creds.get_username(), self.base_dn)
+ pso.apply_to(userdn)
+
+ # update the global lockout settings to be wildly different to what
+ # the test cases normally expect
+ self.update_lockout_settings(threshold=10, duration=600,
+ observation_window=600)
+
+ def _reset_samr(self, res):
+
+ # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
+ samr_user = self._open_samr_user(res)
+ acb_info = self.samr.QueryUserInfo(samr_user, 16)
+ acb_info.acct_flags &= ~samr.ACB_AUTOLOCK
+ self.samr.SetUserInfo(samr_user, 16, acb_info)
+ self.samr.Close(samr_user)
+
+
+class PasswordTestsWithoutSleep(PasswordTests):
+ def setUp(self):
+ # The tests in this class do not sleep, so we can have a
+ # longer window and not flap on slower hosts
+ self.account_lockout_duration = 30
+ self.lockout_observation_window = 30
+ super(PasswordTestsWithoutSleep, self).setUp()
+
+ def _reset_ldap_lockoutTime(self, res):
+ self.ldb.modify_ldif("""
+dn: """ + str(res[0].dn) + """
+changetype: modify
+replace: lockoutTime
+lockoutTime: 0
+""")
+
+ def _reset_samba_tool(self, res):
+ username = res[0]["sAMAccountName"][0]
+
+ cmd = cmd_sambatool.subcommands['user'].subcommands['unlock']
+ result = cmd._run("samba-tool user unlock",
+ username,
+ "-H%s" % self.host_url,
+ "-U%s%%%s" % (global_creds.get_username(),
+ global_creds.get_password()))
+ self.assertEqual(result, None)
+
+ def _reset_ldap_userAccountControl(self, res):
+ self.assertTrue("userAccountControl" in res[0])
+ self.assertTrue("msDS-User-Account-Control-Computed" in res[0])
+
+ uac = int(res[0]["userAccountControl"][0])
+ uacc = int(res[0]["msDS-User-Account-Control-Computed"][0])
+
+ uac |= uacc
+ uac = uac & ~dsdb.UF_LOCKOUT
+
+ self.ldb.modify_ldif("""
+dn: """ + str(res[0].dn) + """
+changetype: modify
+replace: userAccountControl
+userAccountControl: %d
+""" % uac)
+
+ def _reset_by_method(self, res, method):
+ if method == "ldap_userAccountControl":
+ self._reset_ldap_userAccountControl(res)
+ elif method == "ldap_lockoutTime":
+ self._reset_ldap_lockoutTime(res)
+ elif method == "samr":
+ self._reset_samr(res)
+ elif method == "samba-tool":
+ self._reset_samba_tool(res)
+ else:
+ self.assertTrue(False, msg="Invalid reset method[%s]" % method)
+
+ def _test_userPassword_lockout_with_clear_change(self, creds, other_ldb, method,
+ initial_lastlogon_relation=None):
+ """
+ Tests user lockout behaviour when we try to change the user's password
+ but specify an incorrect old-password. The method parameter specifies
+ how to reset the locked out account (e.g. by resetting lockoutTime)
+ """
+ # Notice: This works only against Windows if "dSHeuristics" has been set
+ # properly
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ use_kerberos = creds.get_kerberos_state()
+ if use_kerberos == MUST_USE_KERBEROS:
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ print("Performs a password cleartext change operation on 'userPassword' using Kerberos")
+ else:
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+ print("Performs a password cleartext change operation on 'userPassword' using NTLMSSP")
+
+ if initial_lastlogon_relation is not None:
+ lastlogon_relation = initial_lastlogon_relation
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=(lastlogon_relation, 0),
+ lastLogonTimestamp=('greater', 0),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ if lastlogon_relation == 'greater':
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ # Change password on a connection as another user
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: """ + userpass + """
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e1:
+ (num, msg) = e1.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ print("two failed password change")
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e2:
+ (num, msg) = e2.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e3:
+ (num, msg) = e3.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000775' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000775' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ try:
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2x
+""")
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000775' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # Now reset the password, which does NOT change the lockout!
+ self.ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS2
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ try:
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2x
+""")
+ self.fail()
+ except LdbError as e6:
+ (num, msg) = e6.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000775' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ m = Message()
+ m.dn = Dn(self.ldb, userdn)
+ m["userAccountControl"] = MessageElement(
+ str(dsdb.UF_LOCKOUT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+
+ self.ldb.modify(m)
+
+ # This shows that setting the UF_LOCKOUT flag alone makes no difference
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # This shows that setting the UF_LOCKOUT flag makes no difference
+ try:
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e7:
+ (num, msg) = e7.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000775' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ self._reset_by_method(res, method)
+
+ # Here bad password counts are reset without logon success.
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The correct password after doing the unlock
+
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ userpass = "thatsAcomplPASS2x"
+ creds.set_password(userpass)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1xyz
+add: userPassword
+userPassword: thatsAcomplPASS2XYZ
+""")
+ self.fail()
+ except LdbError as e8:
+ (num, msg) = e8.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1xyz
+add: userPassword
+userPassword: thatsAcomplPASS2XYZ
+""")
+ self.fail()
+ except LdbError as e9:
+ (num, msg) = e9.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ self._reset_ldap_lockoutTime(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The following test lockout behaviour when modifying a user's password
+ # and specifying an invalid old password. There are variants for both
+ # NTLM and kerberos user authentication. As well as that, there are 3 ways
+ # to reset the locked out account: by clearing the lockout bit for
+ # userAccountControl (via LDAP), resetting it via SAMR, and by resetting
+ # the lockoutTime.
+ def test_userPassword_lockout_with_clear_change_krb5_ldap_userAccountControl(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "ldap_userAccountControl")
+
+ def test_userPassword_lockout_with_clear_change_krb5_ldap_lockoutTime(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "ldap_lockoutTime")
+
+ def test_userPassword_lockout_with_clear_change_krb5_samr(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "samr")
+
+ def test_userPassword_lockout_with_clear_change_ntlm_ldap_userAccountControl(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "ldap_userAccountControl",
+ initial_lastlogon_relation='greater')
+
+ def test_userPassword_lockout_with_clear_change_ntlm_ldap_lockoutTime(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "ldap_lockoutTime",
+ initial_lastlogon_relation='greater')
+
+ def test_userPassword_lockout_with_clear_change_ntlm_samr(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "samr",
+ initial_lastlogon_relation='greater')
+
+ # For PSOs, just test a selection of the above combinations
+ def test_pso_userPassword_lockout_with_clear_change_krb5_ldap_userAccountControl(self):
+ self.use_pso_lockout_settings(self.lockout1krb5_creds)
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "ldap_userAccountControl")
+
+ def test_pso_userPassword_lockout_with_clear_change_ntlm_ldap_lockoutTime(self):
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "ldap_lockoutTime",
+ initial_lastlogon_relation='greater')
+
+ def test_pso_userPassword_lockout_with_clear_change_ntlm_samr(self):
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "samr",
+ initial_lastlogon_relation='greater')
+
+ # just test "samba-tool user unlock" command once
+ def test_userPassword_lockout_with_clear_change_krb5_ldap_samba_tool(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "samba-tool")
+
+ def test_multiple_logon_krb5(self):
+ self._test_multiple_logon(self.lockout1krb5_creds)
+
+ def test_multiple_logon_ntlm(self):
+ self._test_multiple_logon(self.lockout1ntlm_creds)
+
+ def _test_samr_password_change(self, creds, other_creds, lockout_threshold=3):
+ """Tests user lockout by using bad password in SAMR password_change"""
+
+ # create a connection for SAMR using another user's credentials
+ lp = self.get_loadparm()
+ net = Net(other_creds, lp, server=self.host)
+
+ # work out the initial account values for this user
+ username = creds.get_username()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ badPwdCountOnly=True)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+
+ # prove we can change the user password (using the correct password)
+ new_password = "thatsAcomplPASS2"
+ net.change_password(newpassword=new_password,
+ username=username,
+ oldpassword=creds.get_password())
+ creds.set_password(new_password)
+
+ # try entering 'x' many bad passwords in a row to lock the user out
+ new_password = "thatsAcomplPASS3"
+ for i in range(lockout_threshold):
+ badPwdCount = i + 1
+ try:
+ print("Trying bad password, attempt #%u" % badPwdCount)
+ net.change_password(newpassword=new_password,
+ username=creds.get_username(),
+ oldpassword="bad-password")
+ self.fail("Invalid SAMR change_password accepted")
+ except NTSTATUSError as e:
+ enum = ctypes.c_uint32(e.args[0]).value
+ self.assertEqual(enum, ntstatus.NT_STATUS_WRONG_PASSWORD)
+
+ # check the status of the account is updated after each bad attempt
+ account_flags = 0
+ lockoutTime = None
+ if badPwdCount >= lockout_threshold:
+ account_flags = dsdb.UF_LOCKOUT
+ lockoutTime = ("greater", badPasswordTime)
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=account_flags)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # the user is now locked out
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # check the user remains locked out regardless of whether they use a
+ # good or a bad password now
+ for password in (creds.get_password(), "bad-password"):
+ try:
+ print("Trying password %s" % password)
+ net.change_password(newpassword=new_password,
+ username=creds.get_username(),
+ oldpassword=password)
+ self.fail("Invalid SAMR change_password accepted")
+ except NTSTATUSError as e:
+ enum = ctypes.c_uint32(e.args[0]).value
+ self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT)
+
+ res = self._check_account(userdn,
+ badPwdCount=lockout_threshold,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # reset the user account lockout
+ self._reset_samr(res)
+
+ # check bad password counts are reset
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # check we can change the user password successfully now
+ net.change_password(newpassword=new_password,
+ username=username,
+ oldpassword=creds.get_password())
+ creds.set_password(new_password)
+
+ def test_samr_change_password(self):
+ self._test_samr_password_change(self.lockout1ntlm_creds,
+ other_creds=self.lockout2ntlm_creds)
+
+ # same as above, but use a PSO to enforce the lockout
+ def test_pso_samr_change_password(self):
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_samr_password_change(self.lockout1ntlm_creds,
+ other_creds=self.lockout2ntlm_creds)
+
+ def test_ntlm_lockout_protected(self):
+ creds = self.lockout1ntlm_creds
+ self.assertEqual(DONT_USE_KERBEROS, creds.get_kerberos_state())
+
+ # Work out the initial account values for this user.
+ username = creds.get_username()
+ userdn = f'cn={username},cn=users,{self.base_dn}'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=('greater', 0),
+ badPwdCountOnly=True)
+ badPasswordTime = int(res[0]['badPasswordTime'][0])
+ logonCount = int(res[0]['logonCount'][0])
+ lastLogon = int(res[0]['lastLogon'][0])
+ lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ orig_msg = group_res[0]
+
+ # Add the user to the list of members.
+ members = list(orig_msg.get('member', ()))
+ self.assertNotIn(userdn, members, 'account already in Protected Users')
+ members.append(userdn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ cleanup = self.ldb.msg_diff(m, orig_msg)
+ self.ldb.modify(m)
+
+ password = creds.get_password()
+ creds.set_password('wrong_password')
+
+ lockout_threshold = 5
+
+ lp = self.get_loadparm()
+ server = f'ldap://{self.ldb.host_dns_name()}'
+
+ for _ in range(lockout_threshold):
+ with self.assertRaises(LdbError) as err:
+ SamDB(url=server,
+ credentials=creds,
+ lp=lp)
+
+ num, _ = err.exception.args
+ self.assertEqual(ERR_INVALID_CREDENTIALS, num)
+
+ res = self._check_account(
+ userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=None,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The user should not be locked out.
+ self.assertNotIn('lockoutTime', res[0],
+ 'account unexpectedly locked out')
+
+ # Move the account out of 'Protected Users'.
+ self.ldb.modify(cleanup)
+
+ # The account should not be locked out.
+ creds.set_password(password)
+
+ try:
+ SamDB(url=server,
+ credentials=creds,
+ lp=lp)
+ except LdbError:
+ self.fail('account unexpectedly locked out')
+
+ def test_samr_change_password_protected(self):
+ """Tests the SAMR password change method for Protected Users"""
+
+ creds = self.lockout1ntlm_creds
+ other_creds = self.lockout2ntlm_creds
+ lockout_threshold = 5
+
+ # Create a connection for SAMR using another user's credentials.
+ lp = self.get_loadparm()
+ net = Net(other_creds, lp, server=self.host)
+
+ # Work out the initial account values for this user.
+ username = creds.get_username()
+ userdn = f'cn={username},cn=users,{self.base_dn}'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=('greater', 0),
+ badPwdCountOnly=True)
+ badPasswordTime = int(res[0]['badPasswordTime'][0])
+ logonCount = int(res[0]['logonCount'][0])
+ lastLogon = int(res[0]['lastLogon'][0])
+ lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+ # prove we can change the user password (using the correct password)
+ new_password = 'thatsAcomplPASS1'
+ net.change_password(newpassword=new_password,
+ username=username,
+ oldpassword=creds.get_password())
+ creds.set_password(new_password)
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ orig_msg = group_res[0]
+
+ # Add the user to the list of members.
+ members = list(orig_msg.get('member', ()))
+ self.assertNotIn(userdn, members, 'account already in Protected Users')
+ members.append(userdn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ self.ldb.modify(m)
+
+ # Try entering the correct password 'x' times in a row, which should
+ # fail, but not lock the user out.
+ new_password = 'thatsAcomplPASS2'
+ for i in range(lockout_threshold):
+ with self.assertRaises(
+ NTSTATUSError,
+ msg='Invalid SAMR change_password accepted') as err:
+ print(f'Trying correct password, attempt #{i}')
+ net.change_password(newpassword=new_password,
+ username=username,
+ oldpassword=creds.get_password())
+
+ enum = ctypes.c_uint32(err.exception.args[0]).value
+ self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_RESTRICTION)
+
+ res = self._check_account(
+ userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=None,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The user should not be locked out.
+ self.assertNotIn('lockoutTime', res[0])
+
+ # Ensure that the password can still be changed via LDAP.
+ self.ldb.modify_ldif(f'''
+dn: {userdn}
+changetype: modify
+delete: userPassword
+userPassword: {creds.get_password()}
+add: userPassword
+userPassword: {new_password}
+''')
+
+ def test_samr_set_password_protected(self):
+ """Tests the SAMR password set method for Protected Users"""
+
+ creds = self.lockout1ntlm_creds
+ lockout_threshold = 5
+
+ # create a connection for SAMR using another user's credentials
+ lp = self.get_loadparm()
+ net = Net(self.global_creds, lp, server=self.host)
+
+ # work out the initial account values for this user
+ username = creds.get_username()
+ userdn = f'cn={username},cn=users,{self.base_dn}'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=('greater', 0),
+ badPwdCountOnly=True)
+ badPasswordTime = int(res[0]['badPasswordTime'][0])
+ logonCount = int(res[0]['logonCount'][0])
+ lastLogon = int(res[0]['lastLogon'][0])
+ lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+ # prove we can change the user password (using the correct password)
+ new_password = 'thatsAcomplPASS1'
+ net.set_password(newpassword=new_password,
+ account_name=username,
+ domain_name=creds.get_domain())
+ creds.set_password(new_password)
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ orig_msg = group_res[0]
+
+ # Add the user to the list of members.
+ members = list(orig_msg.get('member', ()))
+ self.assertNotIn(userdn, members, 'account already in Protected Users')
+ members.append(userdn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ self.ldb.modify(m)
+
+ # Try entering the correct password 'x' times in a row, which should
+ # fail, but not lock the user out.
+ new_password = 'thatsAcomplPASS2'
+ for i in range(lockout_threshold):
+ with self.assertRaises(
+ NTSTATUSError,
+ msg='Invalid SAMR set_password accepted') as err:
+ print(f'Trying correct password, attempt #{i}')
+ net.set_password(newpassword=new_password,
+ account_name=username,
+ domain_name=creds.get_domain())
+
+ enum = ctypes.c_uint32(err.exception.args[0]).value
+ self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_RESTRICTION)
+
+ res = self._check_account(
+ userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=None,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The user should not be locked out.
+ self.assertNotIn('lockoutTime', res[0])
+
+ # Ensure that the password can still be changed via LDAP.
+ self.ldb.modify_ldif(f'''
+dn: {userdn}
+changetype: modify
+delete: userPassword
+userPassword: {creds.get_password()}
+add: userPassword
+userPassword: {new_password}
+''')
+
+
+class PasswordTestsWithSleep(PasswordTests):
+ def setUp(self):
+ super(PasswordTestsWithSleep, self).setUp()
+
+ def _test_unicodePwd_lockout_with_clear_change(self, creds, other_ldb,
+ initial_logoncount_relation=None):
+ print("Performs a password cleartext change operation on 'unicodePwd'")
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+ if initial_logoncount_relation is not None:
+ logoncount_relation = initial_logoncount_relation
+ else:
+ logoncount_relation = "greater"
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=("greater", 0),
+ lastLogonTimestamp=("greater", 0),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ self.assertGreater(lastLogonTimestamp, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ # Change password on a connection as another user
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1x\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e10:
+ (num, msg) = e10.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Correct old password
+ old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
+ invalid_utf16 = "\"thatsAcomplPASSX\"".encode('utf-16-le')
+ userpass = "thatsAcomplPASS2"
+ creds.set_password(userpass)
+ new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
+
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e11:
+ (num, msg) = e11.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
+ # It doesn't create "lockoutTime" = 0 and doesn't
+ # reset "badPwdCount" = 0.
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ print("two failed password change")
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e12:
+ (num, msg) = e12.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ # this is strange, why do we have lockoutTime=badPasswordTime here?
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e13:
+ (num, msg) = e13.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000775' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e14:
+ (num, msg) = e14.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000775' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ try:
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e15:
+ (num, msg) = e15.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000775' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Correct old password
+ old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
+ invalid_utf16 = "\"thatsAcomplPASSiX\"".encode('utf-16-le')
+ userpass = "thatsAcomplPASS2x"
+ creds.set_password(userpass)
+ new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
+
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e16:
+ (num, msg) = e16.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e17:
+ (num, msg) = e17.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
+ # It doesn't reset "badPwdCount" = 0.
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e18:
+ (num, msg) = e18.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ time.sleep(self.account_lockout_duration + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=3, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
+ # It doesn't reset "lockoutTime" = 0 and doesn't
+ # reset "badPwdCount" = 0.
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=3, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ def test_unicodePwd_lockout_with_clear_change_krb5(self):
+ self._test_unicodePwd_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb)
+
+ def test_unicodePwd_lockout_with_clear_change_ntlm(self):
+ self._test_unicodePwd_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ initial_logoncount_relation="equal")
+
+ def test_login_lockout_krb5(self):
+ self._test_login_lockout(self.lockout1krb5_creds)
+
+ def test_login_lockout_ntlm(self):
+ self._test_login_lockout(self.lockout1ntlm_creds)
+
+ # Repeat the login lockout tests using PSOs
+ def test_pso_login_lockout_krb5(self):
+ """Check the PSO lockout settings get applied to the user correctly"""
+ self.use_pso_lockout_settings(self.lockout1krb5_creds)
+ self._test_login_lockout(self.lockout1krb5_creds)
+
+ def test_pso_login_lockout_ntlm(self):
+ """Check the PSO lockout settings get applied to the user correctly"""
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_login_lockout(self.lockout1ntlm_creds)
+
+ def _testing_add_user(self, creds, lockOutObservationWindow=0):
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ use_kerberos = creds.get_kerberos_state()
+ if use_kerberos == MUST_USE_KERBEROS:
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ else:
+ logoncount_relation = 'equal'
+ if lockOutObservationWindow == 0:
+ lastlogon_relation = 'greater'
+ else:
+ lastlogon_relation = 'equal'
+
+ delete_force(self.ldb, userdn)
+ self.ldb.add({
+ "dn": userdn,
+ "objectclass": "user",
+ "sAMAccountName": username})
+
+ self.addCleanup(delete_force, self.ldb, userdn)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=0,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
+ dsdb.UF_ACCOUNTDISABLE |
+ dsdb.UF_PASSWD_NOTREQD),
+ msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
+
+ # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
+ # It doesn't create "lockoutTime" = 0.
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=0,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
+ dsdb.UF_ACCOUNTDISABLE |
+ dsdb.UF_PASSWD_NOTREQD),
+ msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
+
+ # Tests a password change when we don't have any password yet with a
+ # wrong old password
+ try:
+ self.ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: noPassword
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e19:
+ (num, msg) = e19.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ # Windows (2008 at least) seems to have some small bug here: it
+ # returns "0000056A" on longer (always wrong) previous passwords.
+ self.assertTrue('00000056' in msg, msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", 0),
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
+ dsdb.UF_ACCOUNTDISABLE |
+ dsdb.UF_PASSWD_NOTREQD),
+ msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
+ badPwdCount = int(res[0]["badPwdCount"][0])
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Sets the initial user password with a "special" password change
+ # I think that this internally is a password set operation and it can
+ # only be performed by someone which has password set privileges on the
+ # account (at least in s4 we do handle it like that).
+ self.ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+add: userPassword
+userPassword: """ + userpass + """
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ badPasswordTime=badPasswordTime,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
+ dsdb.UF_ACCOUNTDISABLE |
+ dsdb.UF_PASSWD_NOTREQD),
+ msDSUserAccountControlComputed=0)
+
+ # Enables the user account
+ self.ldb.enable_account("(sAMAccountName=%s)" % username)
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ badPasswordTime=badPasswordTime,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ if lockOutObservationWindow != 0:
+ time.sleep(lockOutObservationWindow + 1)
+ effective_bad_password_count = 0
+ else:
+ effective_bad_password_count = badPwdCount
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ effective_bad_password_count=effective_bad_password_count,
+ badPasswordTime=badPasswordTime,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ ldb = SamDB(url=self.host_url, credentials=creds, lp=self.lp)
+
+ if lockOutObservationWindow == 0:
+ badPwdCount = 0
+ effective_bad_password_count = 0
+ if use_kerberos == MUST_USE_KERBEROS:
+ badPwdCount = 0
+ effective_bad_password_count = 0
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ effective_bad_password_count=effective_bad_password_count,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, 0),
+ lastLogon=(lastlogon_relation, 0),
+ lastLogonTimestamp=('greater', badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ if lastlogon_relation == 'greater':
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ effective_bad_password_count=effective_bad_password_count,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ return ldb
+
+ def test_lockout_observation_window(self):
+ lockout3krb5_creds = self.insta_creds(self.template_creds,
+ username="lockout3krb5",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=MUST_USE_KERBEROS)
+ self._testing_add_user(lockout3krb5_creds)
+
+ lockout4krb5_creds = self.insta_creds(self.template_creds,
+ username="lockout4krb5",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=MUST_USE_KERBEROS)
+ self._testing_add_user(lockout4krb5_creds,
+ lockOutObservationWindow=self.lockout_observation_window)
+
+ lockout3ntlm_creds = self.insta_creds(self.template_creds,
+ username="lockout3ntlm",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._testing_add_user(lockout3ntlm_creds)
+ lockout4ntlm_creds = self.insta_creds(self.template_creds,
+ username="lockout4ntlm",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._testing_add_user(lockout4ntlm_creds,
+ lockOutObservationWindow=self.lockout_observation_window)
+
+class PasswordTestsWithDefaults(PasswordTests):
+ def setUp(self):
+ # The tests in this class do not sleep, so we can use the default
+ # timeout windows here
+ self.account_lockout_duration = 30 * 60
+ self.lockout_observation_window = 30 * 60
+ super(PasswordTestsWithDefaults, self).setUp()
+
+ # sanity-check that user lockout works with the default settings (we just
+ # check the user is locked out - we don't wait for the lockout to expire)
+ def test_login_lockout_krb5(self):
+ self._test_login_lockout(self.lockout1krb5_creds,
+ wait_lockout_duration=False)
+
+ def test_login_lockout_ntlm(self):
+ self._test_login_lockout(self.lockout1ntlm_creds,
+ wait_lockout_duration=False)
+
+ # Repeat the login lockout tests using PSOs
+ def test_pso_login_lockout_krb5(self):
+ """Check the PSO lockout settings get applied to the user correctly"""
+ self.use_pso_lockout_settings(self.lockout1krb5_creds)
+ self._test_login_lockout(self.lockout1krb5_creds,
+ wait_lockout_duration=False)
+
+ def test_pso_login_lockout_ntlm(self):
+ """Check the PSO lockout settings get applied to the user correctly"""
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_login_lockout(self.lockout1ntlm_creds,
+ wait_lockout_duration=False)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/password_lockout_base.py b/source4/dsdb/tests/python/password_lockout_base.py
new file mode 100644
index 0000000..93371ef
--- /dev/null
+++ b/source4/dsdb/tests/python/password_lockout_base.py
@@ -0,0 +1,787 @@
+import samba
+
+from samba.auth import system_session
+from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS
+from ldb import SCOPE_BASE, LdbError
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_INVALID_CREDENTIALS
+from ldb import SUCCESS as LDB_SUCCESS
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE
+from samba import gensec, dsdb
+from samba.samdb import SamDB
+import samba.tests
+from samba.tests import delete_force
+from samba.dcerpc import security, samr
+from samba.ndr import ndr_unpack
+from samba.tests.password_test import PasswordTestCase
+
+import time
+
+
+class BasePasswordTestCase(PasswordTestCase):
+ def _open_samr_user(self, res):
+ self.assertTrue("objectSid" in res[0])
+
+ (domain_sid, rid) = ndr_unpack(security.dom_sid, res[0]["objectSid"][0]).split()
+ self.assertEqual(self.domain_sid, domain_sid)
+
+ return self.samr.OpenUser(self.samr_domain, security.SEC_FLAG_MAXIMUM_ALLOWED, rid)
+
+ def _check_attribute(self, res, name, value):
+ if value is None:
+ self.assertTrue(name not in res[0],
+ msg="attr[%s]=%r on dn[%s]" %
+ (name, res[0], res[0].dn))
+ return
+
+ if isinstance(value, tuple):
+ (mode, value) = value
+ else:
+ mode = "equal"
+
+ if mode == "ignore":
+ return
+
+ if mode == "absent":
+ self.assertFalse(name in res[0],
+ msg="attr[%s] not missing on dn[%s]" %
+ (name, res[0].dn))
+ return
+
+ self.assertTrue(name in res[0],
+ msg="attr[%s] missing on dn[%s]" %
+ (name, res[0].dn))
+ self.assertTrue(len(res[0][name]) == 1,
+ msg="attr[%s]=%r on dn[%s]" %
+ (name, res[0][name], res[0].dn))
+
+ print("%s = '%s'" % (name, res[0][name][0]))
+
+ if mode == "present":
+ return
+
+ if mode == "equal":
+ v = int(res[0][name][0])
+ value = int(value)
+ msg = ("attr[%s]=[%s] != [%s] on dn[%s]\n"
+ "(diff %d; actual value is %s than expected)" %
+ (name, v, value, res[0].dn, v - value,
+ ('less' if v < value else 'greater')))
+
+ self.assertTrue(v == value, msg)
+ return
+
+ if mode == "greater":
+ v = int(res[0][name][0])
+ self.assertTrue(v > int(value),
+ msg="attr[%s]=[%s] <= [%s] on dn[%s] (diff %d)" %
+ (name, v, int(value), res[0].dn, v - int(value)))
+ return
+ if mode == "less":
+ v = int(res[0][name][0])
+ self.assertTrue(v < int(value),
+ msg="attr[%s]=[%s] >= [%s] on dn[%s] (diff %d)" %
+ (name, v, int(value), res[0].dn, v - int(value)))
+ return
+ self.assertEqual(mode, not mode, "Invalid Mode[%s]" % mode)
+
+ def _check_account_initial(self, userdn):
+ self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=0,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=("absent", None),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ def _check_account(self, dn,
+ badPwdCount=None,
+ badPasswordTime=None,
+ logonCount=None,
+ lastLogon=None,
+ lastLogonTimestamp=None,
+ lockoutTime=None,
+ userAccountControl=None,
+ msDSUserAccountControlComputed=None,
+ effective_bad_password_count=None,
+ msg=None,
+ badPwdCountOnly=False):
+ print('-=' * 36)
+ if msg is not None:
+ print("\033[01;32m %s \033[00m\n" % msg)
+ attrs = [
+ "objectSid",
+ "sAMAccountName",
+ "badPwdCount",
+ "badPasswordTime",
+ "lastLogon",
+ "lastLogonTimestamp",
+ "logonCount",
+ "lockoutTime",
+ "userAccountControl",
+ "msDS-User-Account-Control-Computed"
+ ]
+
+ # in order to prevent some time resolution problems we sleep for
+ # 10 micro second
+ time.sleep(0.01)
+
+ res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs)
+ self.assertTrue(len(res) == 1)
+ self._check_attribute(res, "badPwdCount", badPwdCount)
+ self._check_attribute(res, "lockoutTime", lockoutTime)
+ self._check_attribute(res, "badPasswordTime", badPasswordTime)
+ if not badPwdCountOnly:
+ self._check_attribute(res, "logonCount", logonCount)
+ self._check_attribute(res, "lastLogon", lastLogon)
+ self._check_attribute(res, "lastLogonTimestamp", lastLogonTimestamp)
+ self._check_attribute(res, "userAccountControl", userAccountControl)
+ self._check_attribute(res, "msDS-User-Account-Control-Computed",
+ msDSUserAccountControlComputed)
+
+ lastLogon = int(res[0]["lastLogon"][0])
+ logonCount = int(res[0]["logonCount"][0])
+
+ samr_user = self._open_samr_user(res)
+ uinfo3 = self.samr.QueryUserInfo(samr_user, 3)
+ uinfo5 = self.samr.QueryUserInfo(samr_user, 5)
+ uinfo16 = self.samr.QueryUserInfo(samr_user, 16)
+ uinfo21 = self.samr.QueryUserInfo(samr_user, 21)
+ self.samr.Close(samr_user)
+
+ expected_acb_info = 0
+ if not badPwdCountOnly:
+ if userAccountControl & dsdb.UF_NORMAL_ACCOUNT:
+ expected_acb_info |= samr.ACB_NORMAL
+ if userAccountControl & dsdb.UF_ACCOUNTDISABLE:
+ expected_acb_info |= samr.ACB_DISABLED
+ if userAccountControl & dsdb.UF_PASSWD_NOTREQD:
+ expected_acb_info |= samr.ACB_PWNOTREQ
+ if msDSUserAccountControlComputed & dsdb.UF_LOCKOUT:
+ expected_acb_info |= samr.ACB_AUTOLOCK
+ if msDSUserAccountControlComputed & dsdb.UF_PASSWORD_EXPIRED:
+ expected_acb_info |= samr.ACB_PW_EXPIRED
+
+ self.assertEqual(uinfo3.acct_flags, expected_acb_info)
+ self.assertEqual(uinfo3.last_logon, lastLogon)
+ self.assertEqual(uinfo3.logon_count, logonCount)
+
+ expected_bad_password_count = 0
+ if badPwdCount is not None:
+ expected_bad_password_count = badPwdCount
+ if effective_bad_password_count is None:
+ effective_bad_password_count = expected_bad_password_count
+
+ self.assertEqual(uinfo3.bad_password_count, expected_bad_password_count)
+
+ if not badPwdCountOnly:
+ self.assertEqual(uinfo5.acct_flags, expected_acb_info)
+ self.assertEqual(uinfo5.bad_password_count, effective_bad_password_count)
+ self.assertEqual(uinfo5.last_logon, lastLogon)
+ self.assertEqual(uinfo5.logon_count, logonCount)
+
+ self.assertEqual(uinfo16.acct_flags, expected_acb_info)
+
+ self.assertEqual(uinfo21.acct_flags, expected_acb_info)
+ self.assertEqual(uinfo21.bad_password_count, effective_bad_password_count)
+ self.assertEqual(uinfo21.last_logon, lastLogon)
+ self.assertEqual(uinfo21.logon_count, logonCount)
+
+ # check LDAP again and make sure the samr.QueryUserInfo
+ # doesn't have any impact.
+ res2 = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs)
+ self.assertEqual(res[0], res2[0])
+
+ # in order to prevent some time resolution problems we sleep for
+ # 10 micro second
+ time.sleep(0.01)
+ return res
+
+ def update_lockout_settings(self, threshold, duration, observation_window):
+ """Updates the global user lockout settings"""
+ m = Message()
+ m.dn = Dn(self.ldb, self.base_dn)
+ account_lockout_duration_ticks = -int(duration * (1e7))
+ m["lockoutDuration"] = MessageElement(str(account_lockout_duration_ticks),
+ FLAG_MOD_REPLACE, "lockoutDuration")
+ m["lockoutThreshold"] = MessageElement(str(threshold),
+ FLAG_MOD_REPLACE, "lockoutThreshold")
+ lockout_observation_window_ticks = -int(observation_window * (1e7))
+ m["lockOutObservationWindow"] = MessageElement(str(lockout_observation_window_ticks),
+ FLAG_MOD_REPLACE, "lockOutObservationWindow")
+ self.ldb.modify(m)
+
+ def _readd_user(self, creds, lockOutObservationWindow=0, simple=False):
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ if simple:
+ creds.set_bind_dn(userdn)
+ ldap_url = self.host_url_ldaps
+ else:
+ ldap_url = self.host_url
+
+ delete_force(self.ldb, userdn)
+ self.ldb.add({
+ "dn": userdn,
+ "objectclass": "user",
+ "sAMAccountName": username})
+
+ self.addCleanup(delete_force, self.ldb, userdn)
+
+ # Sets the initial user password with a "special" password change
+ # I think that this internally is a password set operation and it can
+ # only be performed by someone which has password set privileges on the
+ # account (at least in s4 we do handle it like that).
+ self.ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+add: userPassword
+userPassword: """ + userpass + """
+""")
+ # Enables the user account
+ self.ldb.enable_account("(sAMAccountName=%s)" % username)
+
+ use_kerberos = creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+ if simple:
+ fail_creds.set_bind_dn(userdn)
+
+ self._check_account_initial(userdn)
+
+ # Fail once to get a badPasswordTime
+ self.assertLoginFailure(ldap_url, fail_creds, self.lp)
+
+ # Always reset with Simple bind or Kerberos, allows testing without NTLM
+ if simple or use_kerberos == MUST_USE_KERBEROS:
+ success_creds = creds
+ else:
+ success_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass)
+ success_creds.set_bind_dn(userdn)
+ ldap_url = self.host_url_ldaps
+
+ # Succeed to reset everything to 0
+ self.assertLoginSuccess(ldap_url, success_creds, self.lp)
+
+ def assertLoginFailure(self, url, creds, lp, errno=ERR_INVALID_CREDENTIALS):
+ try:
+ ldb = SamDB(url=url, credentials=creds, lp=lp)
+ self.fail("Login unexpectedly succeeded")
+ except LdbError as e1:
+ (num, msg) = e1.args
+ if errno is not None:
+ self.assertEqual(num, errno, ("Login failed in the wrong way"
+ "(got err %d, expected %d)" %
+ (num, errno)))
+
+ def assertLoginSuccess(self, url, creds, lp):
+ try:
+ ldb = SamDB(url=url, credentials=creds, lp=lp)
+ return ldb
+ except LdbError as e1:
+ (num, msg) = e1.args
+ self.assertEqual(num, LDB_SUCCESS,
+ ("Login failed - %d - %s" % (
+ num, msg)))
+
+
+ def setUp(self):
+ super(BasePasswordTestCase, self).setUp()
+
+ self.global_creds.set_gensec_features(self.global_creds.get_gensec_features() |
+ gensec.FEATURE_SEAL)
+
+ self.template_creds = Credentials()
+ self.template_creds.set_username("testuser")
+ self.template_creds.set_password("thatsAcomplPASS1")
+ self.template_creds.set_domain(self.global_creds.get_domain())
+ self.template_creds.set_realm(self.global_creds.get_realm())
+ self.template_creds.set_workstation(self.global_creds.get_workstation())
+ self.template_creds.set_gensec_features(self.global_creds.get_gensec_features())
+ self.template_creds.set_kerberos_state(self.global_creds.get_kerberos_state())
+
+ # Gets back the basedn
+ base_dn = self.ldb.domain_dn()
+
+ # Gets back the configuration basedn
+ configuration_dn = self.ldb.get_config_basedn().get_linearized()
+
+ res = self.ldb.search(base_dn,
+ scope=SCOPE_BASE, attrs=["lockoutDuration", "lockOutObservationWindow", "lockoutThreshold"])
+
+ if "lockoutDuration" in res[0]:
+ lockoutDuration = res[0]["lockoutDuration"][0]
+ else:
+ lockoutDuration = 0
+
+ if "lockoutObservationWindow" in res[0]:
+ lockoutObservationWindow = res[0]["lockoutObservationWindow"][0]
+ else:
+ lockoutObservationWindow = 0
+
+ if "lockoutThreshold" in res[0]:
+ lockoutThreshold = res[0]["lockoutThreshold"][0]
+ else:
+ lockoutTreshold = 0
+
+ self.addCleanup(self.ldb.modify_ldif, """
+dn: """ + base_dn + """
+changetype: modify
+replace: lockoutDuration
+lockoutDuration: """ + str(lockoutDuration) + """
+replace: lockoutObservationWindow
+lockoutObservationWindow: """ + str(lockoutObservationWindow) + """
+replace: lockoutThreshold
+lockoutThreshold: """ + str(lockoutThreshold) + """
+""")
+
+ self.base_dn = self.ldb.domain_dn()
+
+ #
+ # Some test cases sleep() for self.account_lockout_duration
+ # so allow it to be controlled via the subclass
+ #
+ if not hasattr(self, 'account_lockout_duration'):
+ self.account_lockout_duration = 3
+ if not hasattr(self, 'lockout_observation_window'):
+ self.lockout_observation_window = 3
+ self.update_lockout_settings(threshold=3,
+ duration=self.account_lockout_duration,
+ observation_window=self.lockout_observation_window)
+
+ # update DC to allow password changes for the duration of this test
+ self.allow_password_changes()
+
+ self.domain_sid = security.dom_sid(self.ldb.get_domain_sid())
+ self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % self.host, self.lp, self.global_creds)
+ self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
+
+ self.addCleanup(self.delete_ldb_connections)
+
+ # (Re)adds the test user accounts
+ self.lockout1krb5_creds = self.insta_creds(self.template_creds,
+ username="lockout1krb5",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=MUST_USE_KERBEROS)
+ self._readd_user(self.lockout1krb5_creds)
+ self.lockout1ntlm_creds = self.insta_creds(self.template_creds,
+ username="lockout1ntlm",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._readd_user(self.lockout1ntlm_creds)
+ self.lockout1simple_creds = self.insta_creds(self.template_creds,
+ username="lockout1simple",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._readd_user(self.lockout1simple_creds,
+ simple=True)
+
+ def delete_ldb_connections(self):
+ del self.ldb
+
+ def tearDown(self):
+ super(BasePasswordTestCase, self).tearDown()
+
+ def _test_login_lockout(self, creds, wait_lockout_duration=True):
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ use_kerberos = creds.get_kerberos_state()
+ # This unlocks by waiting for account_lockout_duration
+ if use_kerberos == MUST_USE_KERBEROS:
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ print("Performs a lockout attempt against LDAP using Kerberos")
+ else:
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+ print("Performs a lockout attempt against LDAP using NTLM")
+
+ # Change password on a connection as another user
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=("greater", 0),
+ lastLogonTimestamp=("greater", 0),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ firstLogon = lastLogon
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ print(firstLogon)
+ print(lastLogonTimestamp)
+
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ creds_lockout = self.insta_creds(creds)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='lastlogontimestamp with wrong password')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Correct old password
+ creds_lockout.set_password(userpass)
+
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ # lastLogonTimestamp should not change
+ # lastLogon increases if badPwdCount is non-zero (!)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='LLTimestamp is updated to lastlogon')
+
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+
+ except LdbError as e2:
+ (num, msg) = e2.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ print("two failed password change")
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+
+ except LdbError as e3:
+ (num, msg) = e3.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # The correct password, but we are locked out
+ creds_lockout.set_password(userpass)
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e6:
+ (num, msg) = e6.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # if we're just checking the user gets locked out, we can stop here
+ if not wait_lockout_duration:
+ return
+
+ # wait for the lockout to end
+ time.sleep(self.account_lockout_duration + 1)
+ print(self.account_lockout_duration + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=3, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The correct password after letting the timeout expire
+
+ creds_lockout.set_password(userpass)
+
+ creds_lockout2 = self.insta_creds(creds_lockout)
+
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout2, lp=self.lp)
+ time.sleep(3)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg="lastLogon is way off")
+
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e7:
+ (num, msg) = e7.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e8:
+ (num, msg) = e8.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ time.sleep(self.lockout_observation_window + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=2, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e9:
+ (num, msg) = e9.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The correct password without letting the timeout expire
+ creds_lockout.set_password(userpass)
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lockoutTime=0,
+ lastLogon=("greater", lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ def _test_multiple_logon(self, creds):
+ # Test the happy case in which a user logs on correctly, then
+ # logs on correctly again, so that the bad password and
+ # lockout times are both zero the second time. The lastlogon
+ # time should increase.
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ username = creds.get_username()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ use_kerberos = creds.get_kerberos_state()
+ if use_kerberos == MUST_USE_KERBEROS:
+ print("Testing multiple logon with Kerberos")
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ else:
+ print("Testing multiple logon with NTLM")
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+
+ SamDB(url=self.host_url, credentials=self.insta_creds(creds), lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=("greater", 0),
+ lastLogonTimestamp=("greater", 0),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ firstLogon = lastLogon
+ print("last logon is %d" % lastLogon)
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ time.sleep(1)
+ SamDB(url=self.host_url, credentials=self.insta_creds(creds), lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg=("second logon, firstlogon was %s" %
+ firstLogon))
+
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ time.sleep(1)
+
+ SamDB(url=self.host_url, credentials=self.insta_creds(creds), lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
diff --git a/source4/dsdb/tests/python/password_settings.py b/source4/dsdb/tests/python/password_settings.py
new file mode 100644
index 0000000..1fc0c05
--- /dev/null
+++ b/source4/dsdb/tests/python/password_settings.py
@@ -0,0 +1,876 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Tests for Password Settings Objects.
+#
+# This also tests the default password complexity (i.e. pwdProperties),
+# minPwdLength, pwdHistoryLength settings as a side-effect.
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+#
+# Usage:
+# export SERVER_IP=target_dc
+# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
+# PYTHONPATH="$PYTHONPATH:$samba4srcdir/dsdb/tests/python" $SUBUNITRUN \
+# password_settings -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
+#
+
+import samba.tests
+import ldb
+from ldb import FLAG_MOD_DELETE, FLAG_MOD_ADD, FLAG_MOD_REPLACE
+from samba import dsdb
+import time
+from samba.tests.password_test import PasswordTestCase
+from samba.tests.pso import TestUser
+from samba.tests.pso import PasswordSettings
+from samba.tests import env_get_var_value
+from samba.credentials import Credentials
+from samba import gensec
+import base64
+
+
+class PasswordSettingsTestCase(PasswordTestCase):
+ def setUp(self):
+ super(PasswordSettingsTestCase, self).setUp()
+
+ self.host_url = "ldap://%s" % env_get_var_value("SERVER_IP")
+ self.ldb = samba.tests.connect_samdb(self.host_url)
+
+ # create a temp OU to put this test's users into
+ self.ou = samba.tests.create_test_ou(self.ldb, "password_settings")
+
+ # update DC to allow password changes for the duration of this test
+ self.allow_password_changes()
+
+ # store the current password-settings for the domain
+ self.pwd_defaults = PasswordSettings(None, self.ldb)
+ self.test_objs = []
+
+ def tearDown(self):
+ super(PasswordSettingsTestCase, self).tearDown()
+
+ # remove all objects under the top-level OU
+ self.ldb.delete(self.ou, ["tree_delete:1"])
+
+ # PSOs can't reside within an OU so they get cleaned up separately
+ for obj in self.test_objs:
+ self.ldb.delete(obj)
+
+ def add_obj_cleanup(self, dn_list):
+ """Handles cleanup of objects outside of the test OU in the tearDown"""
+ self.test_objs.extend(dn_list)
+
+ def add_group(self, group_name):
+ """Creates a new group"""
+ dn = "CN=%s,%s" % (group_name, self.ou)
+ self.ldb.add({"dn": dn, "objectclass": "group"})
+ return dn
+
+ def set_attribute(self, dn, attr, value, operation=FLAG_MOD_ADD,
+ samdb=None):
+ """Modifies an attribute for an object"""
+ if samdb is None:
+ samdb = self.ldb
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, dn)
+ m[attr] = ldb.MessageElement(value, operation, attr)
+ samdb.modify(m)
+
+ def add_user(self, username):
+ # add a new user to the DB under our top-level OU
+ userou = "ou=%s" % self.ou.get_component_value(0)
+ return TestUser(username, self.ldb, userou=userou)
+
+ def assert_password_invalid(self, user, password):
+ """
+ Check we can't set a password that violates complexity or length
+ constraints
+ """
+ try:
+ user.set_password(password)
+ # fail the test if no exception was encountered
+ self.fail("Password '%s' should have been rejected" % password)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
+ self.assertTrue('0000052D' in msg, msg)
+
+ def assert_password_valid(self, user, password):
+ """Checks that we can set a password successfully"""
+ try:
+ user.set_password(password)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ # fail the test (rather than throw an error)
+ self.fail("Password '%s' unexpectedly rejected: %s" % (password,
+ msg))
+
+ def assert_PSO_applied(self, user, pso):
+ """
+ Asserts the expected PSO is applied by checking the msDS-ResultantPSO
+ attribute, as well as checking the corresponding password-length,
+ complexity, and history are enforced correctly
+ """
+ resultant_pso = user.get_resultant_PSO()
+ self.assertTrue(resultant_pso == pso.dn,
+ "Expected PSO %s, not %s" % (pso.name,
+ str(resultant_pso)))
+
+ # we're mirroring the pwd_history for the user, so make sure this is
+ # up-to-date, before we start making password changes
+ if user.last_pso:
+ user.pwd_history_change(user.last_pso.history_len, pso.history_len)
+ user.last_pso = pso
+
+ # check if we can set a sufficiently long, but non-complex, password.
+ # (We use the history-size to generate a unique password for each
+ # assertion - otherwise, if the password is already in the history,
+ # then it'll be rejected)
+ unique_char = chr(ord('a') + len(user.all_old_passwords))
+ noncomplex_pwd = "%cabcdefghijklmnopqrst" % unique_char
+
+ if pso.complexity:
+ self.assert_password_invalid(user, noncomplex_pwd)
+ else:
+ self.assert_password_valid(user, noncomplex_pwd)
+
+ # use a unique and sufficiently complex base-string to check pwd-length
+ pass_phrase = "%d#AaBbCcDdEeFfGgHhIi" % len(user.all_old_passwords)
+
+ # check that passwords less than the specified length are rejected
+ for i in range(3, pso.password_len):
+ self.assert_password_invalid(user, pass_phrase[:i])
+
+ # check we can set a password that's exactly the minimum length
+ self.assert_password_valid(user, pass_phrase[:pso.password_len])
+
+ # check the password history is enforced correctly.
+ # first, check the last n items in the password history are invalid
+ invalid_passwords = user.old_invalid_passwords(pso.history_len)
+ for pwd in invalid_passwords:
+ self.assert_password_invalid(user, pwd)
+
+ # next, check any passwords older than the history-len can be re-used
+ valid_passwords = user.old_valid_passwords(pso.history_len)
+ for pwd in valid_passwords:
+ self.assert_set_old_password(user, pwd, pso)
+
+ def password_is_complex(self, password):
+ # non-complex passwords used in the tests are all lower-case letters
+ # If it's got a number in the password, assume it's complex
+ return any(c.isdigit() for c in password)
+
+ def assert_set_old_password(self, user, password, pso):
+ """
+ Checks a user password can be set (if the password conforms to the PSO
+ settings). Used to check an old password that falls outside the history
+ length, but might still be invalid for other reasons.
+ """
+ if self.password_is_complex(password):
+ # check password conforms to length requirements
+ if len(password) < pso.password_len:
+ self.assert_password_invalid(user, password)
+ else:
+ self.assert_password_valid(user, password)
+ else:
+ # password is not complex, check PSO handles it appropriately
+ if pso.complexity:
+ self.assert_password_invalid(user, password)
+ else:
+ self.assert_password_valid(user, password)
+
+ def test_pso_basics(self):
+ """Simple tests that a PSO takes effect when applied to a group/user"""
+
+ # create some PSOs that vary in priority and basic password-len
+ best_pso = PasswordSettings("highest-priority-PSO", self.ldb,
+ precedence=5, password_len=16,
+ history_len=6)
+ medium_pso = PasswordSettings("med-priority-PSO", self.ldb,
+ precedence=15, password_len=10,
+ history_len=4)
+ worst_pso = PasswordSettings("lowest-priority-PSO", self.ldb,
+ precedence=100, complexity=False,
+ password_len=4, history_len=2)
+
+ # handle PSO clean-up (as they're outside the top-level test OU)
+ self.add_obj_cleanup([worst_pso.dn, medium_pso.dn, best_pso.dn])
+
+ # create some groups and apply the PSOs to the groups
+ group1 = self.add_group("Group-1")
+ group2 = self.add_group("Group-2")
+ group3 = self.add_group("Group-3")
+ group4 = self.add_group("Group-4")
+ worst_pso.apply_to(group1)
+ medium_pso.apply_to(group2)
+ best_pso.apply_to(group3)
+ worst_pso.apply_to(group4)
+
+ # create a user and check the default settings apply to it
+ user = self.add_user("testuser")
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # add user to a group. Check that the group's PSO applies to the user
+ self.set_attribute(group1, "member", user.dn)
+ self.assert_PSO_applied(user, worst_pso)
+
+ # add the user to a group with a higher precedence PSO and and check
+ # that now trumps the previous PSO
+ self.set_attribute(group2, "member", user.dn)
+ self.assert_PSO_applied(user, medium_pso)
+
+ # add the user to the remaining groups. The highest precedence PSO
+ # should now take effect
+ self.set_attribute(group3, "member", user.dn)
+ self.set_attribute(group4, "member", user.dn)
+ self.assert_PSO_applied(user, best_pso)
+
+ # delete a group membership and check the PSO changes
+ self.set_attribute(group3, "member", user.dn,
+ operation=FLAG_MOD_DELETE)
+ self.assert_PSO_applied(user, medium_pso)
+
+ # apply the low-precedence PSO directly to the user
+ # (directly applied PSOs should trump higher precedence group PSOs)
+ worst_pso.apply_to(user.dn)
+ self.assert_PSO_applied(user, worst_pso)
+
+ # remove applying the PSO directly to the user and check PSO changes
+ worst_pso.unapply(user.dn)
+ self.assert_PSO_applied(user, medium_pso)
+
+ # remove all appliesTo and check we have the default settings again
+ worst_pso.unapply(group1)
+ medium_pso.unapply(group2)
+ worst_pso.unapply(group4)
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ def test_pso_nested_groups(self):
+ """PSOs operate correctly when applied to nested groups"""
+
+ # create some PSOs that vary in priority and basic password-len
+ group1_pso = PasswordSettings("group1-PSO", self.ldb, precedence=50,
+ password_len=12, history_len=3)
+ group2_pso = PasswordSettings("group2-PSO", self.ldb, precedence=25,
+ password_len=10, history_len=5,
+ complexity=False)
+ group3_pso = PasswordSettings("group3-PSO", self.ldb, precedence=10,
+ password_len=6, history_len=2)
+
+ # create some groups and apply the PSOs to the groups
+ group1 = self.add_group("Group-1")
+ group2 = self.add_group("Group-2")
+ group3 = self.add_group("Group-3")
+ group4 = self.add_group("Group-4")
+ group1_pso.apply_to(group1)
+ group2_pso.apply_to(group2)
+ group3_pso.apply_to(group3)
+
+ # create a PSO and apply it to a group that the user is not a member
+ # of - it should not have any effect on the user
+ unused_pso = PasswordSettings("unused-PSO", self.ldb, precedence=1,
+ password_len=20)
+ unused_pso.apply_to(group4)
+
+ # handle PSO clean-up (as they're outside the top-level test OU)
+ self.add_obj_cleanup([group1_pso.dn, group2_pso.dn, group3_pso.dn,
+ unused_pso.dn])
+
+ # create a user and check the default settings apply to it
+ user = self.add_user("testuser")
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # add user to a group. Check that the group's PSO applies to the user
+ self.set_attribute(group1, "member", user.dn)
+ self.set_attribute(group2, "member", group1)
+ self.assert_PSO_applied(user, group2_pso)
+
+ # add another level to the group hierarchy & check this PSO takes effect
+ self.set_attribute(group3, "member", group2)
+ self.assert_PSO_applied(user, group3_pso)
+
+ # invert the PSO precedence and check the new lowest value takes effect
+ group1_pso.set_precedence(3)
+ group2_pso.set_precedence(13)
+ group3_pso.set_precedence(33)
+ self.assert_PSO_applied(user, group1_pso)
+
+ # delete a PSO and check it no longer applies
+ self.ldb.delete(group1_pso.dn)
+ self.test_objs.remove(group1_pso.dn)
+ self.assert_PSO_applied(user, group2_pso)
+
+ def get_guid(self, dn):
+ res = self.ldb.search(base=dn, attrs=["objectGUID"],
+ scope=ldb.SCOPE_BASE)
+ return res[0]['objectGUID'][0]
+
+ def guid_string(self, guid):
+ return self.ldb.schema_format_value("objectGUID", guid)
+
+ def PSO_with_lowest_GUID(self, pso_list):
+ """Returns the PSO object in the list with the lowest GUID"""
+ # go through each PSO and fetch its GUID
+ guid_list = []
+ mapping = {}
+ for pso in pso_list:
+ guid = self.get_guid(pso.dn)
+ guid_list.append(guid)
+ # remember which GUID maps to what PSO
+ mapping[guid] = pso
+
+ # sort the GUID list to work out the lowest/best GUID
+ guid_list.sort()
+ best_guid = guid_list[0]
+
+ # sanity-check the mapping between GUID and DN is correct
+ best_pso_dn = mapping[best_guid].dn
+ self.assertEqual(self.guid_string(self.get_guid(best_pso_dn)),
+ self.guid_string(best_guid))
+
+ # return the PSO that this GUID corresponds to
+ return mapping[best_guid]
+
+ def test_pso_equal_precedence(self):
+ """Tests expected PSO wins when several have the same precedence"""
+
+ # create some PSOs that vary in priority and basic password-len
+ pso1 = PasswordSettings("PSO-1", self.ldb, precedence=5, history_len=1,
+ password_len=11)
+ pso2 = PasswordSettings("PSO-2", self.ldb, precedence=5, history_len=2,
+ password_len=8)
+ pso3 = PasswordSettings("PSO-3", self.ldb, precedence=5, history_len=3,
+ password_len=5, complexity=False)
+ pso4 = PasswordSettings("PSO-4", self.ldb, precedence=5, history_len=4,
+ password_len=13, complexity=False)
+
+ # handle PSO clean-up (as they're outside the top-level test OU)
+ self.add_obj_cleanup([pso1.dn, pso2.dn, pso3.dn, pso4.dn])
+
+ # create some groups and apply the PSOs to the groups
+ group1 = self.add_group("Group-1")
+ group2 = self.add_group("Group-2")
+ group3 = self.add_group("Group-3")
+ group4 = self.add_group("Group-4")
+ pso1.apply_to(group1)
+ pso2.apply_to(group2)
+ pso3.apply_to(group3)
+ pso4.apply_to(group4)
+
+ # create a user and check the default settings apply to it
+ user = self.add_user("testuser")
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # add the user to all the groups
+ self.set_attribute(group1, "member", user.dn)
+ self.set_attribute(group2, "member", user.dn)
+ self.set_attribute(group3, "member", user.dn)
+ self.set_attribute(group4, "member", user.dn)
+
+ # precedence is equal, so the PSO with lowest GUID gets applied
+ pso_list = [pso1, pso2, pso3, pso4]
+ best_pso = self.PSO_with_lowest_GUID(pso_list)
+ self.assert_PSO_applied(user, best_pso)
+
+ # excluding the winning PSO, apply the other PSOs directly to the user
+ pso_list.remove(best_pso)
+ for pso in pso_list:
+ pso.apply_to(user.dn)
+
+ # we should now have a different PSO applied (the 2nd lowest GUID)
+ next_best_pso = self.PSO_with_lowest_GUID(pso_list)
+ self.assertTrue(next_best_pso is not best_pso)
+ self.assert_PSO_applied(user, next_best_pso)
+
+ # bump the precedence of another PSO and it should now win
+ pso_list.remove(next_best_pso)
+ best_pso = pso_list[0]
+ best_pso.set_precedence(4)
+ self.assert_PSO_applied(user, best_pso)
+
+ def test_pso_invalid_location(self):
+ """Tests that PSOs in an invalid location have no effect"""
+
+ # PSOs should only be able to be created within a Password Settings
+ # Container object. Trying to create one under an OU should fail
+ try:
+ rogue_pso = PasswordSettings("rogue-PSO", self.ldb, precedence=1,
+ complexity=False, password_len=20,
+ container=self.ou)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_NAMING_VIOLATION, msg)
+ # Windows returns 2099 (Illegal superior), Samba returns 2037
+ # (Naming violation - "not a valid child class")
+ self.assertTrue('00002099' in msg or '00002037' in msg, msg)
+
+ # we can't create Password Settings Containers under an OU either
+ try:
+ rogue_psc = "CN=Rogue-PSO-container,%s" % self.ou
+ self.ldb.add({"dn": rogue_psc,
+ "objectclass": "msDS-PasswordSettingsContainer"})
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_NAMING_VIOLATION, msg)
+ self.assertTrue('00002099' in msg or '00002037' in msg, msg)
+
+ base_dn = self.ldb.get_default_basedn()
+ rogue_psc = "CN=Rogue-PSO-container,CN=Computers,%s" % base_dn
+ self.ldb.add({"dn": rogue_psc,
+ "objectclass": "msDS-PasswordSettingsContainer"})
+
+ rogue_pso = PasswordSettings("rogue-PSO", self.ldb, precedence=1,
+ container=rogue_psc, password_len=20)
+ self.add_obj_cleanup([rogue_pso.dn, rogue_psc])
+
+ # apply the PSO to a group and check it has no effect on the user
+ user = self.add_user("testuser")
+ group = self.add_group("Group-1")
+ rogue_pso.apply_to(group)
+ self.set_attribute(group, "member", user.dn)
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # apply the PSO directly to the user and check it has no effect
+ rogue_pso.apply_to(user.dn)
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # the PSOs created in these test-cases all use a default min-age of zero.
+ # This is the only test case that checks the PSO's min-age is enforced
+ def test_pso_min_age(self):
+ """Tests that a PSO's min-age is enforced"""
+ pso = PasswordSettings("min-age-PSO", self.ldb, password_len=10,
+ password_age_min=2, complexity=False)
+ self.add_obj_cleanup([pso.dn])
+
+ # create a user and apply the PSO
+ user = self.add_user("testuser")
+ pso.apply_to(user.dn)
+ self.assertTrue(user.get_resultant_PSO() == pso.dn)
+
+ # changing the password immediately should fail, even if the password
+ # is valid
+ valid_password = "min-age-passwd"
+ self.assert_password_invalid(user, valid_password)
+ # then trying the same password later should succeed
+ time.sleep(pso.password_age_min + 0.5)
+ self.assert_password_valid(user, valid_password)
+
+ def test_pso_max_age(self):
+ """Tests that a PSO's max-age is used"""
+
+ # create PSOs that use the domain's max-age +/- 1 day
+ domain_max_age = self.pwd_defaults.password_age_max
+ day_in_secs = 60 * 60 * 24
+ higher_max_age = domain_max_age + day_in_secs
+ lower_max_age = domain_max_age - day_in_secs
+ longer_pso = PasswordSettings("longer-age-PSO", self.ldb, precedence=5,
+ password_age_max=higher_max_age)
+ shorter_pso = PasswordSettings("shorter-age-PSO", self.ldb,
+ precedence=1,
+ password_age_max=lower_max_age)
+ self.add_obj_cleanup([longer_pso.dn, shorter_pso.dn])
+
+ user = self.add_user("testuser")
+
+ # we can't wait around long enough for the max-age to expire, so
+ # instead just check the msDS-UserPasswordExpiryTimeComputed for
+ # the user
+ attrs = ['msDS-UserPasswordExpiryTimeComputed']
+ res = self.ldb.search(user.dn, attrs=attrs)
+ domain_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
+
+ # apply the longer PSO and check the expiry-time becomes longer
+ longer_pso.apply_to(user.dn)
+ self.assertTrue(user.get_resultant_PSO() == longer_pso.dn)
+ res = self.ldb.search(user.dn, attrs=attrs)
+ new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
+
+ # use timestamp diff of 1 day - 1 minute. The new expiry should still
+ # be greater than this, without getting into nano-second granularity
+ approx_timestamp_diff = (day_in_secs - 60) * (1e7)
+ self.assertTrue(new_expiry > domain_expiry + approx_timestamp_diff)
+
+ # apply the shorter PSO and check the expiry-time is shorter
+ shorter_pso.apply_to(user.dn)
+ self.assertTrue(user.get_resultant_PSO() == shorter_pso.dn)
+ res = self.ldb.search(user.dn, attrs=attrs)
+ new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
+ self.assertTrue(new_expiry < domain_expiry - approx_timestamp_diff)
+
+ def test_pso_special_groups(self):
+ """Checks applying a PSO to built-in AD groups takes effect"""
+
+ # create some PSOs that will apply to special groups
+ default_pso = PasswordSettings("default-PSO", self.ldb, precedence=20,
+ password_len=8, complexity=False)
+ guest_pso = PasswordSettings("guest-PSO", self.ldb, history_len=4,
+ precedence=5, password_len=5)
+ builtin_pso = PasswordSettings("builtin-PSO", self.ldb, history_len=9,
+ precedence=1, password_len=9)
+ admin_pso = PasswordSettings("admin-PSO", self.ldb, history_len=0,
+ precedence=2, password_len=10)
+ self.add_obj_cleanup([default_pso.dn, guest_pso.dn, admin_pso.dn,
+ builtin_pso.dn])
+ base_dn = self.ldb.domain_dn()
+ domain_users = "CN=Domain Users,CN=Users,%s" % base_dn
+ domain_guests = "CN=Domain Guests,CN=Users,%s" % base_dn
+ admin_users = "CN=Domain Admins,CN=Users,%s" % base_dn
+
+ # if we apply a PSO to Domain Users (which all users are a member of)
+ # then that PSO should take effect on a new user
+ default_pso.apply_to(domain_users)
+ user = self.add_user("testuser")
+ self.assert_PSO_applied(user, default_pso)
+
+ # Apply a PSO to a builtin group. 'Domain Users' should be a member of
+ # Builtin/Users, but builtin groups should be excluded from the PSO
+ # calculation, so this should have no effect
+ builtin_pso.apply_to("CN=Users,CN=Builtin,%s" % base_dn)
+ builtin_pso.apply_to("CN=Administrators,CN=Builtin,%s" % base_dn)
+ self.assert_PSO_applied(user, default_pso)
+
+ # change the user's primary group to another group (the primaryGroupID
+ # is a little odd in that there's no memberOf backlink for it)
+ self.set_attribute(domain_guests, "member", user.dn)
+ user.set_primary_group(domain_guests)
+ # No PSO is applied to the Domain Guests yet, so the default PSO should
+ # still apply
+ self.assert_PSO_applied(user, default_pso)
+
+ # now apply a PSO to the guests group, which should trump the default
+ # PSO (because the guest PSO has a better precedence)
+ guest_pso.apply_to(domain_guests)
+ self.assert_PSO_applied(user, guest_pso)
+
+ # create a new group that's a member of Admin Users
+ nested_group = self.add_group("nested-group")
+ self.set_attribute(admin_users, "member", nested_group)
+ # set the user's primary-group to be the new group
+ self.set_attribute(nested_group, "member", user.dn)
+ user.set_primary_group(nested_group)
+ # we've only changed group membership so far, not the PSO
+ self.assert_PSO_applied(user, guest_pso)
+
+ # now apply the best-precedence PSO to Admin Users and check it applies
+ # to the user (via the nested-group's membership)
+ admin_pso.apply_to(admin_users)
+ self.assert_PSO_applied(user, admin_pso)
+
+ # restore the default primaryGroupID so we can safely delete the group
+ user.set_primary_group(domain_users)
+
+ def test_pso_none_applied(self):
+ """Tests cases where no Resultant PSO should be returned"""
+
+ # create a PSO that we will check *doesn't* get returned
+ dummy_pso = PasswordSettings("dummy-PSO", self.ldb, password_len=20)
+ self.add_obj_cleanup([dummy_pso.dn])
+
+ # you can apply a PSO to other objects (like OUs), but the resultantPSO
+ # attribute should only be returned for users
+ dummy_pso.apply_to(str(self.ou))
+ res = self.ldb.search(self.ou, attrs=['msDS-ResultantPSO'])
+ self.assertFalse('msDS-ResultantPSO' in res[0])
+
+ # create a dummy user and apply the PSO
+ user = self.add_user("testuser")
+ dummy_pso.apply_to(user.dn)
+ self.assertTrue(user.get_resultant_PSO() == dummy_pso.dn)
+
+ try:
+ # now clear the ADS_UF_NORMAL_ACCOUNT flag for the user, which should
+ # mean a resultant PSO is no longer returned (we're essentially turning
+ # the user into a DC here, which is a little overkill but tests
+ # behaviour as per the Windows specification)
+ self.set_attribute(user.dn, "userAccountControl",
+ str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ operation=FLAG_MOD_REPLACE)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.fail("Failed to change user into a workstation: {msg}")
+ self.assertIsNone(user.get_resultant_PSO())
+
+ try:
+ # reset it back to a normal user account
+ self.set_attribute(user.dn, "userAccountControl",
+ str(dsdb.UF_NORMAL_ACCOUNT),
+ operation=FLAG_MOD_REPLACE)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.fail("Failed to change user back into a user: {msg}")
+ self.assertTrue(user.get_resultant_PSO() == dummy_pso.dn)
+
+ # no PSO should be returned if RID is equal to DOMAIN_USER_RID_KRBTGT
+ # (note this currently fails against Windows due to a Windows bug)
+ krbtgt_user = "CN=krbtgt,CN=Users,%s" % self.ldb.domain_dn()
+ dummy_pso.apply_to(krbtgt_user)
+ res = self.ldb.search(krbtgt_user, attrs=['msDS-ResultantPSO'])
+ self.assertFalse('msDS-ResultantPSO' in res[0])
+
+ def get_ldb_connection(self, username, password, ldaphost):
+ """Returns an LDB connection using the specified user's credentials"""
+ creds = self.get_credentials()
+ creds_tmp = Credentials()
+ creds_tmp.set_username(username)
+ creds_tmp.set_password(password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ features = creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL
+ creds_tmp.set_gensec_features(features)
+ return samba.tests.connect_samdb(ldaphost, credentials=creds_tmp)
+
+ def test_pso_permissions(self):
+ """Checks that regular users can't modify/view PSO objects"""
+
+ user = self.add_user("testuser")
+
+ # get an ldb connection with the new user's privileges
+ user_ldb = self.get_ldb_connection("testuser", user.get_password(),
+ self.host_url)
+
+ # regular users should not be able to create a PSO (at least, not in
+ # the default Password Settings container)
+ try:
+ priv_pso = PasswordSettings("priv-PSO", user_ldb, password_len=20)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
+
+ # create a PSO as the admin user
+ priv_pso = PasswordSettings("priv-PSO", self.ldb, password_len=20)
+ self.add_obj_cleanup([priv_pso.dn])
+
+ # regular users should not be able to apply a PSO to a user
+ try:
+ self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
+ samdb=user_ldb)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
+ self.assertTrue('00002098' in msg, msg)
+
+ self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
+ samdb=self.ldb)
+
+ # regular users should not be able to change a PSO's precedence
+ try:
+ priv_pso.set_precedence(100, samdb=user_ldb)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
+ self.assertTrue('00002098' in msg, msg)
+
+ priv_pso.set_precedence(100, samdb=self.ldb)
+
+ # regular users should not be able to view a PSO's settings
+ pso_attrs = ["msDS-PSOAppliesTo", "msDS-PasswordSettingsPrecedence",
+ "msDS-PasswordHistoryLength", "msDS-LockoutThreshold",
+ "msDS-PasswordComplexityEnabled"]
+
+ # users can see the PSO object's DN, but not its attributes
+ res = user_ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
+ attrs=pso_attrs)
+ self.assertTrue(str(priv_pso.dn) == str(res[0].dn))
+ for attr in pso_attrs:
+ self.assertFalse(attr in res[0])
+
+ # whereas admin users can see everything
+ res = self.ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
+ attrs=pso_attrs)
+ for attr in pso_attrs:
+ self.assertTrue(attr in res[0])
+
+ # check replace/delete operations can't be performed by regular users
+ operations = [FLAG_MOD_REPLACE, FLAG_MOD_DELETE]
+
+ for oper in operations:
+ try:
+ self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
+ samdb=user_ldb, operation=oper)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
+ self.assertTrue('00002098' in msg, msg)
+
+ # ...but can be performed by the admin user
+ self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
+ samdb=self.ldb, operation=oper)
+
+ def format_password_for_ldif(self, password):
+ """Encodes/decodes the password so that it's accepted in an LDIF"""
+ pwd = '"{0}"'.format(password)
+ return base64.b64encode(pwd.encode('utf-16-le')).decode('utf8')
+
+ # The 'user add' case is a bit more complicated as you can't really query
+ # the msDS-ResultantPSO attribute on a user that doesn't exist yet (it
+ # won't have any group membership or PSOs applied directly against it yet).
+ # In theory it's possible to still get an applicable PSO via the user's
+ # primaryGroupID (i.e. 'Domain Users' by default). However, testing against
+ # Windows shows that the PSO doesn't take effect during the user add
+ # operation. (However, the Windows GUI tools presumably adds the user in 2
+ # steps, because it does enforce the PSO for users added via the GUI).
+ def test_pso_add_user(self):
+ """Tests against a 'Domain Users' PSO taking effect on a new user"""
+
+ # create a PSO that will apply to users by default
+ default_pso = PasswordSettings("default-PSO", self.ldb, precedence=20,
+ password_len=12, complexity=False)
+ self.add_obj_cleanup([default_pso.dn])
+
+ # apply the PSO to Domain Users (which all users are a member of). In
+ # theory, this PSO *could* take effect on a new user (but it doesn't)
+ domain_users = "CN=Domain Users,CN=Users,%s" % self.ldb.domain_dn()
+ default_pso.apply_to(domain_users)
+
+ # first try to add a user with a password that doesn't meet the domain
+ # defaults, to prove that the DC will reject bad passwords during a
+ # user add
+ userdn = "CN=testuser,%s" % self.ou
+ password = self.format_password_for_ldif('abcdef')
+
+ # Note we use an LDIF operation to ensure that the password gets set
+ # as part of the 'add' operation (whereas self.add_user() adds the user
+ # first, then sets the password later in a 2nd step)
+ try:
+ ldif = """
+dn: %s
+objectClass: user
+sAMAccountName: testuser
+unicodePwd:: %s
+""" % (userdn, password)
+ self.ldb.add_ldif(ldif)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ # error codes differ between Samba and Windows
+ self.assertTrue(num == ldb.ERR_UNWILLING_TO_PERFORM or
+ num == ldb.ERR_CONSTRAINT_VIOLATION, msg)
+ self.assertTrue('0000052D' in msg, msg)
+
+ # now use a password that meets the domain defaults, but doesn't meet
+ # the PSO requirements. Note that Windows allows this, i.e. it doesn't
+ # honour the PSO during the add operation
+ password = self.format_password_for_ldif('abcde12#')
+ ldif = """
+dn: %s
+objectClass: user
+sAMAccountName: testuser
+unicodePwd:: %s
+""" % (userdn, password)
+ self.ldb.add_ldif(ldif)
+
+ # Now do essentially the same thing, but set the password in a 2nd step
+ # which proves that the same password doesn't meet the PSO requirements
+ userdn = "CN=testuser2,%s" % self.ou
+ ldif = """
+dn: %s
+objectClass: user
+sAMAccountName: testuser2
+""" % userdn
+ self.ldb.add_ldif(ldif)
+
+ # now that the user exists, assert that the PSO is honoured
+ try:
+ ldif = """
+dn: %s
+changetype: modify
+delete: unicodePwd
+add: unicodePwd
+unicodePwd:: %s
+""" % (userdn, password)
+ self.ldb.modify_ldif(ldif)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
+ self.assertTrue('0000052D' in msg, msg)
+
+ # check setting a password that meets the PSO settings works
+ password = self.format_password_for_ldif('abcdefghijkl')
+ ldif = """
+dn: %s
+changetype: modify
+delete: unicodePwd
+add: unicodePwd
+unicodePwd:: %s
+""" % (userdn, password)
+ self.ldb.modify_ldif(ldif)
+
+ def set_domain_pwdHistoryLength(self, value):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, self.ldb.domain_dn())
+ m["pwdHistoryLength"] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ "pwdHistoryLength")
+ self.ldb.modify(m)
+
+ def test_domain_pwd_history(self):
+ """Non-PSO test for domain's pwdHistoryLength setting"""
+
+ # restore the current pwdHistoryLength setting after the test completes
+ curr_hist_len = str(self.pwd_defaults.history_len)
+ self.addCleanup(self.set_domain_pwdHistoryLength, curr_hist_len)
+
+ self.set_domain_pwdHistoryLength("4")
+ user = self.add_user("testuser")
+
+ initial_pwd = user.get_password()
+ passwords = ["First12#", "Second12#", "Third12#", "Fourth12#"]
+
+ # we should be able to set the password to new values OK
+ for pwd in passwords:
+ self.assert_password_valid(user, pwd)
+
+ # the 2nd time round it should fail because they're in the history now
+ for pwd in passwords:
+ self.assert_password_invalid(user, pwd)
+
+ # but the initial password is now outside the history, so should be OK
+ self.assert_password_valid(user, initial_pwd)
+
+ # if we set the history to zero, all the old passwords should now be OK
+ self.set_domain_pwdHistoryLength("0")
+ for pwd in passwords:
+ self.assert_password_valid(user, pwd)
+
+ def test_domain_pwd_history_zero(self):
+ """Non-PSO test for pwdHistoryLength going from zero to non-zero"""
+
+ # restore the current pwdHistoryLength setting after the test completes
+ curr_hist_len = str(self.pwd_defaults.history_len)
+ self.addCleanup(self.set_domain_pwdHistoryLength, curr_hist_len)
+
+ self.set_domain_pwdHistoryLength("0")
+ user = self.add_user("testuser")
+
+ self.assert_password_valid(user, "NewPwd12#")
+ # we can set the exact same password again because there's no history
+ self.assert_password_valid(user, "NewPwd12#")
+
+ # When going from zero to non-zero password-history, Windows treats
+ # the current user's password as invalid (even though the password has
+ # not been altered since the setting changed).
+ self.set_domain_pwdHistoryLength("1")
+ self.assert_password_invalid(user, "NewPwd12#")
diff --git a/source4/dsdb/tests/python/passwords.py b/source4/dsdb/tests/python/passwords.py
new file mode 100755
index 0000000..4e2182a
--- /dev/null
+++ b/source4/dsdb/tests/python/passwords.py
@@ -0,0 +1,1454 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This tests the password changes over LDAP for AD implementations
+#
+# Copyright Matthias Dieter Wallnoefer 2010
+#
+# Notice: This tests will also work against Windows Server if the connection is
+# secured enough (SASL with a minimum of 128 Bit encryption) - consider
+# MS-ADTS 3.1.1.3.1.5
+
+import optparse
+import sys
+import base64
+import time
+import os
+
+sys.path.insert(0, "bin/python")
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+from samba.tests.password_test import PasswordTestCase
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba.credentials import Credentials
+from samba.dcerpc import drsblobs, misc, security
+from samba.drs_utils import drsuapi_connect
+from samba.ndr import ndr_unpack
+from ldb import SCOPE_BASE, LdbError
+from ldb import ERR_ATTRIBUTE_OR_VALUE_EXISTS
+from ldb import ERR_UNWILLING_TO_PERFORM, ERR_INSUFFICIENT_ACCESS_RIGHTS
+from ldb import ERR_NO_SUCH_ATTRIBUTE
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_INVALID_CREDENTIALS
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from samba import gensec, net, werror
+from samba.samdb import SamDB
+from samba.tests import delete_force
+
+parser = optparse.OptionParser("passwords.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)
+
+# Force an encrypted connection
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+
+
+class PasswordTests(PasswordTestCase):
+
+ def setUp(self):
+ super(PasswordTests, self).setUp()
+ self.ldb = SamDB(url=host, session_info=system_session(lp), credentials=creds, lp=lp)
+
+ # permit password changes during this test
+ self.allow_password_changes()
+
+ self.base_dn = self.ldb.domain_dn()
+
+ # (Re)adds the test user "testuser" with no password atm
+ delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ self.ldb.add({
+ "dn": "cn=testuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "sAMAccountName": "testuser"})
+
+ # Tests a password change when we don't have any password yet with a
+ # wrong old password
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: noPassword
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ # Windows (2008 at least) seems to have some small bug here: it
+ # returns "0000056A" on longer (always wrong) previous passwords.
+ self.assertTrue('00000056' in msg)
+
+ # Sets the initial user password with a "special" password change
+ # I think that this internally is a password set operation and it can
+ # only be performed by someone which has password set privileges on the
+ # account (at least in s4 we do handle it like that).
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+add: userPassword
+userPassword: thatsAcomplPASS1
+""")
+
+ # But in the other way around this special syntax doesn't work
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+""")
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # Enables the user account
+ self.ldb.enable_account("(sAMAccountName=testuser)")
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ creds2 = Credentials()
+ creds2.set_username("testuser")
+ creds2.set_password("thatsAcomplPASS1")
+ creds2.set_domain(creds.get_domain())
+ creds2.set_realm(creds.get_realm())
+ creds2.set_workstation(creds.get_workstation())
+ creds2.set_gensec_features(creds2.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ self.ldb2 = SamDB(url=host, credentials=creds2, lp=lp)
+ self.creds = creds2
+
+ def test_unicodePwd_hash_set(self):
+ """Performs a password hash set operation on 'unicodePwd' which should be prevented"""
+ # Notice: Direct hash password sets should never work
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
+ "unicodePwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ def test_unicodePwd_hash_change(self):
+ """Performs a password hash change operation on 'unicodePwd' which should be prevented"""
+ # Notice: Direct hash password changes should never work
+
+ # Hash password changes should never work
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd: XXXXXXXXXXXXXXXX
+add: unicodePwd
+unicodePwd: YYYYYYYYYYYYYYYY
+""")
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ def test_unicodePwd_clear_set(self):
+ """Performs a password cleartext set operation on 'unicodePwd'"""
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement("\"thatsAcomplPASS2\"".encode('utf-16-le'),
+ FLAG_MOD_REPLACE, "unicodePwd")
+ self.ldb.modify(m)
+
+ def test_unicodePwd_clear_change(self):
+ """Performs a password cleartext change operation on 'unicodePwd'"""
+
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ # Wrong old password
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS4\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg)
+
+ # A change to the same password again will not work (password history)
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('0000052D' in msg)
+
+ def test_old_password_simple_bind(self):
+ '''Shows that we can log in with the immediate previous password, but not any earlier passwords.'''
+
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Change the account password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show we can still log in using the previous password.
+ self.creds.set_bind_dn(user_dn_str)
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError:
+ self.fail('failed to login with previous password!')
+
+ # Change the account password a second time.
+ m = Message(user_dn)
+ m['0'] = MessageElement('Password#2',
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#3',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show we can no longer log in using the original password.
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError as err:
+ HRES_SEC_E_INVALID_TOKEN = '80090308'
+
+ num, estr = err.args
+ self.assertEqual(ERR_INVALID_CREDENTIALS, num)
+ self.assertIn(HRES_SEC_E_INVALID_TOKEN, estr)
+ else:
+ self.fail('should have failed to login with previous password!')
+
+ def test_old_password_attempt_reuse(self):
+ '''Shows that we cannot reuse the original password after changing the password twice.'''
+ res = self.ldb.search(self.ldb.domain_dn(), scope=SCOPE_BASE,
+ attrs=['pwdHistoryLength'])
+
+ history_len = int(res[0].get('pwdHistoryLength', idx=0))
+ self.assertGreaterEqual(history_len, 3)
+
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ first_pwd = self.creds.get_password()
+ previous_pwd = first_pwd
+
+ for new_pwd in ['Password#0', 'Password#1']:
+ # Change the account password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(previous_pwd,
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(new_pwd,
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show that the original password is in the history by trying to
+ # set it as our new password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(new_pwd,
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(first_pwd,
+ FLAG_MOD_ADD, 'userPassword')
+ try:
+ self.ldb.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', estr)
+ else:
+ self.fail('should not have been able to reuse password!')
+
+ previous_pwd = new_pwd
+
+ def test_old_password_rename_simple_bind(self):
+ '''Shows that we can log in with the previous password after renaming the account.'''
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Change the account password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show we can still log in using the previous password.
+ self.creds.set_bind_dn(user_dn_str)
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError:
+ self.fail('failed to login with previous password!')
+
+ # Rename the account, causing the salt to change.
+ m = Message(user_dn)
+ m['1'] = MessageElement('testuser_2',
+ FLAG_MOD_REPLACE, 'sAMAccountName')
+ self.ldb.modify(m)
+
+ # Show that a simple bind can still be performed using the previous
+ # password.
+ self.creds.set_username('testuser_2')
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError:
+ self.fail('failed to login with previous password!')
+
+ def test_old_password_rename_simple_bind_2(self):
+ '''Shows that we can rename the account, change the password and log in with the previous password.'''
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Rename the account, causing the salt to change.
+ m = Message(user_dn)
+ m['1'] = MessageElement('testuser_2',
+ FLAG_MOD_REPLACE, 'sAMAccountName')
+ self.ldb.modify(m)
+
+ # Change the account password, causing the new salt to be stored.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show that a simple bind can still be performed using the previous
+ # password.
+ self.creds.set_bind_dn(user_dn_str)
+ self.creds.set_username('testuser_2')
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError:
+ self.fail('failed to login with previous password!')
+
+ def test_old_password_rename_attempt_reuse(self):
+ '''Shows that we cannot reuse the original password after renaming the account.'''
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Change the account password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show that the previous password is in the history by trying to set it
+ # as our new password.
+ m = Message(user_dn)
+ m['0'] = MessageElement('Password#2',
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_ADD, 'userPassword')
+ try:
+ self.ldb.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', estr)
+ else:
+ self.fail('should not have been able to reuse password!')
+
+ # Rename the account, causing the salt to change.
+ m = Message(user_dn)
+ m['1'] = MessageElement('testuser_2',
+ FLAG_MOD_REPLACE, 'sAMAccountName')
+ self.ldb.modify(m)
+
+ # Show that the previous password is still in the history by trying to
+ # set it as our new password.
+ m = Message(user_dn)
+ m['0'] = MessageElement('Password#2',
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_ADD, 'userPassword')
+ try:
+ self.ldb.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', estr)
+ else:
+ self.fail('should not have been able to reuse password!')
+
+ def test_old_password_rename_attempt_reuse_2(self):
+ '''Shows that we cannot reuse the original password after renaming the account and changing the password.'''
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Rename the account, causing the salt to change.
+ m = Message(user_dn)
+ m['1'] = MessageElement('testuser_2',
+ FLAG_MOD_REPLACE, 'sAMAccountName')
+ self.ldb.modify(m)
+
+ # Change the account password, causing the new salt to be stored.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show that the previous password is in the history by trying to set it
+ # as our new password.
+ m = Message(user_dn)
+ m['0'] = MessageElement('Password#2',
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_ADD, 'userPassword')
+ try:
+ self.ldb.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', estr)
+ else:
+ self.fail('should not have been able to reuse password!')
+
+ def test_protected_unicodePwd_clear_set(self):
+ """Performs a password cleartext set operation on 'unicodePwd' with the user in
+the Protected Users group"""
+
+ user_dn = f'cn=testuser,cn=users,{self.base_dn}'
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ # Add the user to the list of members.
+ members = list(group_res[0].get('member', ()))
+ members.append(user_dn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ self.ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(self.ldb, user_dn)
+ m['unicodePwd'] = MessageElement(
+ '"thatsAcomplPASS2"'.encode('utf-16-le'),
+ FLAG_MOD_REPLACE, 'unicodePwd')
+ self.ldb.modify(m)
+
+ def test_protected_unicodePwd_clear_change(self):
+ """Performs a password cleartext change operation on 'unicodePwd' with the user
+in the Protected Users group"""
+
+ user_dn = f'cn=testuser,cn=users,{self.base_dn}'
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ # Add the user to the list of members.
+ members = list(group_res[0].get('member', ()))
+ members.append(user_dn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ self.ldb.modify(m)
+
+ self.ldb2.modify_ldif(f"""
+dn: cn=testuser,cn=users,{self.base_dn}
+changetype: modify
+delete: unicodePwd
+unicodePwd:: {base64.b64encode('"thatsAcomplPASS1"'.encode('utf-16-le'))
+ .decode('utf8')}
+add: unicodePwd
+unicodePwd:: {base64.b64encode('"thatsAcomplPASS2"'.encode('utf-16-le'))
+ .decode('utf8')}
+""")
+
+ def test_dBCSPwd_hash_set(self):
+ """Performs a password hash set operation on 'dBCSPwd' which should be prevented"""
+ # Notice: Direct hash password sets should never work
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["dBCSPwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
+ "dBCSPwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ def test_dBCSPwd_hash_change(self):
+ """Performs a password hash change operation on 'dBCSPwd' which should be prevented"""
+ # Notice: Direct hash password changes should never work
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: dBCSPwd
+dBCSPwd: XXXXXXXXXXXXXXXX
+add: dBCSPwd
+dBCSPwd: YYYYYYYYYYYYYYYY
+""")
+ self.fail()
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ def test_userPassword_clear_set(self):
+ """Performs a password cleartext set operation on 'userPassword'"""
+ # Notice: This works only against Windows if "dSHeuristics" has been set
+ # properly
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ def test_userPassword_clear_change(self):
+ """Performs a password cleartext change operation on 'userPassword'"""
+ # Notice: This works only against Windows if "dSHeuristics" has been set
+ # properly
+
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+
+ # Wrong old password
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS3
+add: userPassword
+userPassword: thatsAcomplPASS4
+""")
+ self.fail()
+ except LdbError as e8:
+ (num, msg) = e8.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg)
+
+ # A change to the same password again will not work (password history)
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e9:
+ (num, msg) = e9.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('0000052D' in msg)
+
+ def test_clearTextPassword_clear_set(self):
+ """Performs a password cleartext set operation on 'clearTextPassword'"""
+ # Notice: This never works against Windows - only supported by us
+
+ try:
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["clearTextPassword"] = MessageElement("thatsAcomplPASS2".encode('utf-16-le'),
+ FLAG_MOD_REPLACE, "clearTextPassword")
+ self.ldb.modify(m)
+ # this passes against s4
+ except LdbError as e10:
+ (num, msg) = e10.args
+ # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
+ if num != ERR_NO_SUCH_ATTRIBUTE:
+ raise LdbError(num, msg)
+
+ def test_clearTextPassword_clear_change(self):
+ """Performs a password cleartext change operation on 'clearTextPassword'"""
+ # Notice: This never works against Windows - only supported by us
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS1".encode('utf-16-le')).decode('utf8') + """
+add: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')).decode('utf8') + """
+""")
+ # this passes against s4
+ except LdbError as e11:
+ (num, msg) = e11.args
+ # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
+ if num != ERR_NO_SUCH_ATTRIBUTE:
+ raise LdbError(num, msg)
+
+ # Wrong old password
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS3".encode('utf-16-le')).decode('utf8') + """
+add: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS4".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e12:
+ (num, msg) = e12.args
+ # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
+ if num != ERR_NO_SUCH_ATTRIBUTE:
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg)
+
+ # A change to the same password again will not work (password history)
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')).decode('utf8') + """
+add: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e13:
+ (num, msg) = e13.args
+ # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
+ if num != ERR_NO_SUCH_ATTRIBUTE:
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('0000052D' in msg)
+
+ def test_failures(self):
+ """Performs some failure testing"""
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ self.fail()
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ self.fail()
+ except LdbError as e15:
+ (num, _) = e15.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+""")
+ self.fail()
+ except LdbError as e16:
+ (num, _) = e16.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+""")
+ self.fail()
+ except LdbError as e17:
+ (num, _) = e17.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+add: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ self.fail()
+ except LdbError as e18:
+ (num, _) = e18.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+add: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ self.fail()
+ except LdbError as e19:
+ (num, _) = e19.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e20:
+ (num, _) = e20.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e21:
+ (num, _) = e21.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e22:
+ (num, _) = e22.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e23:
+ (num, _) = e23.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e24:
+ (num, _) = e24.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e25:
+ (num, _) = e25.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e26:
+ (num, _) = e26.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e27:
+ (num, _) = e27.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+replace: userPassword
+userPassword: thatsAcomplPASS3
+""")
+ self.fail()
+ except LdbError as e28:
+ (num, _) = e28.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+replace: userPassword
+userPassword: thatsAcomplPASS3
+""")
+ self.fail()
+ except LdbError as e29:
+ (num, _) = e29.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ # Reverse order does work
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+add: userPassword
+userPassword: thatsAcomplPASS2
+delete: userPassword
+userPassword: thatsAcomplPASS1
+""")
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS2
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ # this passes against s4
+ except LdbError as e30:
+ (num, _) = e30.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')).decode('utf8') + """
+add: userPassword
+userPassword: thatsAcomplPASS4
+""")
+ # this passes against s4
+ except LdbError as e31:
+ (num, _) = e31.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # Several password changes at once are allowed
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+userPassword: thatsAcomplPASS2
+""")
+
+ # Several password changes at once are allowed
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+userPassword: thatsAcomplPASS2
+replace: userPassword
+userPassword: thatsAcomplPASS3
+replace: userPassword
+userPassword: thatsAcomplPASS4
+""")
+
+ # This surprisingly should work
+ delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS2"]})
+
+ # This surprisingly should work
+ delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS1"]})
+
+ def test_empty_passwords(self):
+ print("Performs some empty passwords testing")
+
+ try:
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "unicodePwd": []})
+ self.fail()
+ except LdbError as e32:
+ (num, _) = e32.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "dBCSPwd": []})
+ self.fail()
+ except LdbError as e33:
+ (num, _) = e33.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userPassword": []})
+ self.fail()
+ except LdbError as e34:
+ (num, _) = e34.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "clearTextPassword": []})
+ self.fail()
+ except LdbError as e35:
+ (num, _) = e35.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+
+ delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement([], FLAG_MOD_ADD, "unicodePwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e36:
+ (num, _) = e36.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["dBCSPwd"] = MessageElement([], FLAG_MOD_ADD, "dBCSPwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e37:
+ (num, _) = e37.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement([], FLAG_MOD_ADD, "userPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e38:
+ (num, _) = e38.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["clearTextPassword"] = MessageElement([], FLAG_MOD_ADD, "clearTextPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e39:
+ (num, _) = e39.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement([], FLAG_MOD_REPLACE, "unicodePwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e40:
+ (num, _) = e40.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["dBCSPwd"] = MessageElement([], FLAG_MOD_REPLACE, "dBCSPwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e41:
+ (num, _) = e41.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement([], FLAG_MOD_REPLACE, "userPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e42:
+ (num, _) = e42.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["clearTextPassword"] = MessageElement([], FLAG_MOD_REPLACE, "clearTextPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e43:
+ (num, _) = e43.args
+ self.assertTrue(num == ERR_UNWILLING_TO_PERFORM or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement([], FLAG_MOD_DELETE, "unicodePwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e44:
+ (num, _) = e44.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["dBCSPwd"] = MessageElement([], FLAG_MOD_DELETE, "dBCSPwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e45:
+ (num, _) = e45.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement([], FLAG_MOD_DELETE, "userPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e46:
+ (num, _) = e46.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["clearTextPassword"] = MessageElement([], FLAG_MOD_DELETE, "clearTextPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e47:
+ (num, _) = e47.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+
+ def test_plain_userPassword(self):
+ print("Performs testing about the standard 'userPassword' behaviour")
+
+ # Delete the "dSHeuristics"
+ self.ldb.set_dsheuristics(None)
+
+ time.sleep(1) # This switching time is strictly needed!
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("myPassword", FLAG_MOD_ADD,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "myPassword")
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("myPassword2", FLAG_MOD_REPLACE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "myPassword2")
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement([], FLAG_MOD_DELETE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ # Set the test "dSHeuristics" to deactivate "userPassword" pwd changes
+ self.ldb.set_dsheuristics("000000000")
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("myPassword3", FLAG_MOD_REPLACE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "myPassword3")
+
+ # Set the test "dSHeuristics" to deactivate "userPassword" pwd changes
+ self.ldb.set_dsheuristics("000000002")
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("myPassword4", FLAG_MOD_REPLACE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "myPassword4")
+
+ # Reset the test "dSHeuristics" (reactivate "userPassword" pwd changes)
+ self.ldb.set_dsheuristics("000000001")
+
+ def test_modify_dsheuristics_userPassword(self):
+ print("Performs testing about reading userPassword between dsHeuristic modifies")
+
+ # Make sure userPassword cannot be read
+ self.ldb.set_dsheuristics("000000000")
+
+ # Open a new connection (with dsHeuristic=000000000)
+ ldb1 = SamDB(url=host, session_info=system_session(lp),
+ credentials=creds, lp=lp)
+
+ # Set userPassword to be read
+ # This setting only affects newer connections (ldb2)
+ ldb1.set_dsheuristics("000000001")
+ time.sleep(1)
+
+ m = Message()
+ m.dn = Dn(ldb1, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("thatsAcomplPASS1", FLAG_MOD_REPLACE,
+ "userPassword")
+ ldb1.modify(m)
+
+ res = ldb1.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # userPassword cannot be read, it wasn't set, instead the
+ # password was
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ # Open another new connection (with dsHeuristic=000000001)
+ ldb2 = SamDB(url=host, session_info=system_session(lp),
+ credentials=creds, lp=lp)
+
+ res = ldb2.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # Check on the new connection that userPassword was not stored
+ # from ldb1 or is not readable
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ # Set userPassword to be readable
+ # This setting does not affect this connection
+ ldb2.set_dsheuristics("000000000")
+ time.sleep(1)
+
+ res = ldb2.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # Check that userPassword was not stored from ldb1
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ m = Message()
+ m.dn = Dn(ldb2, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
+ "userPassword")
+ ldb2.modify(m)
+
+ res = ldb2.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # Check despite setting it with userPassword support disabled
+ # on this connection it should still not be readable
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ # Only password from ldb1 is the user's password
+ creds2 = Credentials()
+ creds2.set_username("testuser")
+ creds2.set_password("thatsAcomplPASS1")
+ creds2.set_domain(creds.get_domain())
+ creds2.set_realm(creds.get_realm())
+ creds2.set_workstation(creds.get_workstation())
+ creds2.set_gensec_features(creds2.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+
+ try:
+ SamDB(url=host, credentials=creds2, lp=lp)
+ except:
+ self.fail("testuser used the wrong password")
+
+ ldb3 = SamDB(url=host, session_info=system_session(lp),
+ credentials=creds, lp=lp)
+
+ # Check that userPassword was stored from ldb2
+ res = ldb3.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # userPassword can be read
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "thatsAcomplPASS2")
+
+ # Reset the test "dSHeuristics" (reactivate "userPassword" pwd changes)
+ self.ldb.set_dsheuristics("000000001")
+
+ ldb4 = SamDB(url=host, session_info=system_session(lp),
+ credentials=creds, lp=lp)
+
+ # Check that userPassword that was stored from ldb2
+ res = ldb4.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # userPassword can be not be read
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ def test_zero_length(self):
+ # Get the old "minPwdLength"
+ minPwdLength = self.ldb.get_minPwdLength()
+ # Set it temporarily to "0"
+ self.ldb.set_minPwdLength("0")
+
+ # Get the old "pwdProperties"
+ pwdProperties = self.ldb.get_pwdProperties()
+ # Set them temporarily to "0" (to deactivate eventually the complexity)
+ self.ldb.set_pwdProperties("0")
+
+ self.ldb.setpassword("(sAMAccountName=testuser)", "")
+
+ # Reset the "pwdProperties" as they were before
+ self.ldb.set_pwdProperties(pwdProperties)
+
+ # Reset the "minPwdLength" as it was before
+ self.ldb.set_minPwdLength(minPwdLength)
+
+ def test_pw_change_delete_no_value_userPassword(self):
+ """Test password change with userPassword where the delete attribute doesn't have a value"""
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+add: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def test_pw_change_delete_no_value_clearTextPassword(self):
+ """Test password change with clearTextPassword where the delete attribute doesn't have a value"""
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: clearTextPassword
+add: clearTextPassword
+clearTextPassword: thatsAcomplPASS2
+""")
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+ else:
+ self.fail()
+
+ def test_pw_change_delete_no_value_unicodePwd(self):
+ """Test password change with unicodePwd where the delete attribute doesn't have a value"""
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def tearDown(self):
+ super(PasswordTests, self).tearDown()
+ delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
+ # Close the second LDB connection (with the user credentials)
+ self.ldb2 = None
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host_ldaps = None
+ host = "tdb://%s" % host
+ else:
+ host_ldaps = "ldaps://%s" % host
+ host = "ldap://%s" % host
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/priv_attrs.py b/source4/dsdb/tests/python/priv_attrs.py
new file mode 100644
index 0000000..4dfdfb9
--- /dev/null
+++ b/source4/dsdb/tests/python/priv_attrs.py
@@ -0,0 +1,398 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This tests the restrictions on userAccountControl that apply even if write access is permitted
+#
+# Copyright Samuel Cabrero 2014 <samuelcabrero@kernevil.me>
+# Copyright Andrew Bartlett 2014 <abartlet@samba.org>
+#
+# Licenced under the GPLv3
+#
+
+import optparse
+import sys
+import unittest
+import samba
+import samba.getopt as options
+import samba.tests
+import ldb
+import base64
+
+sys.path.insert(0, "bin/python")
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+from samba.tests import DynamicTestCase
+from samba.subunit.run import SubunitTestRunner
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.dcerpc import samr, security, lsa
+from samba.credentials import Credentials
+from samba.ndr import ndr_unpack, ndr_pack
+from samba.tests import delete_force
+from samba import gensec, sd_utils
+from samba.credentials import DONT_USE_KERBEROS
+from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from samba.dsdb import UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED, \
+ UF_LOCKOUT, UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,\
+ UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400, UF_INTERDOMAIN_TRUST_ACCOUNT, \
+ UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000, \
+ UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED, \
+ UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY, UF_DONT_REQUIRE_PREAUTH, \
+ UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_NO_AUTH_DATA_REQUIRED, \
+ UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS
+
+
+parser = optparse.OptionParser("user_account_control.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)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+host = args[0]
+
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+
+"""
+Check the combinations of:
+
+rodc kdc
+a2d2
+useraccountcontrol (trusted for delegation)
+sidHistory
+
+x
+
+add
+modify(replace)
+modify(add)
+
+x
+
+sd WP on add
+cc default perms
+admin created, WP to user
+
+x
+
+computer
+user
+"""
+
+attrs = {"sidHistory":
+ {"value": ndr_pack(security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS)),
+ "priv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+
+ "msDS-AllowedToDelegateTo":
+ {"value": f"host/{host}",
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+
+ "userAccountControl-a2d-user":
+ {"attr": "userAccountControl",
+ "value": str(UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION|UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+
+ "userAccountControl-a2d-computer":
+ {"attr": "userAccountControl",
+ "value": str(UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION|UF_WORKSTATION_TRUST_ACCOUNT),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "only-1": "computer"},
+
+ # This flag makes many legitimate authenticated clients
+ # send a forwardable ticket-granting-ticket to the server
+ "userAccountControl-t4d-user":
+ {"attr": "userAccountControl",
+ "value": str(UF_TRUSTED_FOR_DELEGATION|UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+
+ "userAccountControl-t4d-computer":
+ {"attr": "userAccountControl",
+ "value": str(UF_TRUSTED_FOR_DELEGATION|UF_WORKSTATION_TRUST_ACCOUNT),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "only-1": "computer"},
+
+ "userAccountControl-DC":
+ {"attr": "userAccountControl",
+ "value": str(UF_SERVER_TRUST_ACCOUNT),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "only-2": "computer"},
+
+ "userAccountControl-RODC":
+ {"attr": "userAccountControl",
+ "value": str(UF_PARTIAL_SECRETS_ACCOUNT|UF_WORKSTATION_TRUST_ACCOUNT),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "only-1": "computer"},
+
+ "msDS-SecondaryKrbTgtNumber":
+ {"value": "65536",
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+ "primaryGroupID":
+ {"value": str(security.DOMAIN_RID_ADMINS),
+ "priv-error": ldb.ERR_UNWILLING_TO_PERFORM,
+ "unpriv-add-error": ldb.ERR_UNWILLING_TO_PERFORM,
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS}
+ }
+
+
+
+@DynamicTestCase
+class PrivAttrsTests(samba.tests.TestCase):
+
+ def get_creds(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ return creds_tmp
+
+ def assertGotLdbError(self, wanted, got):
+ if not self.strict_checking:
+ self.assertNotEqual(got, ldb.SUCCESS)
+ else:
+ self.assertEqual(got, wanted)
+
+ def setUp(self):
+ super().setUp()
+
+ strict_checking = samba.tests.env_get_var_value('STRICT_CHECKING', allow_missing=True)
+ if strict_checking is None:
+ strict_checking = '1'
+ self.strict_checking = bool(int(strict_checking))
+
+ self.admin_creds = creds
+ self.admin_samdb = SamDB(url=ldaphost, credentials=self.admin_creds, lp=lp)
+ self.domain_sid = security.dom_sid(self.admin_samdb.get_domain_sid())
+ self.base_dn = self.admin_samdb.domain_dn()
+
+ self.unpriv_user = "testuser1"
+ self.unpriv_user_pw = "samba123@"
+ self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw)
+
+ self.admin_sd_utils = sd_utils.SDUtils(self.admin_samdb)
+
+ self.test_ou_name = "OU=test_priv_attrs"
+ self.test_ou = self.test_ou_name + "," + self.base_dn
+
+ delete_force(self.admin_samdb, self.test_ou, controls=["tree_delete:0"])
+
+ self.admin_samdb.create_ou(self.test_ou)
+
+ expected_user_dn = f"CN={self.unpriv_user},{self.test_ou_name},{self.base_dn}"
+
+ self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw, userou=self.test_ou_name)
+ res = self.admin_samdb.search(expected_user_dn,
+ scope=SCOPE_BASE,
+ attrs=["objectSid"])
+
+ self.assertEqual(1, len(res))
+
+ self.unpriv_user_dn = res[0].dn
+ self.addCleanup(delete_force, self.admin_samdb, self.unpriv_user_dn, controls=["tree_delete:0"])
+
+ self.unpriv_user_sid = self.admin_sd_utils.get_object_sid(self.unpriv_user_dn)
+
+ self.unpriv_samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
+
+ @classmethod
+ def setUpDynamicTestCases(cls):
+ for test_name in attrs.keys():
+ for add_or_mod in ["add", "mod-del-add", "mod-replace"]:
+ for permission in ["admin-add", "CC"]:
+ for sd in ["default", "WP"]:
+ for objectclass in ["computer", "user"]:
+ tname = f"{test_name}_{add_or_mod}_{permission}_{sd}_{objectclass}"
+ targs = (test_name,
+ add_or_mod,
+ permission,
+ sd,
+ objectclass)
+ cls.generate_dynamic_test("test_priv_attr",
+ tname,
+ *targs)
+
+ def add_computer_ldap(self, computername, others=None, samdb=None):
+ dn = "CN=%s,%s" % (computername, self.test_ou)
+ domainname = ldb.Dn(samdb, samdb.domain_dn()).canonical_str().replace("/", "")
+ samaccountname = "%s$" % computername
+ dnshostname = "%s.%s" % (computername, domainname)
+ msg_dict = {
+ "dn": dn,
+ "objectclass": "computer"}
+ if others is not None:
+ msg_dict = dict(list(msg_dict.items()) + list(others.items()))
+
+ msg = ldb.Message.from_dict(samdb, msg_dict)
+ msg["sAMAccountName"] = samaccountname
+
+ print("Adding computer account %s" % computername)
+ try:
+ samdb.add(msg)
+ except ldb.LdbError:
+ print(msg)
+ raise
+ return msg.dn
+
+ def add_user_ldap(self, username, others=None, samdb=None):
+ dn = "CN=%s,%s" % (username, self.test_ou)
+ domainname = ldb.Dn(samdb, samdb.domain_dn()).canonical_str().replace("/", "")
+ samaccountname = "%s$" % username
+ msg_dict = {
+ "dn": dn,
+ "objectclass": "user"}
+ if others is not None:
+ msg_dict = dict(list(msg_dict.items()) + list(others.items()))
+
+ msg = ldb.Message.from_dict(samdb, msg_dict)
+ msg["sAMAccountName"] = samaccountname
+
+ print("Adding user account %s" % username)
+ try:
+ samdb.add(msg)
+ except ldb.LdbError:
+ print(msg)
+ raise
+ return msg.dn
+
+ def add_thing_ldap(self, user, others, samdb, objectclass):
+ if objectclass == "user":
+ dn = self.add_user_ldap(user, others, samdb=samdb)
+ elif objectclass == "computer":
+ dn = self.add_computer_ldap(user, others, samdb=samdb)
+ return dn
+
+ def _test_priv_attr_with_args(self, test_name, add_or_mod, permission, sd, objectclass):
+ user="privattrs"
+ if "attr" in attrs[test_name]:
+ attr = attrs[test_name]["attr"]
+ else:
+ attr = test_name
+ if add_or_mod == "add":
+ others = {attr: attrs[test_name]["value"]}
+ else:
+ others = {}
+
+ if permission == "CC":
+ samdb = self.unpriv_samdb
+ # Set CC on container to allow user add
+ mod = "(OA;CI;CC;bf967aba-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid)
+ self.admin_sd_utils.dacl_add_ace(self.test_ou, mod)
+ mod = "(OA;CI;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid)
+ self.admin_sd_utils.dacl_add_ace(self.test_ou, mod)
+
+ else:
+ samdb = self.admin_samdb
+
+ if sd == "WP":
+ # Set SD to WP to the target user as part of add
+ sd = "O:%sG:DUD:(OA;CIID;RPWP;;;%s)(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;%s)" % (self.unpriv_user_sid, self.unpriv_user_sid, self.unpriv_user_sid)
+ tmp_desc = security.descriptor.from_sddl(sd, self.domain_sid)
+ others["ntSecurityDescriptor"] = ndr_pack(tmp_desc)
+
+ if add_or_mod == "add":
+
+ # only-1 and only-2 are due to windows behaviour
+
+ if "only-1" in attrs[test_name] and \
+ attrs[test_name]["only-1"] != objectclass:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN (should fail LDAP_OBJECT_CLASS_VIOLATION)")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertGotLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION, enum)
+ elif permission == "CC":
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on new {objectclass}")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ if "unpriv-add-error" in attrs[test_name]:
+ self.assertGotLdbError(attrs[test_name]["unpriv-add-error"], \
+ enum)
+ else:
+ self.assertGotLdbError(attrs[test_name]["unpriv-error"], \
+ enum)
+ elif "only-2" in attrs[test_name] and \
+ attrs[test_name]["only-2"] != objectclass:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN (should fail LDAP_OBJECT_CLASS_VIOLATION)")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertGotLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION, enum)
+ elif "priv-error" in attrs[test_name]:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertGotLdbError(attrs[test_name]["priv-error"], enum)
+ else:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.fail(f"Failed to add account {user} as objectclass {objectclass}")
+ else:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.fail(f"Failed to add account {user} as objectclass {objectclass}")
+
+ if add_or_mod == "add":
+ return
+
+ m = ldb.Message()
+ m.dn = dn
+
+ # Do modify
+ if add_or_mod == "mod-del-add":
+ m["0"] = ldb.MessageElement([],
+ ldb.FLAG_MOD_DELETE,
+ attr)
+ m["1"] = ldb.MessageElement(attrs[test_name]["value"],
+ ldb.FLAG_MOD_ADD,
+ attr)
+ else:
+ m["0"] = ldb.MessageElement(attrs[test_name]["value"],
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+
+ try:
+ self.unpriv_samdb.modify(m)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on {m.dn}")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertGotLdbError(attrs[test_name]["unpriv-error"], enum)
+
+
+
+
+runner = SubunitTestRunner()
+rc = 0
+if not runner.run(unittest.makeSuite(PrivAttrsTests)).wasSuccessful():
+ rc = 1
+sys.exit(rc)
diff --git a/source4/dsdb/tests/python/rodc.py b/source4/dsdb/tests/python/rodc.py
new file mode 100755
index 0000000..1a4b0f9
--- /dev/null
+++ b/source4/dsdb/tests/python/rodc.py
@@ -0,0 +1,258 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+import os
+import base64
+import random
+import re
+import uuid
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import drsblobs
+
+import time
+
+
+class RodcTestException(Exception):
+ pass
+
+
+class RodcTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(RodcTests, self).setUp()
+ self.samdb = SamDB(HOST, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ self.base_dn = self.samdb.domain_dn()
+
+ root = self.samdb.search(base='', scope=ldb.SCOPE_BASE,
+ attrs=['dsServiceName'])
+ self.service = root[0]['dsServiceName'][0]
+ self.tag = uuid.uuid4().hex
+
+ def test_add_replicated_objects(self):
+ for o in (
+ {
+ 'dn': "ou=%s1,%s" % (self.tag, self.base_dn),
+ "objectclass": "organizationalUnit"
+ },
+ {
+ 'dn': "cn=%s2,%s" % (self.tag, self.base_dn),
+ "objectclass": "user"
+ },
+ {
+ 'dn': "cn=%s3,%s" % (self.tag, self.base_dn),
+ "objectclass": "group"
+ },
+ {
+ 'dn': "cn=%s4,%s" % (self.tag, self.service),
+ "objectclass": "NTDSConnection",
+ 'enabledConnection': 'TRUE',
+ 'fromServer': self.base_dn,
+ 'options': '0'
+ },
+ ):
+ try:
+ self.samdb.add(o)
+ self.fail("Failed to fail to add %s" % o['dn'])
+ except ldb.LdbError as e:
+ (ecode, emsg) = e.args
+ if ecode != ldb.ERR_REFERRAL:
+ print(emsg)
+ self.fail("Adding %s: ldb error: %s %s, wanted referral" %
+ (o['dn'], ecode, emsg))
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ try:
+ tmpdb = SamDB(address, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+ tmpdb.add(o)
+ tmpdb.delete(o['dn'])
+ except ldb.LdbError as e:
+ self.fail("couldn't modify referred location %s" %
+ address)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_modify_replicated_attributes(self):
+ # some timestamp ones
+ dn = 'CN=Guest,CN=Users,' + self.base_dn
+ value = 'hallooo'
+ for attr in ['carLicense', 'middleName']:
+ msg = ldb.Message()
+ msg.dn = ldb.Dn(self.samdb, dn)
+ msg[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ try:
+ self.samdb.modify(msg)
+ self.fail("Failed to fail to modify %s %s" % (dn, attr))
+ except ldb.LdbError as e1:
+ (ecode, emsg) = e1.args
+ if ecode != ldb.ERR_REFERRAL:
+ self.fail("Failed to REFER when trying to modify %s %s" %
+ (dn, attr))
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ try:
+ tmpdb = SamDB(address, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+ tmpdb.modify(msg)
+ except ldb.LdbError as e:
+ self.fail("couldn't modify referred location %s" %
+ address)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_modify_nonreplicated_attributes(self):
+ # some timestamp ones
+ dn = 'CN=Guest,CN=Users,' + self.base_dn
+ value = '123456789'
+ for attr in ['badPwdCount', 'lastLogon', 'lastLogoff']:
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, dn)
+ m[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ # Windows refers these ones even though they are non-replicated
+ try:
+ self.samdb.modify(m)
+ self.fail("Failed to fail to modify %s %s" % (dn, attr))
+ except ldb.LdbError as e2:
+ (ecode, emsg) = e2.args
+ if ecode != ldb.ERR_REFERRAL:
+ self.fail("Failed to REFER when trying to modify %s %s" %
+ (dn, attr))
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_modify_nonreplicated_reps_attributes(self):
+ # some timestamp ones
+ dn = self.base_dn
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, dn)
+ attr = 'repsFrom'
+
+ res = self.samdb.search(dn, scope=ldb.SCOPE_BASE,
+ attrs=['repsFrom'])
+ rep = ndr_unpack(drsblobs.repsFromToBlob, res[0]['repsFrom'][0],
+ allow_remaining=True)
+ rep.ctr.result_last_attempt = -1
+ value = ndr_pack(rep)
+
+ m[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ try:
+ self.samdb.modify(m)
+ self.fail("Failed to fail to modify %s %s" % (dn, attr))
+ except ldb.LdbError as e3:
+ (ecode, emsg) = e3.args
+ if ecode != ldb.ERR_REFERRAL:
+ self.fail("Failed to REFER when trying to modify %s %s" %
+ (dn, attr))
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_delete_special_objects(self):
+ dn = 'CN=Guest,CN=Users,' + self.base_dn
+ try:
+ self.samdb.delete(dn)
+ self.fail("Failed to fail to delete %s" % (dn))
+ except ldb.LdbError as e4:
+ (ecode, emsg) = e4.args
+ if ecode != ldb.ERR_REFERRAL:
+ print(ecode, emsg)
+ self.fail("Failed to REFER when trying to delete %s" % dn)
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_no_delete_nonexistent_objects(self):
+ dn = 'CN=does-not-exist-%s,CN=Users,%s' % (self.tag, self.base_dn)
+ try:
+ self.samdb.delete(dn)
+ self.fail("Failed to fail to delete %s" % (dn))
+ except ldb.LdbError as e5:
+ (ecode, emsg) = e5.args
+ if ecode != ldb.ERR_NO_SUCH_OBJECT:
+ print(ecode, emsg)
+ self.fail("Failed to NO_SUCH_OBJECT when trying to delete "
+ "%s (which does not exist)" % dn)
+
+
+def main():
+ global HOST, CREDS, LP
+ parser = optparse.OptionParser("rodc.py [options] <host>")
+
+ sambaopts = options.SambaOptions(parser)
+ versionopts = options.VersionOptions(parser)
+ credopts = options.CredentialsOptions(parser)
+ subunitopts = SubunitOptions(parser)
+
+ parser.add_option_group(sambaopts)
+ parser.add_option_group(versionopts)
+ parser.add_option_group(credopts)
+ parser.add_option_group(subunitopts)
+
+ opts, args = parser.parse_args()
+
+ LP = sambaopts.get_loadparm()
+ CREDS = credopts.get_credentials(LP)
+
+ try:
+ HOST = args[0]
+ except IndexError:
+ parser.print_usage()
+ sys.exit(1)
+
+ if "://" not in HOST:
+ if os.path.isfile(HOST):
+ HOST = "tdb://%s" % HOST
+ else:
+ HOST = "ldap://%s" % HOST
+
+ TestProgram(module=__name__, opts=subunitopts)
+
+
+main()
diff --git a/source4/dsdb/tests/python/rodc_rwdc.py b/source4/dsdb/tests/python/rodc_rwdc.py
new file mode 100644
index 0000000..53d5480
--- /dev/null
+++ b/source4/dsdb/tests/python/rodc_rwdc.py
@@ -0,0 +1,1325 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+"""Test communication of credentials etc, between an RODC and a RWDC.
+
+How does it work when the password is changed on the RWDC?
+"""
+
+import optparse
+import sys
+import base64
+import uuid
+import subprocess
+import itertools
+import time
+
+sys.path.insert(0, "bin/python")
+import samba
+import ldb
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS
+from samba import gensec, dsdb
+from ldb import SCOPE_BASE, LdbError, ERR_INVALID_CREDENTIALS
+from samba.dcerpc import security, samr
+import os
+
+import password_lockout_base
+
+def adjust_cmd_for_py_version(parts):
+ if os.getenv("PYTHON", None):
+ parts.insert(0, os.environ["PYTHON"])
+ return parts
+
+def passwd_encode(pw):
+ return base64.b64encode(('"%s"' % pw).encode('utf-16-le')).decode('utf8')
+
+
+class RodcRwdcTestException(Exception):
+ pass
+
+
+def make_creds(username, password, kerberos_state=None, simple_dn=None):
+ # use the global CREDS as a template
+ c = Credentials()
+ c.set_username(username)
+ c.set_password(password)
+ c.set_domain(CREDS.get_domain())
+ c.set_realm(CREDS.get_realm())
+ c.set_workstation(CREDS.get_workstation())
+
+ if simple_dn is not None:
+ c.set_bind_dn(simple_dn)
+
+ if kerberos_state is None:
+ kerberos_state = CREDS.get_kerberos_state()
+ c.set_kerberos_state(kerberos_state)
+
+ print('-' * 73)
+ if kerberos_state == MUST_USE_KERBEROS:
+ print("we seem to be using kerberos for %s %s" % (username, password))
+ elif kerberos_state == DONT_USE_KERBEROS:
+ print("NOT using kerberos for %s %s" % (username, password))
+ else:
+ print("kerberos state is %s" % kerberos_state)
+
+ c.set_gensec_features(c.get_gensec_features() |
+ gensec.FEATURE_SEAL)
+ return c
+
+
+def set_auto_replication(dc, allow):
+ credstring = '-U%s%%%s' % (CREDS.get_username(),
+ CREDS.get_password())
+
+ on_or_off = '-' if allow else '+'
+
+ for opt in ['DISABLE_INBOUND_REPL',
+ 'DISABLE_OUTBOUND_REPL']:
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'drs', 'options',
+ credstring, dc,
+ "--dsa-option=%s%s" % (on_or_off, opt)])
+
+ p = subprocess.Popen(cmd,
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode:
+ if b'LDAP_REFERRAL' not in stderr:
+ raise RodcRwdcTestException()
+ print("ignoring +%s REFERRAL error; assuming %s is RODC" %
+ (opt, dc))
+
+
+def preload_rodc_user(user_dn):
+ credstring = '-U%s%%%s' % (CREDS.get_username(),
+ CREDS.get_password())
+
+ set_auto_replication(RWDC, True)
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'rodc', 'preload',
+ user_dn,
+ credstring,
+ '--server', RWDC, ])
+
+ print(' '.join(cmd))
+ subprocess.check_call(cmd)
+ set_auto_replication(RWDC, False)
+
+
+def get_server_ref_from_samdb(samdb):
+ server_name = samdb.get_serverName()
+ res = samdb.search(server_name,
+ scope=ldb.SCOPE_BASE,
+ attrs=['serverReference'])
+
+ return res[0]['serverReference'][0]
+
+
+class RodcRwdcCachedTests(password_lockout_base.BasePasswordTestCase):
+
+ def _check_account_initial(self, dn):
+ self.force_replication()
+ return super(RodcRwdcCachedTests, self)._check_account_initial(dn)
+
+ def _check_account(self, dn,
+ badPwdCount=None,
+ badPasswordTime=None,
+ logonCount=None,
+ lastLogon=None,
+ lastLogonTimestamp=None,
+ lockoutTime=None,
+ userAccountControl=None,
+ msDSUserAccountControlComputed=None,
+ effective_bad_password_count=None,
+ msg=None,
+ badPwdCountOnly=False):
+ # Wait for the RWDC to get any delayed messages
+ # e.g. SendToSam or KRB5 bad passwords via winbindd
+ if (self.kerberos and isinstance(badPasswordTime, tuple) or
+ badPwdCount == 0):
+ time.sleep(5)
+
+ return super(RodcRwdcCachedTests,
+ self)._check_account(dn, badPwdCount, badPasswordTime,
+ logonCount, lastLogon,
+ lastLogonTimestamp, lockoutTime,
+ userAccountControl,
+ msDSUserAccountControlComputed,
+ effective_bad_password_count, msg,
+ True)
+
+ def force_replication(self, base=None):
+ if base is None:
+ base = self.base_dn
+
+ # XXX feels like a horrendous way to do it.
+ credstring = '-U%s%%%s' % (CREDS.get_username(),
+ CREDS.get_password())
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'drs', 'replicate',
+ RODC, RWDC, base,
+ credstring,
+ '--sync-forced'])
+
+ p = subprocess.Popen(cmd,
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode:
+ print("failed with code %s" % p.returncode)
+ print(' '.join(cmd))
+ print("stdout")
+ print(stdout)
+ print("stderr")
+ print(stderr)
+ raise RodcRwdcTestException()
+
+ def _change_password(self, user_dn, old_password, new_password):
+ self.rwdc_db.modify_ldif(
+ "dn: %s\n"
+ "changetype: modify\n"
+ "delete: userPassword\n"
+ "userPassword: %s\n"
+ "add: userPassword\n"
+ "userPassword: %s\n" % (user_dn, old_password, new_password))
+
+ def tearDown(self):
+ super(RodcRwdcCachedTests, self).tearDown()
+ set_auto_replication(RWDC, True)
+
+ def setUp(self):
+ self.kerberos = False # To be set later
+
+ self.rodc_db = SamDB('ldap://%s' % RODC, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ self.rwdc_db = SamDB('ldap://%s' % RWDC, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ # Define variables for BasePasswordTestCase
+ self.lp = LP
+ self.global_creds = CREDS
+ self.host = RWDC
+ self.host_url = 'ldap://%s' % RWDC
+ self.host_url_ldaps = 'ldaps://%s' % RWDC
+ self.ldb = SamDB(url='ldap://%s' % RWDC, session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+
+ super(RodcRwdcCachedTests, self).setUp()
+ self.host_url = 'ldap://%s' % RODC
+ self.host_url_ldaps = 'ldaps://%s' % RODC
+
+ self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % self.host, self.lp, self.global_creds)
+ self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
+
+ self.base_dn = self.rwdc_db.domain_dn()
+
+ root = self.rodc_db.search(base='', scope=ldb.SCOPE_BASE,
+ attrs=['dsServiceName'])
+ self.service = root[0]['dsServiceName'][0]
+ self.tag = uuid.uuid4().hex
+
+ self.rwdc_dsheuristics = self.rwdc_db.get_dsheuristics()
+ self.rwdc_db.set_dsheuristics("000000001")
+
+ set_auto_replication(RWDC, False)
+
+ # make sure DCs are synchronized before the test
+ self.force_replication()
+
+ def delete_ldb_connections(self):
+ super(RodcRwdcCachedTests, self).delete_ldb_connections()
+ del self.rwdc_db
+ del self.rodc_db
+
+ def test_cache_and_flush_password(self):
+ username = self.lockout1krb5_creds.get_username()
+ userpass = self.lockout1krb5_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ ldb_system = SamDB(session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+
+ res = ldb_system.search(userdn, attrs=['unicodePwd'])
+ self.assertFalse('unicodePwd' in res[0])
+
+ preload_rodc_user(userdn)
+
+ res = ldb_system.search(userdn, attrs=['unicodePwd'])
+ self.assertTrue('unicodePwd' in res[0])
+
+ # force replication here to flush any pending preloads (this
+ # was a racy test).
+ self.force_replication()
+
+ newpass = userpass + '!'
+
+ # Forcing replication should blank out password (when changed)
+ self._change_password(userdn, userpass, newpass)
+ self.force_replication()
+
+ res = ldb_system.search(userdn, attrs=['unicodePwd'])
+ self.assertFalse('unicodePwd' in res[0])
+
+ def test_login_lockout_krb5(self):
+ username = self.lockout1krb5_creds.get_username()
+ userpass = self.lockout1krb5_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ self.kerberos = True
+
+ self.rodc_dn = get_server_ref_from_samdb(self.rodc_db)
+
+ res = self.rodc_db.search(self.rodc_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['msDS-RevealOnDemandGroup'])
+
+ group = res[0]['msDS-RevealOnDemandGroup'][0].decode('utf8')
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, group)
+ m['member'] = ldb.MessageElement(userdn, ldb.FLAG_MOD_ADD, 'member')
+ self.rwdc_db.modify(m)
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, self.base_dn)
+
+ self.account_lockout_duration = 15
+ account_lockout_duration_ticks = -int(self.account_lockout_duration * (1e7))
+
+ m["lockoutDuration"] = ldb.MessageElement(str(account_lockout_duration_ticks),
+ ldb.FLAG_MOD_REPLACE,
+ "lockoutDuration")
+
+ self.lockout_observation_window = 15
+ lockout_observation_window_ticks = -int(self.lockout_observation_window * (1e7))
+
+ m["lockOutObservationWindow"] = ldb.MessageElement(str(lockout_observation_window_ticks),
+ ldb.FLAG_MOD_REPLACE,
+ "lockOutObservationWindow")
+
+ self.rwdc_db.modify(m)
+ self.force_replication()
+
+ self._test_login_lockout_rodc_rwdc(self.lockout1krb5_creds, userdn)
+
+ def test_login_lockout_ntlm(self):
+ username = self.lockout1ntlm_creds.get_username()
+ userpass = self.lockout1ntlm_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ self.kerberos = False
+
+ self.rodc_dn = get_server_ref_from_samdb(self.rodc_db)
+
+ res = self.rodc_db.search(self.rodc_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['msDS-RevealOnDemandGroup'])
+
+ group = res[0]['msDS-RevealOnDemandGroup'][0].decode('utf8')
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, group)
+ m['member'] = ldb.MessageElement(userdn, ldb.FLAG_MOD_ADD, 'member')
+ self.rwdc_db.modify(m)
+
+ self._test_login_lockout_rodc_rwdc(self.lockout1ntlm_creds, userdn)
+
+ def test_login_lockout_not_revealed(self):
+ '''Test that SendToSam is restricted by preloaded users/groups'''
+
+ username = self.lockout1ntlm_creds.get_username()
+ userpass = self.lockout1ntlm_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ # Preload but do not add to revealed group
+ preload_rodc_user(userdn)
+
+ self.kerberos = False
+
+ creds = self.lockout1ntlm_creds
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ creds_lockout = self.insta_creds(creds)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ badPasswordTime = 0
+ logonCount = 0
+ lastLogon = 0
+ lastLogonTimestamp = 0
+ logoncount_relation = ''
+ lastlogon_relation = ''
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='lastlogontimestamp with wrong password')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # BadPwdCount on RODC increases alongside RWDC
+ res = self.rodc_db.search(userdn, attrs=['badPwdCount'])
+ self.assertTrue('badPwdCount' in res[0])
+ self.assertEqual(int(res[0]['badPwdCount'][0]), 1)
+
+ # Correct old password
+ creds_lockout.set_password(userpass)
+
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ # Wait for potential SendToSam...
+ time.sleep(5)
+
+ # BadPwdCount on RODC decreases, but not the RWDC
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='badPwdCount not reset on RWDC')
+
+ res = self.rodc_db.search(userdn, attrs=['badPwdCount'])
+ self.assertTrue('badPwdCount' in res[0])
+ self.assertEqual(int(res[0]['badPwdCount'][0]), 0)
+
+ def _test_login_lockout_rodc_rwdc(self, creds, userdn):
+ username = creds.get_username()
+ userpass = creds.get_password()
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ creds_lockout = self.insta_creds(creds)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ badPasswordTime = 0
+ logonCount = 0
+ lastLogon = 0
+ lastLogonTimestamp = 0
+ logoncount_relation = ''
+ lastlogon_relation = ''
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='lastlogontimestamp with wrong password')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Correct old password
+ creds_lockout.set_password(userpass)
+
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ # lastLogonTimestamp should not change
+ # lastLogon increases if badPwdCount is non-zero (!)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='LLTimestamp is updated to lastlogon')
+
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+
+ except LdbError as e1:
+ (num, msg) = e1.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ print("two failed password change")
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+
+ except LdbError as e2:
+ (num, msg) = e2.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e3:
+ (num, msg) = e3.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # The correct password, but we are locked out
+ creds_lockout.set_password(userpass)
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # wait for the lockout to end
+ time.sleep(self.account_lockout_duration + 1)
+ print(self.account_lockout_duration + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=3, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The correct password after letting the timeout expire
+
+ creds_lockout.set_password(userpass)
+
+ creds_lockout2 = self.insta_creds(creds_lockout)
+
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout2, lp=self.lp)
+ time.sleep(3)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg="lastLogon is way off")
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e6:
+ (num, msg) = e6.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e7:
+ (num, msg) = e7.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ time.sleep(self.lockout_observation_window + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=2, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e8:
+ (num, msg) = e8.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The correct password without letting the timeout expire
+ creds_lockout.set_password(userpass)
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lockoutTime=lockoutTime,
+ lastLogon=("greater", lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+
+class RodcRwdcTests(password_lockout_base.BasePasswordTestCase):
+ counter = itertools.count(1, 1)
+
+ def force_replication(self, base=None):
+ if base is None:
+ base = self.base_dn
+
+ # XXX feels like a horrendous way to do it.
+ credstring = '-U%s%%%s' % (CREDS.get_username(),
+ CREDS.get_password())
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'drs', 'replicate',
+ RODC, RWDC, base,
+ credstring,
+ '--sync-forced'])
+
+ p = subprocess.Popen(cmd,
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode:
+ print("failed with code %s" % p.returncode)
+ print(' '.join(cmd))
+ print("stdout")
+ print(stdout)
+ print("stderr")
+ print(stderr)
+ raise RodcRwdcTestException()
+
+ def _check_account_initial(self, dn):
+ self.force_replication()
+ return super(RodcRwdcTests, self)._check_account_initial(dn)
+
+ def tearDown(self):
+ super(RodcRwdcTests, self).tearDown()
+ self.rwdc_db.set_dsheuristics(self.rwdc_dsheuristics)
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ set_auto_replication(RWDC, True)
+
+ def setUp(self):
+ self.rodc_db = SamDB('ldap://%s' % RODC, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ self.rwdc_db = SamDB('ldap://%s' % RWDC, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ # Define variables for BasePasswordTestCase
+ self.lp = LP
+ self.global_creds = CREDS
+ self.host = RWDC
+ self.host_url = 'ldap://%s' % RWDC
+ self.host_url_ldaps = 'ldaps://%s' % RWDC
+ self.ldb = SamDB(url='ldap://%s' % RWDC, session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+
+ super(RodcRwdcTests, self).setUp()
+ self.host = RODC
+ self.host_url = 'ldap://%s' % RODC
+ self.host_url_ldaps = 'ldaps://%s' % RODC
+ self.ldb = SamDB(url='ldap://%s' % RODC, session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+
+ self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % self.host, self.lp, self.global_creds)
+ self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
+
+ self.base_dn = self.rwdc_db.domain_dn()
+
+ root = self.rodc_db.search(base='', scope=ldb.SCOPE_BASE,
+ attrs=['dsServiceName'])
+ self.service = root[0]['dsServiceName'][0]
+ self.tag = uuid.uuid4().hex
+
+ self.rwdc_dsheuristics = self.rwdc_db.get_dsheuristics()
+ self.rwdc_db.set_dsheuristics("000000001")
+
+ set_auto_replication(RWDC, False)
+
+ # make sure DCs are synchronized before the test
+ self.force_replication()
+ self.rwdc_dn = get_server_ref_from_samdb(self.rwdc_db)
+ self.rodc_dn = get_server_ref_from_samdb(self.rodc_db)
+
+ def delete_ldb_connections(self):
+ super(RodcRwdcTests, self).delete_ldb_connections()
+ del self.rwdc_db
+ del self.rodc_db
+
+ def assertReferral(self, fn, *args, **kwargs):
+ try:
+ fn(*args, **kwargs)
+ self.fail("failed to raise ldap referral")
+ except ldb.LdbError as e9:
+ (code, msg) = e9.args
+ self.assertEqual(code, ldb.ERR_REFERRAL,
+ "expected referral, got %s %s" % (code, msg))
+
+ def _test_rodc_dsheuristics(self):
+ d = self.rodc_db.get_dsheuristics()
+ self.assertReferral(self.rodc_db.set_dsheuristics, "000000001")
+ self.assertReferral(self.rodc_db.set_dsheuristics, d)
+
+ def TEST_rodc_heuristics_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_rodc_dsheuristics()
+
+ def TEST_rodc_heuristics_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_rodc_dsheuristics()
+
+ def _test_add(self, objects, cross_ncs=False):
+ for o in objects:
+ dn = o['dn']
+ if cross_ncs:
+ base = str(self.rwdc_db.get_config_basedn())
+ controls = ["search_options:1:2"]
+ cn = dn.split(',', 1)[0]
+ expression = '(%s)' % cn
+ else:
+ base = dn
+ controls = []
+ expression = None
+
+ try:
+ res = self.rodc_db.search(base,
+ expression=expression,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=['dn'],
+ controls=controls)
+ self.assertEqual(len(res), 0)
+ except ldb.LdbError as e:
+ if e.args[0] != ldb.ERR_NO_SUCH_OBJECT:
+ raise
+
+ try:
+ self.rwdc_db.add(o)
+ except ldb.LdbError as e:
+ (ecode, emsg) = e.args
+ self.fail("Failed to add %s to rwdc: ldb error: %s %s" %
+ (o, ecode, emsg))
+
+ if cross_ncs:
+ self.force_replication(base=base)
+ else:
+ self.force_replication()
+
+ try:
+ res = self.rodc_db.search(base,
+ expression=expression,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=['dn'],
+ controls=controls)
+ self.assertEqual(len(res), 1)
+ except ldb.LdbError as e:
+ self.assertNotEqual(e.args[0], ldb.ERR_NO_SUCH_OBJECT,
+ "replication seems to have failed")
+
+ def _test_add_replicated_objects(self, mode):
+ tag = "%s%s" % (self.tag, mode)
+ self._test_add([
+ {
+ 'dn': "ou=%s1,%s" % (tag, self.base_dn),
+ "objectclass": "organizationalUnit"
+ },
+ {
+ 'dn': "cn=%s2,%s" % (tag, self.base_dn),
+ "objectclass": "user"
+ },
+ {
+ 'dn': "cn=%s3,%s" % (tag, self.base_dn),
+ "objectclass": "group"
+ },
+ ])
+ self.rwdc_db.delete("ou=%s1,%s" % (tag, self.base_dn))
+ self.rwdc_db.delete("cn=%s2,%s" % (tag, self.base_dn))
+ self.rwdc_db.delete("cn=%s3,%s" % (tag, self.base_dn))
+
+ def test_add_replicated_objects_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_add_replicated_objects('kerberos')
+
+ def test_add_replicated_objects_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_add_replicated_objects('ntlm')
+
+ def _test_add_replicated_connections(self, mode):
+ tag = "%s%s" % (self.tag, mode)
+ self._test_add([
+ {
+ 'dn': "cn=%sfoofoofoo,%s" % (tag, self.service),
+ "objectclass": "NTDSConnection",
+ 'enabledConnection': 'TRUE',
+ 'fromServer': self.base_dn,
+ 'options': '0'
+ },
+ ], cross_ncs=True)
+ self.rwdc_db.delete("cn=%sfoofoofoo,%s" % (tag, self.service))
+
+ def test_add_replicated_connections_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_add_replicated_connections('kerberos')
+
+ def test_add_replicated_connections_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_add_replicated_connections('ntlm')
+
+ def _test_modify_replicated_attributes(self):
+ dn = 'CN=Guest,CN=Users,' + self.base_dn
+ value = self.tag
+ for attr in ['carLicense', 'middleName']:
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, dn)
+ m[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ try:
+ self.rwdc_db.modify(m)
+ except ldb.LdbError as e:
+ self.fail("Failed to modify %s %s on RWDC %s with %s" %
+ (dn, attr, RWDC, e))
+
+ self.force_replication()
+
+ try:
+ res = self.rodc_db.search(dn,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr])
+ results = [str(x[attr][0]) for x in res]
+ self.assertEqual(results, [value])
+ except ldb.LdbError as e:
+ self.assertNotEqual(e.args[0], ldb.ERR_NO_SUCH_OBJECT,
+ "replication seems to have failed")
+
+ def test_modify_replicated_attributes_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_modify_replicated_attributes()
+
+ def test_modify_replicated_attributes_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_modify_replicated_attributes()
+
+ def _test_add_modify_delete(self):
+ dn = "cn=%s_add_modify,%s" % (self.tag, self.base_dn)
+ values = ["%s%s" % (i, self.tag) for i in range(3)]
+ attr = "carLicense"
+ self._test_add([
+ {
+ 'dn': dn,
+ "objectclass": "user",
+ attr: values[0]
+ },
+ ])
+ self.force_replication()
+ for value in values[1:]:
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, dn)
+ m[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ try:
+ self.rwdc_db.modify(m)
+ except ldb.LdbError as e:
+ self.fail("Failed to modify %s %s on RWDC %s with %s" %
+ (dn, attr, RWDC, e))
+
+ self.force_replication()
+
+ try:
+ res = self.rodc_db.search(dn,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr])
+ results = [str(x[attr][0]) for x in res]
+ self.assertEqual(results, [value])
+ except ldb.LdbError as e:
+ self.assertNotEqual(e.args[0], ldb.ERR_NO_SUCH_OBJECT,
+ "replication seems to have failed")
+
+ self.rwdc_db.delete(dn)
+ self.force_replication()
+ try:
+ res = self.rodc_db.search(dn,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr])
+ if len(res) > 0:
+ self.fail("Failed to delete %s" % (dn))
+ except ldb.LdbError as e:
+ self.assertEqual(e.args[0], ldb.ERR_NO_SUCH_OBJECT,
+ "Failed to delete %s" % (dn))
+
+ def test_add_modify_delete_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_add_modify_delete()
+
+ def test_add_modify_delete_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_add_modify_delete()
+
+ def _new_user(self):
+ username = "u%sX%s" % (self.tag[:12], next(self.counter))
+ password = 'password#1'
+ dn = 'CN=%s,CN=Users,%s' % (username, self.base_dn)
+ o = {
+ 'dn': dn,
+ "objectclass": "user",
+ 'sAMAccountName': username,
+ }
+ try:
+ self.rwdc_db.add(o)
+ except ldb.LdbError as e:
+ self.fail("Failed to add %s to rwdc: ldb error: %s" % (o, e))
+
+ self.rwdc_db.modify_ldif("dn: %s\n"
+ "changetype: modify\n"
+ "delete: userPassword\n"
+ "add: userPassword\n"
+ "userPassword: %s\n" % (dn, password))
+ self.rwdc_db.enable_account("(sAMAccountName=%s)" % username)
+ return (dn, username, password)
+
+ def _change_password(self, user_dn, old_password, new_password):
+ self.rwdc_db.modify_ldif(
+ "dn: %s\n"
+ "changetype: modify\n"
+ "delete: userPassword\n"
+ "userPassword: %s\n"
+ "add: userPassword\n"
+ "userPassword: %s\n" % (user_dn, old_password, new_password))
+
+ def try_ldap_logon(self, server, creds, errno=None, simple=False):
+ try:
+ if simple:
+ tmpdb = SamDB('ldaps://%s' % server, credentials=creds,
+ session_info=system_session(LP), lp=LP)
+ else:
+ tmpdb = SamDB('ldap://%s' % server, credentials=creds,
+ session_info=system_session(LP), lp=LP)
+ if errno is not None:
+ self.fail("logon failed to fail with ldb error %s" % errno)
+ except ldb.LdbError as e10:
+ (code, msg) = e10.args
+ if code != errno:
+ if errno is None:
+ self.fail("logon incorrectly raised ldb error (code=%s)" %
+ code)
+ else:
+ self.fail("logon failed to raise correct ldb error"
+ "Expected: %s Got: %s" %
+ (errno, code))
+
+ def zero_min_password_age(self):
+ min_pwd_age = int(self.rwdc_db.get_minPwdAge())
+ if min_pwd_age != 0:
+ self.rwdc_db.set_minPwdAge('0')
+
+ def _test_ldap_change_password(self, errno=None, simple=False):
+ self.zero_min_password_age()
+
+ dn, username, password = self._new_user()
+
+ simple_dn = dn if simple else None
+
+ creds1 = make_creds(username, password, simple_dn=simple_dn)
+
+ # With NTLM, this should fail on RODC before replication,
+ # because the user isn't known.
+ self.try_ldap_logon(RODC, creds1, ldb.ERR_INVALID_CREDENTIALS,
+ simple=simple)
+ self.force_replication()
+
+ # Now the user is replicated to RODC, so logon should work
+ self.try_ldap_logon(RODC, creds1, simple=simple)
+
+ passwords = ['password#%s' % i for i in range(1, 6)]
+ for prev, password in zip(passwords[:-1], passwords[1:]):
+ self._change_password(dn, prev, password)
+
+ # The password has changed enough times to make the old
+ # password invalid (though with kerberos that doesn't matter).
+ # For NTLM, the old creds should always fail
+ self.try_ldap_logon(RODC, creds1, errno, simple=simple)
+ self.try_ldap_logon(RWDC, creds1, errno, simple=simple)
+
+ creds2 = make_creds(username, password, simple_dn=simple_dn)
+
+ # new creds work straight away with NTLM, because although it
+ # doesn't have the password, it knows the user and forwards
+ # the query.
+ self.try_ldap_logon(RODC, creds2, simple=simple)
+ self.try_ldap_logon(RWDC, creds2, simple=simple)
+
+ self.force_replication()
+
+ # After another replication check RODC still works and fails,
+ # as appropriate to various creds
+ self.try_ldap_logon(RODC, creds2, simple=simple)
+ self.try_ldap_logon(RODC, creds1, errno, simple=simple)
+
+ prev = password
+ password = 'password#6'
+ self._change_password(dn, prev, password)
+ creds3 = make_creds(username, password, simple_dn=simple_dn)
+
+ # previous password should still work.
+ self.try_ldap_logon(RWDC, creds2, simple=simple)
+ self.try_ldap_logon(RODC, creds2, simple=simple)
+
+ # new password should still work.
+ self.try_ldap_logon(RWDC, creds3, simple=simple)
+ self.try_ldap_logon(RODC, creds3, simple=simple)
+
+ # old password should still fail (but not on kerberos).
+ self.try_ldap_logon(RWDC, creds1, errno, simple=simple)
+ self.try_ldap_logon(RODC, creds1, errno, simple=simple)
+
+ def test_ldap_change_password_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_ldap_change_password()
+
+ def test_ldap_change_password_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_ldap_change_password(ldb.ERR_INVALID_CREDENTIALS)
+
+ def test_ldap_change_password_simple_bind(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_ldap_change_password(ldb.ERR_INVALID_CREDENTIALS, simple=True)
+
+ def _test_ldap_change_password_reveal_on_demand(self, errno=None):
+ self.zero_min_password_age()
+
+ res = self.rodc_db.search(self.rodc_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['msDS-RevealOnDemandGroup'])
+
+ group = res[0]['msDS-RevealOnDemandGroup'][0].decode('utf8')
+
+ user_dn, username, password = self._new_user()
+ creds1 = make_creds(username, password)
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, group)
+ m['member'] = ldb.MessageElement(user_dn, ldb.FLAG_MOD_ADD, 'member')
+ self.rwdc_db.modify(m)
+
+ # Against Windows, this will just forward if no account exists on the KDC
+ # Therefore, this does not error on Windows.
+ self.try_ldap_logon(RODC, creds1, ldb.ERR_INVALID_CREDENTIALS)
+
+ self.force_replication()
+
+ # The proxy case
+ self.try_ldap_logon(RODC, creds1)
+ preload_rodc_user(user_dn)
+
+ # Now the user AND password are replicated to RODC, so logon should work (not proxy case)
+ self.try_ldap_logon(RODC, creds1)
+
+ passwords = ['password#%s' % i for i in range(1, 6)]
+ for prev, password in zip(passwords[:-1], passwords[1:]):
+ self._change_password(user_dn, prev, password)
+
+ # The password has changed enough times to make the old
+ # password invalid, but the RODC shouldn't know that.
+ self.try_ldap_logon(RODC, creds1)
+ self.try_ldap_logon(RWDC, creds1, errno)
+
+ creds2 = make_creds(username, password)
+ self.try_ldap_logon(RWDC, creds2)
+ # The RODC forward WRONG_PASSWORD to the RWDC
+ self.try_ldap_logon(RODC, creds2)
+
+ def test_change_password_reveal_on_demand_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_ldap_change_password_reveal_on_demand(ldb.ERR_INVALID_CREDENTIALS)
+
+ def test_change_password_reveal_on_demand_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_ldap_change_password_reveal_on_demand()
+
+ def test_login_lockout_krb5(self):
+ username = self.lockout1krb5_creds.get_username()
+ userpass = self.lockout1krb5_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ use_kerberos = self.lockout1krb5_creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+
+ try:
+ ldb = SamDB(url=self.host_url, credentials=fail_creds, lp=self.lp)
+ self.fail()
+ except LdbError as e11:
+ (num, msg) = e11.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ # Succeed to reset everything to 0
+ success_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass,
+ kerberos_state=use_kerberos)
+
+ ldb = SamDB(url=self.host_url, credentials=success_creds, lp=self.lp)
+
+ self._test_login_lockout(self.lockout1krb5_creds)
+
+ def test_login_lockout_ntlm(self):
+ username = self.lockout1ntlm_creds.get_username()
+ userpass = self.lockout1ntlm_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ use_kerberos = self.lockout1ntlm_creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+
+ try:
+ ldb = SamDB(url=self.host_url, credentials=fail_creds, lp=self.lp)
+ self.fail()
+ except LdbError as e12:
+ (num, msg) = e12.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ # Succeed to reset everything to 0
+ ldb = SamDB(url=self.host_url, credentials=self.lockout1ntlm_creds, lp=self.lp)
+
+ self._test_login_lockout(self.lockout1ntlm_creds)
+
+ def test_multiple_logon_krb5(self):
+ username = self.lockout1krb5_creds.get_username()
+ userpass = self.lockout1krb5_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ use_kerberos = self.lockout1krb5_creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+
+ try:
+ ldb = SamDB(url=self.host_url, credentials=fail_creds, lp=self.lp)
+ self.fail()
+ except LdbError as e13:
+ (num, msg) = e13.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ # Succeed to reset everything to 0
+ success_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass,
+ kerberos_state=use_kerberos)
+
+ ldb = SamDB(url=self.host_url, credentials=success_creds, lp=self.lp)
+
+ self._test_multiple_logon(self.lockout1krb5_creds)
+
+ def test_multiple_logon_ntlm(self):
+ username = self.lockout1ntlm_creds.get_username()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+ userpass = self.lockout1ntlm_creds.get_password()
+
+ preload_rodc_user(userdn)
+
+ use_kerberos = self.lockout1ntlm_creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+
+ try:
+ ldb = SamDB(url=self.host_url, credentials=fail_creds, lp=self.lp)
+ self.fail()
+ except LdbError as e14:
+ (num, msg) = e14.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ # Succeed to reset everything to 0
+ ldb = SamDB(url=self.host_url, credentials=self.lockout1ntlm_creds, lp=self.lp)
+
+ self._test_multiple_logon(self.lockout1ntlm_creds)
+
+
+def main():
+ global RODC, RWDC, CREDS, LP
+ parser = optparse.OptionParser(
+ "rodc_rwdc.py [options] <rodc host> <rwdc host>")
+
+ sambaopts = options.SambaOptions(parser)
+ versionopts = options.VersionOptions(parser)
+ credopts = options.CredentialsOptions(parser)
+ subunitopts = SubunitOptions(parser)
+
+ parser.add_option_group(sambaopts)
+ parser.add_option_group(versionopts)
+ parser.add_option_group(credopts)
+ parser.add_option_group(subunitopts)
+
+ opts, args = parser.parse_args()
+
+ LP = sambaopts.get_loadparm()
+ CREDS = credopts.get_credentials(LP)
+ CREDS.set_gensec_features(CREDS.get_gensec_features() |
+ gensec.FEATURE_SEAL)
+
+ try:
+ RODC, RWDC = args
+ except ValueError:
+ parser.print_usage()
+ sys.exit(1)
+
+ set_auto_replication(RWDC, True)
+ try:
+ TestProgram(module=__name__, opts=subunitopts)
+ finally:
+ set_auto_replication(RWDC, True)
+
+
+main()
diff --git a/source4/dsdb/tests/python/sam.py b/source4/dsdb/tests/python/sam.py
new file mode 100755
index 0000000..abd6bb7
--- /dev/null
+++ b/source4/dsdb/tests/python/sam.py
@@ -0,0 +1,3877 @@
+#!/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.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, "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": "cn=ldaptest\,specialuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+ delete_force(self.ldb, "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)
+
+ HRES_SEC_E_LOGON_DENIED = 0x8009030C
+ HRES_SEC_E_INVALID_TOKEN = 0x80090308
+
+ 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)
+ sasl_ldb = None
+
+ requires_strong_auth = False
+ try:
+ simple_ldb = SamDB(url=host, credentials=simple_creds, lp=lp)
+ self.assertIsNotNone(simple_ldb)
+ simple_ldb = None
+ 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:
+ ldb_fail = 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:
+ ldb_fail = 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:
+ ldb_fail = 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:
+ ldb_fail = 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:
+ ldb_fail = 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:
+ ldb_fail = 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)
+ lastset1 = int(res[0]["pwdLastSet"][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, 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)
diff --git a/source4/dsdb/tests/python/sec_descriptor.py b/source4/dsdb/tests/python/sec_descriptor.py
new file mode 100755
index 0000000..884cf3e
--- /dev/null
+++ b/source4/dsdb/tests/python/sec_descriptor.py
@@ -0,0 +1,2158 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+import os
+import base64
+import re
+import random
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+# Some error messages that are being tested
+from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError, ERR_NO_SUCH_OBJECT
+
+# For running the test unit
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import security
+
+from samba import gensec, sd_utils
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+from samba.auth import system_session
+from samba.dsdb import DS_DOMAIN_FUNCTION_2008
+from samba.dcerpc.security import (
+ SECINFO_OWNER, SECINFO_GROUP, SECINFO_DACL, SECINFO_SACL)
+import samba.tests
+from samba.tests import delete_force
+
+parser = optparse.OptionParser("sec_descriptor.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)
+
+#
+# Tests start here
+#
+
+
+class DescriptorTests(samba.tests.TestCase):
+
+ def get_users_domain_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def create_schema_class(self, _ldb, desc=None):
+ while True:
+ class_id = random.randint(0, 65535)
+ class_name = "descriptor-test-class%s" % class_id
+ class_dn = "CN=%s,%s" % (class_name, self.schema_dn)
+ try:
+ self.ldb_admin.search(base=class_dn, attrs=["name"])
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ break
+
+ ldif = """
+dn: """ + class_dn + """
+objectClass: classSchema
+objectCategory: CN=Class-Schema,""" + self.schema_dn + """
+defaultObjectCategory: """ + class_dn + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.3.""" + str(class_id) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemFlags: 16
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ if desc:
+ assert(isinstance(desc, str) or isinstance(desc, security.descriptor))
+ if isinstance(desc, str):
+ ldif += "nTSecurityDescriptor: %s" % desc
+ elif isinstance(desc, security.descriptor):
+ ldif += "nTSecurityDescriptor:: %s" % base64.b64encode(ndr_pack(desc)).decode('utf8')
+ _ldb.add_ldif(ldif)
+ return class_dn
+
+ def create_configuration_container(self, _ldb, object_dn, desc=None):
+ ldif = """
+dn: """ + object_dn + """
+objectClass: container
+objectCategory: CN=Container,""" + self.schema_dn + """
+showInAdvancedViewOnly: TRUE
+instanceType: 4
+"""
+ if desc:
+ assert(isinstance(desc, str) or isinstance(desc, security.descriptor))
+ if isinstance(desc, str):
+ ldif += "nTSecurityDescriptor: %s" % desc
+ elif isinstance(desc, security.descriptor):
+ ldif += "nTSecurityDescriptor:: %s" % base64.b64encode(ndr_pack(desc)).decode('utf8')
+ _ldb.add_ldif(ldif)
+
+ def create_configuration_specifier(self, _ldb, object_dn, desc=None):
+ ldif = """
+dn: """ + object_dn + """
+objectClass: displaySpecifier
+showInAdvancedViewOnly: TRUE
+"""
+ if desc:
+ assert(isinstance(desc, str) or isinstance(desc, security.descriptor))
+ if isinstance(desc, str):
+ ldif += "nTSecurityDescriptor: %s" % desc
+ elif isinstance(desc, security.descriptor):
+ ldif += "nTSecurityDescriptor:: %s" % base64.b64encode(ndr_pack(desc)).decode('utf8')
+ _ldb.add_ldif(ldif)
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ ldb_target = SamDB(url=host, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ def setUp(self):
+ super(DescriptorTests, self).setUp()
+ self.ldb_admin = SamDB(host, credentials=creds, session_info=system_session(lp), lp=lp,
+ options=ldb_options)
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.configuration_dn = self.ldb_admin.get_config_basedn().get_linearized()
+ self.schema_dn = self.ldb_admin.get_schema_basedn().get_linearized()
+ self.domain_sid = security.dom_sid(self.ldb_admin.get_domain_sid())
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+ self.addCleanup(self.delete_admin_connection)
+ print("baseDN: %s" % self.base_dn)
+
+ def delete_admin_connection(self):
+ del self.sd_utils
+ del self.ldb_admin
+
+ ################################################################################################
+
+ # Tests for DOMAIN
+
+ # Default descriptor tests #####################################################################
+
+
+class OwnerGroupDescriptorTests(DescriptorTests):
+
+ def deleteAll(self):
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser1"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser2"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser3"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser4"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser5"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser6"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser7"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser8"))
+ # DOMAIN
+ delete_force(self.ldb_admin, self.get_users_domain_dn("test_domain_group1"))
+ delete_force(self.ldb_admin, "CN=test_domain_user1,OU=test_domain_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_domain_ou2,OU=test_domain_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_domain_ou1," + self.base_dn)
+ # SCHEMA
+ # CONFIGURATION
+ delete_force(self.ldb_admin, "CN=test-specifier1,CN=test-container1,CN=DisplaySpecifiers,"
+ + self.configuration_dn)
+ delete_force(self.ldb_admin, "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn)
+
+ def setUp(self):
+ super(OwnerGroupDescriptorTests, self).setUp()
+ self.deleteAll()
+ # Create users
+ # User 1 - Enterprise Admins
+ self.ldb_admin.newuser("testuser1", "samba123@")
+ # User 2 - Domain Admins
+ self.ldb_admin.newuser("testuser2", "samba123@")
+ # User 3 - Schema Admins
+ self.ldb_admin.newuser("testuser3", "samba123@")
+ # User 4 - regular user
+ self.ldb_admin.newuser("testuser4", "samba123@")
+ # User 5 - Enterprise Admins and Domain Admins
+ self.ldb_admin.newuser("testuser5", "samba123@")
+ # User 6 - Enterprise Admins, Domain Admins, Schema Admins
+ self.ldb_admin.newuser("testuser6", "samba123@")
+ # User 7 - Domain Admins and Schema Admins
+ self.ldb_admin.newuser("testuser7", "samba123@")
+ # User 5 - Enterprise Admins and Schema Admins
+ self.ldb_admin.newuser("testuser8", "samba123@")
+
+ self.ldb_admin.add_remove_group_members("Enterprise Admins",
+ ["testuser1", "testuser5", "testuser6", "testuser8"],
+ add_members_operation=True)
+ self.ldb_admin.add_remove_group_members("Domain Admins",
+ ["testuser2", "testuser5", "testuser6", "testuser7"],
+ add_members_operation=True)
+ self.ldb_admin.add_remove_group_members("Schema Admins",
+ ["testuser3", "testuser6", "testuser7", "testuser8"],
+ add_members_operation=True)
+
+ self.results = {
+ # msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
+ "ds_behavior_win2003": {
+ "100": "O:EAG:DU",
+ "101": "O:DAG:DU",
+ "102": "O:%sG:DU",
+ "103": "O:%sG:DU",
+ "104": "O:DAG:DU",
+ "105": "O:DAG:DU",
+ "106": "O:DAG:DU",
+ "107": "O:EAG:DU",
+ "108": "O:DAG:DA",
+ "109": "O:DAG:DA",
+ "110": "O:%sG:DA",
+ "111": "O:%sG:DA",
+ "112": "O:DAG:DA",
+ "113": "O:DAG:DA",
+ "114": "O:DAG:DA",
+ "115": "O:DAG:DA",
+ "130": "O:EAG:DU",
+ "131": "O:DAG:DU",
+ "132": "O:SAG:DU",
+ "133": "O:%sG:DU",
+ "134": "O:EAG:DU",
+ "135": "O:SAG:DU",
+ "136": "O:SAG:DU",
+ "137": "O:SAG:DU",
+ "138": "O:DAG:DA",
+ "139": "O:DAG:DA",
+ "140": "O:%sG:DA",
+ "141": "O:%sG:DA",
+ "142": "O:DAG:DA",
+ "143": "O:DAG:DA",
+ "144": "O:DAG:DA",
+ "145": "O:DAG:DA",
+ "160": "O:EAG:DU",
+ "161": "O:DAG:DU",
+ "162": "O:%sG:DU",
+ "163": "O:%sG:DU",
+ "164": "O:EAG:DU",
+ "165": "O:EAG:DU",
+ "166": "O:DAG:DU",
+ "167": "O:EAG:DU",
+ "168": "O:DAG:DA",
+ "169": "O:DAG:DA",
+ "170": "O:%sG:DA",
+ "171": "O:%sG:DA",
+ "172": "O:DAG:DA",
+ "173": "O:DAG:DA",
+ "174": "O:DAG:DA",
+ "175": "O:DAG:DA",
+ },
+ # msDS-Behavior-Version >= DS_DOMAIN_FUNCTION_2008
+ "ds_behavior_win2008": {
+ "100": "O:EAG:EA",
+ "101": "O:DAG:DA",
+ "102": "O:%sG:DU",
+ "103": "O:%sG:DU",
+ "104": "O:DAG:DA",
+ "105": "O:DAG:DA",
+ "106": "O:DAG:DA",
+ "107": "O:EAG:EA",
+ "108": "O:DAG:DA",
+ "109": "O:DAG:DA",
+ "110": "O:%sG:DA",
+ "111": "O:%sG:DA",
+ "112": "O:DAG:DA",
+ "113": "O:DAG:DA",
+ "114": "O:DAG:DA",
+ "115": "O:DAG:DA",
+ "130": "O:EAG:EA",
+ "131": "O:DAG:DA",
+ "132": "O:SAG:SA",
+ "133": "O:%sG:DU",
+ "134": "O:EAG:EA",
+ "135": "O:SAG:SA",
+ "136": "O:SAG:SA",
+ "137": "O:SAG:SA",
+ "138": "",
+ "139": "",
+ "140": "O:%sG:DA",
+ "141": "O:%sG:DA",
+ "142": "",
+ "143": "",
+ "144": "",
+ "145": "",
+ "160": "O:EAG:EA",
+ "161": "O:DAG:DA",
+ "162": "O:%sG:DU",
+ "163": "O:%sG:DU",
+ "164": "O:EAG:EA",
+ "165": "O:EAG:EA",
+ "166": "O:DAG:DA",
+ "167": "O:EAG:EA",
+ "168": "O:DAG:DA",
+ "169": "O:DAG:DA",
+ "170": "O:%sG:DA",
+ "171": "O:%sG:DA",
+ "172": "O:DAG:DA",
+ "173": "O:DAG:DA",
+ "174": "O:DAG:DA",
+ "175": "O:DAG:DA",
+ },
+ }
+ # Discover 'domainControllerFunctionality'
+ res = self.ldb_admin.search(base="", scope=SCOPE_BASE,
+ attrs=['domainControllerFunctionality'])
+ res = int(res[0]['domainControllerFunctionality'][0])
+ if res < DS_DOMAIN_FUNCTION_2008:
+ self.DS_BEHAVIOR = "ds_behavior_win2003"
+ else:
+ self.DS_BEHAVIOR = "ds_behavior_win2008"
+
+ def tearDown(self):
+ super(OwnerGroupDescriptorTests, self).tearDown()
+ self.deleteAll()
+
+ def check_user_belongs(self, user_dn, groups=[]):
+ """ Test whether user is member of the expected group(s) """
+ if groups != []:
+ # User is member of at least one additional group
+ res = self.ldb_admin.search(user_dn, attrs=["memberOf"])
+ res = [str(x).upper() for x in sorted(list(res[0]["memberOf"]))]
+ expected = []
+ for x in groups:
+ expected.append(self.get_users_domain_dn(x))
+ expected = [x.upper() for x in sorted(expected)]
+ self.assertEqual(expected, res)
+ else:
+ # User is not a member of any additional groups but default
+ res = self.ldb_admin.search(user_dn, attrs=["*"])
+ res = [x.upper() for x in res[0].keys()]
+ self.assertFalse("MEMBEROF" in res)
+
+ def check_modify_inheritance(self, _ldb, object_dn, owner_group=""):
+ # Modify
+ sd_user_utils = sd_utils.SDUtils(_ldb)
+ ace = "(D;;CC;;;LG)" # Deny Create Children to Guest account
+ if owner_group != "":
+ sd_user_utils.modify_sd_on_dn(object_dn, owner_group + "D:" + ace)
+ else:
+ sd_user_utils.modify_sd_on_dn(object_dn, "D:" + ace)
+ # Make sure the modify operation has been applied
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ self.assertTrue(ace in desc_sddl)
+ # Make sure we have identical result for both "add" and "modify"
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ print(self._testMethodName)
+ test_number = self._testMethodName[5:]
+ self.assertEqual(self.results[self.DS_BEHAVIOR][test_number], res)
+
+ def test_100(self):
+ """ Enterprise admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_101(self):
+ """ Domain admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_102(self):
+ """ Schema admin group member with CC right creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WPWDCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create additional object into the first one
+ object_dn = "CN=test_domain_user1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newuser("test_domain_user1", "samba123@",
+ userou="OU=test_domain_ou1", setpassword=False)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ # This fails, research why
+ #self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_103(self):
+ """ Regular user with CC right creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WPWDCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create additional object into the first one
+ object_dn = "CN=test_domain_user1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newuser("test_domain_user1", "samba123@",
+ userou="OU=test_domain_ou1", setpassword=False)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ # this fails, research why
+ #self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_104(self):
+ """ Enterprise & Domain admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_105(self):
+ """ Enterprise & Domain & Schema admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_106(self):
+ """ Domain & Schema admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_107(self):
+ """ Enterprise & Schema admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ # Control descriptor tests #####################################################################
+
+ def test_108(self):
+ """ Enterprise admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_109(self):
+ """ Domain admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_110(self):
+ """ Schema admin group member with CC right creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WOWDCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ # Create additional object into the first one
+ object_dn = "CN=test_domain_user1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newuser("test_domain_user1", "samba123@",
+ userou="OU=test_domain_ou1", sd=tmp_desc, setpassword=False)
+ desc = self.sd_utils.read_sd_on_dn(object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_111(self):
+ """ Regular user with CC right creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WOWDCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ # Create additional object into the first one
+ object_dn = "CN=test_domain_user1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newuser("test_domain_user1", "samba123@",
+ userou="OU=test_domain_ou1", sd=tmp_desc, setpassword=False)
+ desc = self.sd_utils.read_sd_on_dn(object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_112(self):
+ """ Domain & Enterprise admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_113(self):
+ """ Domain & Enterprise & Schema admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_114(self):
+ """ Domain & Schema admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_115(self):
+ """ Enterprise & Schema admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_999(self):
+ user_name = "Administrator"
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(D;CI;WP;;;S-1-3-0)"
+ #mod = ""
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ # Create additional object into the first one
+ object_dn = "OU=test_domain_ou2," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+
+ # Tests for SCHEMA
+
+ # Default descriptor tests ##################################################################
+
+ def test_130(self):
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_131(self):
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_132(self):
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ #self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_133(self):
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ #self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_134(self):
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_135(self):
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_136(self):
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_137(self):
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ # Custom descriptor tests ##################################################################
+
+ def test_138(self):
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_139(self):
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_140(self):
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ desc_sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_141(self):
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ desc_sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_142(self):
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_143(self):
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_144(self):
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_145(self):
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ # Tests for CONFIGURATION
+
+ # Default descriptor tests ##################################################################
+
+ def test_160(self):
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_161(self):
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_162(self):
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ object_dn = "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(self.ldb_admin, object_dn, )
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create child object with user's credentials
+ object_dn = "CN=test-specifier1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_specifier(_ldb, object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ #self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_163(self):
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ object_dn = "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(self.ldb_admin, object_dn, )
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create child object with user's credentials
+ object_dn = "CN=test-specifier1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_specifier(_ldb, object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ #self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_164(self):
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_165(self):
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_166(self):
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_167(self):
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ # Custom descriptor tests ##################################################################
+
+ def test_168(self):
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_169(self):
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_170(self):
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ object_dn = "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(self.ldb_admin, object_dn, )
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create child object with user's credentials
+ object_dn = "CN=test-specifier1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ desc_sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ self.create_configuration_specifier(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_171(self):
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ object_dn = "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(self.ldb_admin, object_dn, )
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create child object with user's credentials
+ object_dn = "CN=test-specifier1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ desc_sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ self.create_configuration_specifier(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_172(self):
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_173(self):
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_174(self):
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_175(self):
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ ########################################################################################
+ # Inheritance tests for DACL
+
+
+class DaclDescriptorTests(DescriptorTests):
+
+ def deleteAll(self):
+ delete_force(self.ldb_admin, "CN=test_inherit_group,OU=test_inherit_ou," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou5,OU=test_inherit_ou1,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou6,OU=test_inherit_ou2,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou1,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou2,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou3,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou4,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou," + self.base_dn)
+
+ def setUp(self):
+ super(DaclDescriptorTests, self).setUp()
+ self.deleteAll()
+
+ def create_clean_ou(self, object_dn):
+ """ Base repeating setup for unittests to follow """
+ res = self.ldb_admin.search(base=self.base_dn, scope=SCOPE_SUBTREE,
+ expression="distinguishedName=%s" % object_dn)
+ # Make sure top testing OU has been deleted before starting the test
+ self.assertEqual(len(res), 0)
+ self.ldb_admin.create_ou(object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ # Make sure there are inheritable ACEs initially
+ self.assertTrue("CI" in desc_sddl or "OI" in desc_sddl)
+ # Find and remove all inherit ACEs
+ res = re.findall(r"\(.*?\)", desc_sddl)
+ res = [x for x in res if ("CI" in x) or ("OI" in x)]
+ for x in res:
+ desc_sddl = desc_sddl.replace(x, "")
+ # Add flag 'protected' in both DACL and SACL so no inherit ACEs
+ # can propagate from above
+ # remove SACL, we are not interested
+ desc_sddl = desc_sddl.replace(":AI", ":AIP")
+ self.sd_utils.modify_sd_on_dn(object_dn, desc_sddl)
+ # Verify all inheritable ACEs are gone
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ self.assertFalse("CI" in desc_sddl)
+ self.assertFalse("OI" in desc_sddl)
+
+ def test_200(self):
+ """ OU with protected flag and child group. See if the group has inherit ACEs.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Create group child object
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4)
+ # Make sure created group object contains NO inherit ACEs
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertFalse("ID" in desc_sddl)
+
+ def test_201(self):
+ """ OU with protected flag and no inherit ACEs, child group with custom descriptor.
+ Verify group has custom and default ACEs only.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Create group child object using custom security descriptor
+ sddl = "O:AUG:AUD:AI(D;;WP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group descriptor has NO additional ACEs
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertEqual(desc_sddl, sddl)
+ sddl = "O:AUG:AUD:AI(D;;CC;;;LG)"
+ self.sd_utils.modify_sd_on_dn(group_dn, sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertEqual(desc_sddl, sddl)
+
+ def test_202(self):
+ """ OU with protected flag and add couple non-inheritable ACEs, child group.
+ See if the group has any of the added ACEs.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom non-inheritable ACEs
+ mod = "(D;;WP;;;DU)(A;;RP;;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ # Verify all inheritable ACEs are gone
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4)
+ # Make sure created group object contains NO inherit ACEs
+ # also make sure the added above non-inheritable ACEs are absent too
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertFalse("ID" in desc_sddl)
+ for x in re.findall(r"\(.*?\)", mod):
+ self.assertFalse(x in desc_sddl)
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertFalse("ID" in desc_sddl)
+ for x in re.findall(r"\(.*?\)", mod):
+ self.assertFalse(x in desc_sddl)
+
+ def test_203(self):
+ """ OU with protected flag and add 'CI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "(D;CI;WP;;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";CI;", ";CIID;")
+ self.assertTrue(mod in desc_sddl)
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue(moded in desc_sddl)
+ self.assertTrue(mod in desc_sddl)
+
+ def test_204(self):
+ """ OU with protected flag and add 'OI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "(D;OI;WP;;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";OI;", ";OIIOID;") # change it how it's gonna look like
+ self.assertTrue(mod in desc_sddl)
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue(moded in desc_sddl)
+ self.assertTrue(mod in desc_sddl)
+
+ def test_205(self):
+ """ OU with protected flag and add 'OA' for GUID & 'CI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'name' attribute & 'CI' ACE
+ mod = "(OA;CI;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";CI;", ";CIID;") # change it how it's gonna look like
+ self.assertTrue(mod in desc_sddl)
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue(moded in desc_sddl)
+ self.assertTrue(mod in desc_sddl)
+
+ def test_206(self):
+ """ OU with protected flag and add 'OA' for GUID & 'OI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'name' attribute & 'OI' ACE
+ mod = "(OA;OI;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";OI;", ";OIIOID;") # change it how it's gonna look like
+ self.assertTrue(mod in desc_sddl)
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue(moded in desc_sddl)
+ self.assertTrue(mod in desc_sddl)
+
+ def test_207(self):
+ """ OU with protected flag and add 'OA' for OU specific GUID & 'CI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'st' attribute (OU specific) & 'CI' ACE
+ mod = "(OA;CI;WP;bf967a39-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";CI;", ";CIID;") # change it how it's gonna look like
+ self.assertTrue(mod in desc_sddl)
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue(moded in desc_sddl)
+ self.assertTrue(mod in desc_sddl)
+
+ def test_208(self):
+ """ OU with protected flag and add 'OA' for OU specific GUID & 'OI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'st' attribute (OU specific) & 'OI' ACE
+ mod = "(OA;OI;WP;bf967a39-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";OI;", ";OIIOID;") # change it how it's gonna look like
+ self.assertTrue(mod in desc_sddl)
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:(OA;OI;WP;bf967a39-0de6-11d0-a285-00aa003049e2;;DU)" + moded)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue(moded in desc_sddl)
+ self.assertTrue(mod in desc_sddl)
+
+ def test_209(self):
+ """ OU with protected flag and add 'CI' ACE with 'CO' SID, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "(D;CI;WP;;;CO)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE(s)
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue("(D;ID;WP;;;AU)" in desc_sddl)
+ self.assertTrue("(D;CIIOID;WP;;;CO)" in desc_sddl)
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue(moded in desc_sddl)
+ self.assertTrue("(D;ID;WP;;;DA)" in desc_sddl)
+ self.assertTrue("(D;CIIOID;WP;;;CO)" in desc_sddl)
+
+ def test_210(self):
+ """ OU with protected flag, provide ACEs with ID flag raised. Should be ignored.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ self.create_clean_ou(ou_dn)
+ # Add some custom ACE
+ mod = "D:(D;CIIO;WP;;;CO)(A;ID;WP;;;AU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object does not contain the ID ace
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertFalse("(A;ID;WP;;;AU)" in desc_sddl)
+
+ def test_211(self):
+ """ Provide ACE with CO SID, should be expanded and replaced
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "D:(D;CI;WP;;;CO)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue("(D;;WP;;;DA)" in desc_sddl)
+ self.assertTrue("(D;CIIO;WP;;;CO)" in desc_sddl)
+
+ def test_212(self):
+ """ Provide ACE with IO flag, should be ignored
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "D:(D;CIIO;WP;;;CO)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE(s)
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertTrue("(D;CIIO;WP;;;CO)" in desc_sddl)
+ self.assertFalse("(D;;WP;;;DA)" in desc_sddl)
+ self.assertFalse("(D;CIIO;WP;;;CO)(D;CIIO;WP;;;CO)" in desc_sddl)
+
+ def test_213(self):
+ """ Provide ACE with IO flag, should be ignored
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "D:(D;IO;WP;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE(s)
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertFalse("(D;IO;WP;;;DA)" in desc_sddl)
+
+ def test_214(self):
+ """ Test behavior of ACEs containing generic rights
+ """
+ ou_dn = "OU=test_inherit_ou_p," + self.base_dn
+ ou_dn1 = "OU=test_inherit_ou1," + ou_dn
+ ou_dn2 = "OU=test_inherit_ou2," + ou_dn
+ ou_dn3 = "OU=test_inherit_ou3," + ou_dn
+ ou_dn4 = "OU=test_inherit_ou4," + ou_dn
+ ou_dn5 = "OU=test_inherit_ou5," + ou_dn1
+ ou_dn6 = "OU=test_inherit_ou6," + ou_dn2
+ # Create inheritable-free OU
+ mod = "D:P(A;CI;WPRPLCCCDCWDRC;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, sd=tmp_desc)
+ mod = "D:(A;CI;GA;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn1, sd=tmp_desc)
+ mod = "D:(A;CIIO;GA;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn2, sd=tmp_desc)
+ mod = "D:(A;;GA;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn3, sd=tmp_desc)
+ mod = "D:(A;IO;GA;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn4, sd=tmp_desc)
+
+ self.ldb_admin.create_ou(ou_dn5)
+ self.ldb_admin.create_ou(ou_dn6)
+
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn1)
+ self.assertTrue("(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DU)" in desc_sddl)
+ self.assertTrue("(A;CIIO;GA;;;DU)" in desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn2)
+ self.assertFalse("(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DU)" in desc_sddl)
+ self.assertTrue("(A;CIIO;GA;;;DU)" in desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn3)
+ self.assertTrue("(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DU)" in desc_sddl)
+ self.assertFalse("(A;CIIO;GA;;;DU)" in desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn4)
+ self.assertFalse("(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DU)" in desc_sddl)
+ self.assertFalse("(A;CIIO;GA;;;DU)" in desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn5)
+ self.assertTrue("(A;ID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DU)" in desc_sddl)
+ self.assertTrue("(A;CIIOID;GA;;;DU)" in desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn6)
+ self.assertTrue("(A;ID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DU)" in desc_sddl)
+ self.assertTrue("(A;CIIOID;GA;;;DU)" in desc_sddl)
+
+ def test_215(self):
+ """ Make sure IO flag is removed in child objects
+ """
+ ou_dn = "OU=test_inherit_ou_p," + self.base_dn
+ ou_dn1 = "OU=test_inherit_ou1," + ou_dn
+ ou_dn5 = "OU=test_inherit_ou5," + ou_dn1
+ # Create inheritable-free OU
+ mod = "D:P(A;CI;WPRPLCCCDCWDRC;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, sd=tmp_desc)
+ mod = "D:(A;CIIO;WP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn1, sd=tmp_desc)
+ self.ldb_admin.create_ou(ou_dn5)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn5)
+ self.assertTrue("(A;CIID;WP;;;DU)" in desc_sddl)
+ self.assertFalse("(A;CIIOID;WP;;;DU)" in desc_sddl)
+
+ def test_216(self):
+ """ Make sure ID ACES provided by user are ignored
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ mod = "D:P(A;;WPRPLCCCDCWDRC;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, sd=tmp_desc)
+ # Add some custom ACE
+ mod = "D:(D;ID;WP;;;AU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object does not contain the ID ace
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertFalse("(A;ID;WP;;;AU)" in desc_sddl)
+ self.assertFalse("(A;;WP;;;AU)" in desc_sddl)
+
+ def test_217(self):
+ """ Make sure ID ACES provided by user are not ignored if P flag is set
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ mod = "D:P(A;;WPRPLCCCDCWDRC;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, sd=tmp_desc)
+ # Add some custom ACE
+ mod = "D:P(A;ID;WP;;;AU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object does not contain the ID ace
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertFalse("(A;ID;WP;;;AU)" in desc_sddl)
+ self.assertTrue("(A;;WP;;;AU)" in desc_sddl)
+
+ ########################################################################################
+
+
+class SdFlagsDescriptorTests(DescriptorTests):
+ def deleteAll(self):
+ delete_force(self.ldb_admin, "OU=test_sdflags_ou," + self.base_dn)
+
+ def setUp(self):
+ super(SdFlagsDescriptorTests, self).setUp()
+ self.test_descr = "O:AUG:AUD:(D;;CC;;;LG)S:(OU;;WP;;;AU)"
+ self.deleteAll()
+
+ def test_301(self):
+ """ Modify a descriptor with OWNER_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:%d" % (SECINFO_OWNER)])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the owner
+ self.assertTrue("O:AU" in desc_sddl)
+ # make sure nothing else has been modified
+ self.assertFalse("G:AU" in desc_sddl)
+ self.assertFalse("D:(D;;CC;;;LG)" in desc_sddl)
+ self.assertFalse("(OU;;WP;;;AU)" in desc_sddl)
+
+ def test_302(self):
+ """ Modify a descriptor with GROUP_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:%d" % (SECINFO_GROUP)])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the group
+ self.assertTrue("G:AU" in desc_sddl)
+ # make sure nothing else has been modified
+ self.assertFalse("O:AU" in desc_sddl)
+ self.assertFalse("D:(D;;CC;;;LG)" in desc_sddl)
+ self.assertFalse("(OU;;WP;;;AU)" in desc_sddl)
+
+ def test_303(self):
+ """ Modify a descriptor with SACL_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:%d" % (SECINFO_DACL)])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the DACL
+ self.assertTrue("(D;;CC;;;LG)" in desc_sddl)
+ # make sure nothing else has been modified
+ self.assertFalse("O:AU" in desc_sddl)
+ self.assertFalse("G:AU" in desc_sddl)
+ self.assertFalse("(OU;;WP;;;AU)" in desc_sddl)
+
+ def test_304(self):
+ """ Modify a descriptor with SACL_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:%d" % (SECINFO_SACL)])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the DACL
+ self.assertTrue("(OU;;WP;;;AU)" in desc_sddl)
+ # make sure nothing else has been modified
+ self.assertFalse("O:AU" in desc_sddl)
+ self.assertFalse("G:AU" in desc_sddl)
+ self.assertFalse("(D;;CC;;;LG)" in desc_sddl)
+
+ def test_305(self):
+ """ Modify a descriptor with 0x0 set.
+ Contrary to logic this is interpreted as no control,
+ which is the same as 0xF
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:0"])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the DACL
+ self.assertTrue("(OU;;WP;;;AU)" in desc_sddl)
+ # make sure nothing else has been modified
+ self.assertTrue("O:AU" in desc_sddl)
+ self.assertTrue("G:AU" in desc_sddl)
+ self.assertTrue("(D;;CC;;;LG)" in desc_sddl)
+
+ def test_306(self):
+ """ Modify a descriptor with 0xF set.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:15"])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the DACL
+ self.assertTrue("(OU;;WP;;;AU)" in desc_sddl)
+ # make sure nothing else has been modified
+ self.assertTrue("O:AU" in desc_sddl)
+ self.assertTrue("G:AU" in desc_sddl)
+ self.assertTrue("(D;;CC;;;LG)" in desc_sddl)
+
+ def test_307(self):
+ """ Read a descriptor with OWNER_SECURITY_INFORMATION
+ Only the owner part should be returned.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn, controls=["sd_flags:1:%d" % (SECINFO_OWNER)])
+ # make sure we have read the owner
+ self.assertTrue("O:" in desc_sddl)
+ # make sure we have read nothing else
+ self.assertFalse("G:" in desc_sddl)
+ self.assertFalse("D:" in desc_sddl)
+ self.assertFalse("S:" in desc_sddl)
+
+ def test_308(self):
+ """ Read a descriptor with GROUP_SECURITY_INFORMATION
+ Only the group part should be returned.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn, controls=["sd_flags:1:%d" % (SECINFO_GROUP)])
+ # make sure we have read the owner
+ self.assertTrue("G:" in desc_sddl)
+ # make sure we have read nothing else
+ self.assertFalse("O:" in desc_sddl)
+ self.assertFalse("D:" in desc_sddl)
+ self.assertFalse("S:" in desc_sddl)
+
+ def test_309(self):
+ """ Read a descriptor with SACL_SECURITY_INFORMATION
+ Only the sacl part should be returned.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn, controls=["sd_flags:1:%d" % (SECINFO_SACL)])
+ # make sure we have read the owner
+ self.assertTrue("S:" in desc_sddl)
+ # make sure we have read nothing else
+ self.assertFalse("O:" in desc_sddl)
+ self.assertFalse("D:" in desc_sddl)
+ self.assertFalse("G:" in desc_sddl)
+
+ def test_310(self):
+ """ Read a descriptor with DACL_SECURITY_INFORMATION
+ Only the dacl part should be returned.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn, controls=["sd_flags:1:%d" % (SECINFO_DACL)])
+ # make sure we have read the owner
+ self.assertTrue("D:" in desc_sddl)
+ # make sure we have read nothing else
+ self.assertFalse("O:" in desc_sddl)
+ self.assertFalse("S:" in desc_sddl)
+ self.assertFalse("G:" in desc_sddl)
+
+ def test_311(self):
+ sd_flags = (SECINFO_OWNER |
+ SECINFO_GROUP |
+ SECINFO_DACL |
+ SECINFO_SACL)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ [], controls=None)
+ self.assertFalse("nTSecurityDescriptor" in res[0])
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["name"], controls=None)
+ self.assertFalse("nTSecurityDescriptor" in res[0])
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["name"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertFalse("nTSecurityDescriptor" in res[0])
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["*"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["nTSecurityDescriptor", "*"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["*", "nTSecurityDescriptor"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["nTSecurityDescriptor", "name"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["name", "nTSecurityDescriptor"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["nTSecurityDescriptor"], controls=None)
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["name", "nTSecurityDescriptor"], controls=None)
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["nTSecurityDescriptor", "name"], controls=None)
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertTrue("O:" in sddl)
+ self.assertTrue("G:" in sddl)
+ self.assertTrue("D:" in sddl)
+ self.assertTrue("S:" in sddl)
+
+ def test_312(self):
+ """This search is done by the windows dc join..."""
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None, ["1.1"],
+ controls=["extended_dn:1:0", "sd_flags:1:0", "search_options:1:1"])
+ self.assertFalse("nTSecurityDescriptor" in res[0])
+
+
+class RightsAttributesTests(DescriptorTests):
+
+ def deleteAll(self):
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser_attr"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser_attr2"))
+ delete_force(self.ldb_admin, "OU=test_domain_ou1," + self.base_dn)
+
+ def setUp(self):
+ super(RightsAttributesTests, self).setUp()
+ self.deleteAll()
+ # Create users
+ # User 1
+ self.ldb_admin.newuser("testuser_attr", "samba123@")
+ # User 2, Domain Admins
+ self.ldb_admin.newuser("testuser_attr2", "samba123@")
+ self.ldb_admin.add_remove_group_members("Domain Admins",
+ ["testuser_attr2"],
+ add_members_operation=True)
+
+ def test_sDRightsEffective(self):
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ print(self.get_users_domain_dn("testuser_attr"))
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn("testuser_attr"))
+ # give testuser1 read access so attributes can be retrieved
+ mod = "(A;CI;RP;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ _ldb = self.get_ldb_connection("testuser_attr", "samba123@")
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["sDRightsEffective"])
+ # user should have no rights at all
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["sDRightsEffective"][0]), "0")
+ # give the user Write DACL and see what happens
+ mod = "(A;CI;WD;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["sDRightsEffective"])
+ # user should have DACL_SECURITY_INFORMATION
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["sDRightsEffective"][0]), ("%d") % SECINFO_DACL)
+ # give the user Write Owners and see what happens
+ mod = "(A;CI;WO;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["sDRightsEffective"])
+ # user should have DACL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["sDRightsEffective"][0]), ("%d") % (SECINFO_DACL | SECINFO_GROUP | SECINFO_OWNER))
+ # no way to grant security privilege bu adding ACE's so we use a member of Domain Admins
+ _ldb = self.get_ldb_connection("testuser_attr2", "samba123@")
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["sDRightsEffective"])
+ # user should have DACL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["sDRightsEffective"][0]),
+ ("%d") % (SECINFO_DACL | SECINFO_GROUP | SECINFO_OWNER | SECINFO_SACL))
+
+ def test_allowedChildClassesEffective(self):
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn("testuser_attr"))
+ # give testuser1 read access so attributes can be retrieved
+ mod = "(A;CI;RP;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ _ldb = self.get_ldb_connection("testuser_attr", "samba123@")
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["allowedChildClassesEffective"])
+ # there should be no allowed child classes
+ self.assertEqual(len(res), 1)
+ self.assertFalse("allowedChildClassesEffective" in res[0].keys())
+ # give the user the right to create children of type user
+ mod = "(OA;CI;CC;bf967aba-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["allowedChildClassesEffective"])
+ # allowedChildClassesEffective should only have one value, user
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]["allowedChildClassesEffective"]), 1)
+ self.assertEqual(str(res[0]["allowedChildClassesEffective"][0]), "user")
+
+ def test_allowedAttributesEffective(self):
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn("testuser_attr"))
+ # give testuser1 read access so attributes can be retrieved
+ mod = "(A;CI;RP;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ _ldb = self.get_ldb_connection("testuser_attr", "samba123@")
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["allowedAttributesEffective"])
+ # there should be no allowed attributes
+ self.assertEqual(len(res), 1)
+ self.assertFalse("allowedAttributesEffective" in res[0].keys())
+ # give the user the right to write displayName and managedBy
+ mod2 = "(OA;CI;WP;bf967953-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ mod = "(OA;CI;WP;0296c120-40da-11d1-a9c0-0000f80367c1;;%s)" % str(user_sid)
+ # also rights to modify an read only attribute, fromEntry
+ mod3 = "(OA;CI;WP;9a7ad949-ca53-11d1-bbd0-0080c76670c0;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod + mod2 + mod3)
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["allowedAttributesEffective"])
+ # value should only contain user and managedBy
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]["allowedAttributesEffective"]), 2)
+ self.assertTrue(b"displayName" in res[0]["allowedAttributesEffective"])
+ self.assertTrue(b"managedBy" in res[0]["allowedAttributesEffective"])
+
+
+class SdAutoInheritTests(DescriptorTests):
+ def deleteAll(self):
+ delete_force(self.ldb_admin, self.sub_dn)
+ delete_force(self.ldb_admin, self.ou_dn)
+
+ def setUp(self):
+ super(SdAutoInheritTests, self).setUp()
+ self.ou_dn = "OU=test_SdAutoInherit_ou," + self.base_dn
+ self.sub_dn = "OU=test_sub," + self.ou_dn
+ self.deleteAll()
+
+ def test_301(self):
+ """ Modify a descriptor with OWNER_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ attrs = ["nTSecurityDescriptor", "replPropertyMetaData", "uSNChanged"]
+ controls = ["sd_flags:1:%d" % (SECINFO_DACL)]
+ ace = "(A;CI;CC;;;NU)"
+ sub_ace = "(A;CIID;CC;;;NU)"
+ sd_sddl = "O:BAG:BAD:P(A;CI;0x000f01ff;;;AU)"
+ sd = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ self.ldb_admin.create_ou(self.ou_dn, sd=sd)
+ self.ldb_admin.create_ou(self.sub_dn)
+
+ ou_res0 = self.sd_utils.ldb.search(self.ou_dn, SCOPE_BASE,
+ None, attrs, controls=controls)
+ sub_res0 = self.sd_utils.ldb.search(self.sub_dn, SCOPE_BASE,
+ None, attrs, controls=controls)
+
+ ou_sd0 = ndr_unpack(security.descriptor, ou_res0[0]["nTSecurityDescriptor"][0])
+ sub_sd0 = ndr_unpack(security.descriptor, sub_res0[0]["nTSecurityDescriptor"][0])
+
+ ou_sddl0 = ou_sd0.as_sddl(self.domain_sid)
+ sub_sddl0 = sub_sd0.as_sddl(self.domain_sid)
+
+ self.assertFalse(ace in ou_sddl0)
+ self.assertFalse(ace in sub_sddl0)
+
+ ou_sddl1 = (ou_sddl0[:ou_sddl0.index("(")] + ace +
+ ou_sddl0[ou_sddl0.index("("):])
+
+ sub_sddl1 = (sub_sddl0[:sub_sddl0.index("(")] + ace +
+ sub_sddl0[sub_sddl0.index("("):])
+
+ self.sd_utils.modify_sd_on_dn(self.ou_dn, ou_sddl1, controls=controls)
+
+ sub_res2 = self.sd_utils.ldb.search(self.sub_dn, SCOPE_BASE,
+ None, attrs, controls=controls)
+ ou_res2 = self.sd_utils.ldb.search(self.ou_dn, SCOPE_BASE,
+ None, attrs, controls=controls)
+
+ ou_sd2 = ndr_unpack(security.descriptor, ou_res2[0]["nTSecurityDescriptor"][0])
+ sub_sd2 = ndr_unpack(security.descriptor, sub_res2[0]["nTSecurityDescriptor"][0])
+
+ ou_sddl2 = ou_sd2.as_sddl(self.domain_sid)
+ sub_sddl2 = sub_sd2.as_sddl(self.domain_sid)
+
+ self.assertFalse(ou_sddl2 == ou_sddl0)
+ self.assertFalse(sub_sddl2 == sub_sddl0)
+
+ if ace not in ou_sddl2:
+ print("ou0: %s" % ou_sddl0)
+ print("ou2: %s" % ou_sddl2)
+
+ if sub_ace not in sub_sddl2:
+ print("sub0: %s" % sub_sddl0)
+ print("sub2: %s" % sub_sddl2)
+
+ self.assertTrue(ace in ou_sddl2)
+ self.assertTrue(sub_ace in sub_sddl2)
+
+ ou_usn0 = int(ou_res0[0]["uSNChanged"][0])
+ ou_usn2 = int(ou_res2[0]["uSNChanged"][0])
+ self.assertTrue(ou_usn2 > ou_usn0)
+
+ sub_usn0 = int(sub_res0[0]["uSNChanged"][0])
+ sub_usn2 = int(sub_res2[0]["uSNChanged"][0])
+ self.assertTrue(sub_usn2 == sub_usn0)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+# use 'paged_search' module when connecting remotely
+if host.lower().startswith("ldap://"):
+ ldb_options = ["modules:paged_searches"]
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py
new file mode 100755
index 0000000..f6efdae
--- /dev/null
+++ b/source4/dsdb/tests/python/sites.py
@@ -0,0 +1,637 @@
+#!/usr/bin/env python3
+#
+# Unit tests for sites manipulation in samba
+# Copyright (C) Matthieu Patou <mat@matws.net> 2011
+#
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+import samba.getopt as options
+from samba import sites
+from samba import subnets
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba import gensec
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+import samba.tests
+from samba.tests import delete_force
+from samba.dcerpc import security
+from ldb import SCOPE_SUBTREE, LdbError, ERR_INSUFFICIENT_ACCESS_RIGHTS
+
+parser = optparse.OptionParser("sites.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]
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+#
+# Tests start here
+#
+
+
+class SitesBaseTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SitesBaseTests, self).setUp()
+ self.ldb = SamDB(ldaphost, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.domain_sid = security.dom_sid(self.ldb.get_domain_sid())
+ self.configuration_dn = self.ldb.get_config_basedn().get_linearized()
+
+ def get_user_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+
+# tests on sites
+class SimpleSitesTests(SitesBaseTests):
+
+ def test_create_and_delete(self):
+ """test creation and deletion of 1 site"""
+
+ sites.create_site(self.ldb, self.ldb.get_config_basedn(),
+ "testsamba")
+
+ self.assertRaises(sites.SiteAlreadyExistsException,
+ sites.create_site, self.ldb,
+ self.ldb.get_config_basedn(),
+ "testsamba")
+
+ sites.delete_site(self.ldb, self.ldb.get_config_basedn(),
+ "testsamba")
+
+ self.assertRaises(sites.SiteNotFoundException,
+ sites.delete_site, self.ldb,
+ self.ldb.get_config_basedn(),
+ "testsamba")
+
+ def test_delete_not_empty(self):
+ """test removal of 1 site with servers"""
+
+ self.assertRaises(sites.SiteServerNotEmptyException,
+ sites.delete_site, self.ldb,
+ self.ldb.get_config_basedn(),
+ "Default-First-Site-Name")
+
+
+# tests for subnets
+class SimpleSubnetTests(SitesBaseTests):
+
+ def setUp(self):
+ super(SimpleSubnetTests, self).setUp()
+ self.basedn = self.ldb.get_config_basedn()
+ self.sitename = "testsite"
+ self.sitename2 = "testsite2"
+ self.ldb.transaction_start()
+ sites.create_site(self.ldb, self.basedn, self.sitename)
+ sites.create_site(self.ldb, self.basedn, self.sitename2)
+ self.ldb.transaction_commit()
+
+ def tearDown(self):
+ self.ldb.transaction_start()
+ sites.delete_site(self.ldb, self.basedn, self.sitename)
+ sites.delete_site(self.ldb, self.basedn, self.sitename2)
+ self.ldb.transaction_commit()
+ super(SimpleSubnetTests, self).tearDown()
+
+ def test_create_delete(self):
+ """Create a subnet and delete it again."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.11.12.0/24"
+
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+
+ self.assertRaises(subnets.SubnetAlreadyExists,
+ subnets.create_subnet, self.ldb, basedn, cidr,
+ self.sitename)
+
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 0, 'Failed to delete subnet %s' % cidr)
+
+ def test_create_shift_delete(self):
+ """Create a subnet, shift it to another site, then delete it."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.11.12.0/24"
+
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+
+ subnets.set_subnet_site(self.ldb, basedn, cidr, self.sitename2)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ sites = ret[0]['siteObject']
+ self.assertEqual(len(sites), 1)
+ self.assertEqual(str(sites[0]),
+ 'CN=testsite2,CN=Sites,%s' % self.ldb.get_config_basedn())
+
+ self.assertRaises(subnets.SubnetAlreadyExists,
+ subnets.create_subnet, self.ldb, basedn, cidr,
+ self.sitename)
+
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 0, 'Failed to delete subnet %s' % cidr)
+
+ def test_delete_subnet_that_does_not_exist(self):
+ """Ensure we can't delete a site that isn't there."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.15.0.0/16"
+
+ self.assertRaises(subnets.SubnetNotFound,
+ subnets.delete_subnet, self.ldb, basedn, cidr)
+
+ def get_user_and_ldb(self, username, password, hostname=ldaphost):
+ """Get a connection for a temporarily user that will vanish as soon as
+ the test is over."""
+ user = self.ldb.newuser(username, password)
+ creds_tmp = Credentials()
+ creds_tmp.set_username(username)
+ creds_tmp.set_password(password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS)
+ ldb_target = SamDB(url=hostname, credentials=creds_tmp, lp=lp)
+ self.addCleanup(delete_force, self.ldb, self.get_user_dn(username))
+ return (user, ldb_target)
+
+ def test_rename_delete_good_subnet_to_good_subnet_other_user(self):
+ """Make sure that we can't rename or delete subnets when we aren't
+ admin."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.16.0.0/24"
+ new_cidr = "10.16.1.0/24"
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+ user, non_admin_ldb = self.get_user_and_ldb("notadmin", "samba123@")
+ try:
+ subnets.rename_subnet(non_admin_ldb, basedn, cidr, new_cidr)
+ except LdbError as e:
+ self.assertEqual(e.args[0], ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ("subnet rename by non-admin failed "
+ "in the wrong way: %s" % e))
+ else:
+ self.fail("subnet rename by non-admin succeeded")
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 1, ('Subnet %s destroyed or renamed '
+ 'by non-admin' % cidr))
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression=('(&(objectclass=subnet)(cn=%s))'
+ % new_cidr))
+
+ self.assertEqual(len(ret), 0,
+ 'New subnet %s created by non-admin' % cidr)
+
+ try:
+ subnets.delete_subnet(non_admin_ldb, basedn, cidr)
+ except LdbError as e:
+ self.assertEqual(e.args[0], ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ("subnet delete by non-admin failed "
+ "in the wrong way: %s" % e))
+ else:
+ self.fail("subnet delete by non-admin succeeded:")
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 1, 'Subnet %s deleted non-admin' % cidr)
+
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ def test_create_good_subnet_other_user(self):
+ """Make sure that we can't create subnets when we aren't admin."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.16.0.0/24"
+ user, non_admin_ldb = self.get_user_and_ldb("notadmin", "samba123@")
+ try:
+ subnets.create_subnet(non_admin_ldb, basedn, cidr, self.sitename)
+ except LdbError as e:
+ self.assertEqual(e.args[0], ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ("subnet create by non-admin failed "
+ "in the wrong way: %s" % e))
+ else:
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+ self.fail("subnet create by non-admin succeeded: %s")
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 0, 'New subnet %s created by non-admin' % cidr)
+
+ def test_rename_good_subnet_to_good_subnet(self):
+ """Make sure that we can rename subnets"""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.16.0.0/24"
+ new_cidr = "10.16.1.0/24"
+
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+
+ subnets.rename_subnet(self.ldb, basedn, cidr, new_cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % new_cidr)
+
+ self.assertEqual(len(ret), 1, 'Failed to rename subnet %s' % cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 0, 'Failed to remove old subnet during rename %s' % cidr)
+
+ subnets.delete_subnet(self.ldb, basedn, new_cidr)
+
+ def test_rename_good_subnet_to_bad_subnet(self):
+ """Make sure that the CIDR checking runs during rename"""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.17.0.0/24"
+ bad_cidr = "10.11.12.0/14"
+
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+
+ self.assertRaises(subnets.SubnetInvalid, subnets.rename_subnet,
+ self.ldb, basedn, cidr, bad_cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % bad_cidr)
+
+ self.assertEqual(len(ret), 0, 'Failed to rename subnet %s' % cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 1, 'Failed to remove old subnet during rename %s' % cidr)
+
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ def test_create_bad_ranges(self):
+ """These CIDR ranges all have something wrong with them, and they
+ should all fail."""
+ basedn = self.ldb.get_config_basedn()
+
+ cidrs = [
+ # IPv4
+ # insufficient zeros
+ "10.11.12.0/14",
+ "110.0.0.0/6",
+ "1.0.0.0/0",
+ "10.11.13.1/24",
+ "1.2.3.4/29",
+ "10.11.12.0/21",
+ # out of range mask
+ "110.0.0.0/33",
+ "110.0.0.0/-1",
+ "4.0.0.0/111",
+ # out of range address
+ "310.0.0.0/24",
+ "10.0.0.256/32",
+ "1.1.-20.0/24",
+ # badly formed
+ "1.0.0.0/1e",
+ "1.0.0.0/24.0",
+ "1.0.0.0/1/1",
+ "1.0.0.0",
+ "1.c.0.0/24",
+ "1.2.0.0.0/27",
+ "1.23.0/24",
+ "1.23.0.-7/24",
+ "1.-23.0.7/24",
+ "1.23.-0.7/24",
+ "1.23.0.0/0x10",
+ # IPv6 insufficient zeros -- this could be a subtle one
+ # due to the vagaries of endianness in the 16 bit groups.
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119",
+ "aaaa:bbbb::/31",
+ "a:b::/31",
+ "c000::/1",
+ "a::b00/119",
+ "1::1/127",
+ "1::2/126",
+ "1::100/119",
+ "1::8000/112",
+ # out of range mask
+ "a:b::/130",
+ "a:b::/-1",
+ "::/129",
+ # An IPv4 address can't be exactly the bitmask (MS ADTS)
+ "128.0.0.0/1",
+ "192.0.0.0/2",
+ "255.192.0.0/10",
+ "255.255.255.0/24",
+ "255.255.255.255/32",
+ "0.0.0.0/0",
+ # The address can't have leading zeros (not RFC 4632, but MS ADTS)
+ "00.1.2.0/24",
+ "003.1.2.0/24",
+ "022.1.0.0/16",
+ "00000000000000000000000003.1.2.0/24",
+ "09876::abfc/126",
+ "0aaaa:bbbb::/32",
+ "009876::abfc/126",
+ "000a:bbbb::/32",
+
+ # How about extraneous zeros later on
+ "3.01.2.0/24",
+ "3.1.2.00/24",
+ "22.001.0.0/16",
+ "3.01.02.0/24",
+ "100a:0bbb:0023::/48",
+ "100a::0023/128",
+
+ # Windows doesn't like the zero IPv4 address
+ "0.0.0.0/8",
+ # or the zero mask on IPv6
+ "::/0",
+
+ # various violations of RFC5952
+ "0:0:0:0:0:0:0:0/8",
+ "0::0/0",
+ "::0:0/48",
+ "::0:4/128",
+ "0::/8",
+ "0::4f/128",
+ "0::42:0:0:0:0/64",
+ "4f::0/48",
+
+ # badly formed -- mostly the wrong arrangement of colons
+ "a::b::0/120",
+ "a::abcdf:0/120",
+ "a::g:0/120",
+ "::0::3/48",
+ "2001:3::110::3/118",
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1111:0000/128",
+ "a:::5:0/120",
+
+ # non-canonical representations (vs RFC 5952)
+ # "2001:0:c633:63::1:0/120" is correct
+ "2001:0:c633:63:0:0:1:0/120",
+ "2001::c633:63:0:0:1:0/120",
+ "2001:0:c633:63:0:0:1::/120",
+
+ # "10:0:0:42::/64" is correct
+ "10::42:0:0:0:0/64",
+ "10:0:0:42:0:0:0:0/64",
+
+ # "1::4:5:0:0:8/127" is correct
+ "1:0:0:4:5:0:0:8/127",
+ "1:0:0:4:5::8/127",
+
+ # "2001:db8:0:1:1:1:1:1/128" is correct
+ "2001:db8::1:1:1:1:1/128",
+
+ # IP4 embedded - rejected
+ "a::10.0.0.0/120",
+ "a::10.9.8.7/128",
+
+ # The next ones tinker indirectly with IPv4 embedding,
+ # where Windows has some odd behaviour.
+ #
+ # Samba's libreplace inet_ntop6 expects IPv4 embedding
+ # with addresses in these forms:
+ #
+ # ::wx:yz
+ # ::FFFF:wx:yz
+ #
+ # these will be stringified with trailing dottted decimal, thus:
+ #
+ # ::w.x.y.z
+ # ::ffff:w.x.y.z
+ #
+ # and this will cause the address to be rejected by Samba,
+ # because it uses a inet_pton / inet_ntop round trip to
+ # ascertain correctness.
+
+ "::ffff:0:0/96", # this one fails on WIN2012r2
+ "::ffff:aaaa:a000/120",
+ "::ffff:10:0/120",
+ "::ffff:2:300/120",
+ "::3:0/120",
+ "::2:30/124",
+ "::ffff:2:30/124",
+
+ # completely wrong
+ None,
+ "bob",
+ 3.1415,
+ False,
+ "10.11.16.0/24\x00hidden bytes past a zero",
+ self,
+ ]
+
+ failures = []
+ for cidr in cidrs:
+ try:
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+ except subnets.SubnetInvalid:
+ print("%s fails properly" % (cidr,), file=sys.stderr)
+ continue
+
+ # we are here because it succeeded when it shouldn't have.
+ print("CIDR %s fails to fail" % (cidr,), file=sys.stderr)
+ failures.append(cidr)
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ if failures:
+ print("These bad subnet names were accepted:")
+ for cidr in failures:
+ print(" %s" % cidr)
+ self.fail()
+
+ def test_create_good_ranges(self):
+ """All of these CIDRs are good, and the subnet creation should
+ succeed."""
+ basedn = self.ldb.get_config_basedn()
+
+ cidrs = [
+ # IPv4
+ "10.11.12.0/24",
+ "10.11.12.0/23",
+ "10.11.12.0/25",
+ "110.0.0.0/7",
+ "1.0.0.0/32",
+ "10.11.13.0/32",
+ "10.11.13.1/32",
+ "99.0.97.0/24",
+ "1.2.3.4/30",
+ "10.11.12.0/22",
+ "0.12.13.0/24",
+ # IPv6
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/120",
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11f0/124",
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11fc/126",
+ # don't forget upper case
+ "FFFF:FFFF:FFFF:FFFF:ABCD:EfFF:FFFF:FFeF/128",
+ "9876::ab00/120",
+ "9876::abf0/124",
+ "9876::abfc/126",
+ "aaaa:bbbb::/32",
+ "aaaa:bbba::/31",
+ "aaaa:ba00::/23",
+ "aaaa:bb00::/24",
+ "aaaa:bb00::/77",
+ "::/48",
+ "a:b::/32",
+ "c000::/2",
+ "a::b00/120",
+ "1::2/127",
+ # this pattern of address suffix == mask is forbidden with
+ # IPv4 but OK for IPv6.
+ "8000::/1",
+ "c000::/2",
+ "ffff:ffff:ffc0::/42",
+ "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF/128",
+ # leading zeros are forbidden, but implicit IPv6 zeros
+ # (via "::") are OK.
+ "::1000/116",
+ "::8000/113",
+ # taken to the logical conclusion, "::/0" should be OK, but no.
+ "::/48",
+
+ # Try some reserved ranges, which it might be reasonable
+ # to exclude, but which are not excluded in practice.
+ "129.0.0.0/16",
+ "129.255.0.0/16",
+ "100.64.0.0/10",
+ "127.0.0.0/8",
+ "127.0.0.0/24",
+ "169.254.0.0/16",
+ "169.254.1.0/24",
+ "192.0.0.0/24",
+ "192.0.2.0/24",
+ "198.18.0.0/15",
+ "198.51.100.0/24",
+ "203.0.113.0/24",
+ "224.0.0.0/4",
+ "130.129.0.0/16",
+ "130.255.0.0/16",
+ "192.12.0.0/24",
+ "223.255.255.0/24",
+ "240.255.255.0/24",
+ "224.0.0.0/8",
+ "::/96",
+ "100::/64",
+ "2001:10::/28",
+ "fec0::/10",
+ "ff00::/8",
+ "::1/128",
+ "2001:db8::/32",
+ "2001:10::/28",
+ "2002::/24",
+ "2002:a00::/24",
+ "2002:7f00::/24",
+ "2002:a9fe::/32",
+ "2002:ac10::/28",
+ "2002:c000::/40",
+ "2002:c000:200::/40",
+ "2002:c0a8::/32",
+ "2002:c612::/31",
+ "2002:c633:6400::/40",
+ "2002:cb00:7100::/40",
+ "2002:e000::/20",
+ "2002:f000::/20",
+ "2002:ffff:ffff::/48",
+ "2001::/40",
+ "2001:0:a00::/40",
+ "2001:0:7f00::/40",
+ "2001:0:a9fe::/48",
+ "2001:0:ac10::/44",
+ "2001:0:c000::/56",
+ "2001:0:c000:200::/56",
+ "2001:0:c0a8::/48",
+ "2001:0:c612::/47",
+ "2001:0:c633:6400::/56",
+ "2001:0:cb00:7100::/56",
+ "2001:0:e000::/36",
+ "2001:0:f000::/36",
+ "2001:0:ffff:ffff::/64",
+
+ # non-RFC-5952 versions of these are tested in create_bad_ranges
+ "2001:0:c633:63::1:0/120",
+ "10:0:0:42::/64",
+ "1::4:5:0:0:8/127",
+ "2001:db8:0:1:1:1:1:1/128",
+
+ # The "well-known prefix" 64::ff9b is another IPv4
+ # embedding scheme. Let's try that.
+ "64:ff9b::aaaa:aaaa/127",
+ "64:ff9b::/120",
+ "64:ff9b::ffff:2:3/128",
+ ]
+ failures = []
+
+ for cidr in cidrs:
+ try:
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+ except subnets.SubnetInvalid as e:
+ print(e)
+ failures.append(cidr)
+ continue
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression=('(&(objectclass=subnet)(cn=%s))' %
+ cidr))
+
+ if len(ret) != 1:
+ print("%s was not created" % cidr)
+ failures.append(cidr)
+ continue
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ if failures:
+ print("These good subnet names were not accepted:")
+ for cidr in failures:
+ print(" %s" % cidr)
+ self.fail()
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/sort.py b/source4/dsdb/tests/python/sort.py
new file mode 100644
index 0000000..a4d7b72
--- /dev/null
+++ b/source4/dsdb/tests/python/sort.py
@@ -0,0 +1,379 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Originally based on ./sam.py
+from unicodedata import normalize
+import locale
+locale.setlocale(locale.LC_ALL, ('en_US', 'UTF-8'))
+
+import optparse
+import sys
+import os
+import re
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+from samba.common import cmp
+from functools import cmp_to_key
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+
+parser = optparse.OptionParser("sort.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)
+
+parser.add_option('--elements', type='int', default=33,
+ help="use this many elements in the tests")
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+datadir = os.getenv("DATA_DIR", None)
+if not datadir:
+ print("Please specify the location of the sort expected results with env variable DATA_DIR")
+ sys.exit(1)
+
+host = os.getenv("SERVER", None)
+if not host:
+ print("Please specify the host with env variable SERVER")
+ sys.exit(1)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+def norm(x):
+ if not isinstance(x, str):
+ x = x.decode('utf8')
+ return normalize('NFKC', x).upper()
+
+
+# Python, Windows, and Samba all sort the following sequence in
+# drastically different ways. The order here is what you get from
+# Windows2012R2.
+FIENDISH_TESTS = [' ', ' e', '\t-\t', '\n\t\t', '!@#!@#!', '¼', '¹', '1',
+ '1/4', '1⁄4', '1\xe2\x81\x845', '3', 'abc', 'fo\x00od',
+
+ # Here we also had '\x00food', but that seems to sort
+ # non-deterministically on Windows vis-a-vis 'fo\x00od'.
+
+ 'kōkako', 'ŋđ¼³ŧ “«đð', 'ŋđ¼³ŧ“«đð',
+ 'sorttest', 'sorttēst11,', 'śorttest2', 'śoRttest2',
+ 'ś-o-r-t-t-e-s-t-2', 'soRTTēst2,', 'ṡorttest4', 'ṡorttesT4',
+ 'sörttest-5', 'sÖrttest-5', 'so-rttest7,', '桑巴']
+
+
+class BaseSortTests(samba.tests.TestCase):
+ avoid_tricky_sort = False
+ maxDiff = 2000
+
+ def create_user(self, i, n, prefix='sorttest', suffix='', attrs=None,
+ tricky=False):
+ name = "%s%d%s" % (prefix, i, suffix)
+ user = {
+ 'cn': name,
+ "objectclass": "user",
+ 'givenName': "abcdefghijklmnopqrstuvwxyz"[i % 26],
+ "roomNumber": "%sb\x00c" % (n - i),
+ # with python3 re.sub(r'[^\w,.]', repl, string) doesn't
+ # work as expected with unicode as value for carLicense
+ "carLicense": "XXXXXXXXX" if self.avoid_tricky_sort else "后来经",
+ "employeeNumber": "%s%sx" % (abs(i * (99 - i)), '\n' * (i & 255)),
+ "accountExpires": "%s" % (10 ** 9 + 1000000 * i),
+ "msTSExpireDate4": "19%02d0101010000.0Z" % (i % 100),
+ "flags": str(i * (n - i)),
+ "serialNumber": "abc %s%s%s" % ('AaBb |-/'[i & 7],
+ ' 3z}'[i & 3],
+ '"@'[i & 1],),
+ "comment": "Favourite colour is %d" % (n % (i + 1)),
+ }
+
+ if self.avoid_tricky_sort:
+ # We are not even going to try passing tests that assume
+ # some kind of Unicode awareness.
+ for k, v in user.items():
+ user[k] = re.sub(r'[^\w,.]', 'X', v)
+ else:
+ # Add some even trickier ones!
+ fiendish_index = i % len(FIENDISH_TESTS)
+ user.update({
+ # Sort doesn't look past a NUL byte.
+ "photo": "\x00%d" % (n - i),
+ "audio": "%sn octet string %s%s ♫♬\x00lalala" % ('Aa'[i & 1],
+ chr(i & 255),
+ i),
+ "displayNamePrintable": "%d\x00%c" % (i, i & 255),
+ "adminDisplayName": "%d\x00b" % (n - i),
+ "title": "%d%sb" % (n - i, '\x00' * i),
+
+ # Names that vary only in case. Windows returns
+ # equivalent addresses in the order they were put
+ # in ('a st', 'A st',...). We don't check that.
+ "street": "%s st" % (chr(65 | (i & 14) | ((i & 1) * 32))),
+
+ "streetAddress": FIENDISH_TESTS[fiendish_index],
+ "postalAddress": FIENDISH_TESTS[-fiendish_index],
+ })
+
+ if attrs is not None:
+ user.update(attrs)
+
+ user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
+
+ self.users.append(user)
+ self.ldb.add(user)
+ return user
+
+ def setUp(self):
+ super(BaseSortTests, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.ldb.domain_dn()
+ self.ou = "ou=sort,%s" % self.base_dn
+ if False:
+ try:
+ self.ldb.delete(self.ou, ['tree_delete:1'])
+ except ldb.LdbError as e:
+ print("tried deleting %s, got error %s" % (self.ou, e))
+
+ self.ldb.add({
+ "dn": self.ou,
+ "objectclass": "organizationalUnit"})
+ self.users = []
+ n = opts.elements
+ for i in range(n):
+ self.create_user(i, n)
+
+ attrs = set(self.users[0].keys()) - set([
+ 'objectclass', 'dn'])
+ self.binary_sorted_keys = attrs.intersection(['audio',
+ 'photo',
+ "msTSExpireDate4",
+ 'serialNumber',
+ "displayNamePrintable"])
+
+ self.numeric_sorted_keys = attrs.intersection(['flags',
+ 'accountExpires'])
+
+ self.timestamp_keys = attrs.intersection(['msTSExpireDate4'])
+
+ self.int64_keys = set(['accountExpires'])
+
+ self.locale_sorted_keys = [x for x in attrs if
+ x not in (self.binary_sorted_keys |
+ self.numeric_sorted_keys)]
+
+ self.expected_results = {}
+ self.expected_results_binary = {}
+
+ for k in self.binary_sorted_keys:
+ forward = sorted((x[k] for x in self.users))
+ reverse = list(reversed(forward))
+ self.expected_results_binary[k] = (forward, reverse)
+
+ # FYI: Expected result data was generated from the old
+ # code that was manually sorting (while executing with
+ # python2)
+ # The resulting data was injected into the data file with
+ # code similar to:
+ #
+ # for k in self.expected_results:
+ # f.write("%s = %s\n" % (k, repr(self.expected_results[k][0])))
+
+ f = open(self.results_file, "r")
+ for line in f:
+ if len(line.split('=', 1)) == 2:
+ key = line.split('=', 1)[0].strip()
+ value = line.split('=', 1)[1].strip()
+ if value.startswith('['):
+ import ast
+ fwd_list = ast.literal_eval(value)
+ rev_list = list(reversed(fwd_list))
+ self.expected_results[key] = (fwd_list, rev_list)
+ f.close()
+ def tearDown(self):
+ super(BaseSortTests, self).tearDown()
+ self.ldb.delete(self.ou, ['tree_delete:1'])
+
+ def _test_server_sort_default(self):
+ attrs = self.locale_sorted_keys
+
+ for attr in attrs:
+ for rev in (0, 1):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL, attrs=[attr],
+ controls=["server_sort:1:%d:%s" %
+ (rev, attr)])
+ self.assertEqual(len(res), len(self.users))
+
+ expected_order = self.expected_results[attr][rev]
+ received_order = [norm(x[attr][0]) for x in res]
+ if expected_order != received_order:
+ print(attr, ['forward', 'reverse'][rev])
+ print("expected", expected_order)
+ print("received", received_order)
+ print("unnormalised:", [x[attr][0] for x in res])
+ print("unnormalised: «%s»" % '» «'.join(str(x[attr][0])
+ for x in res))
+ self.assertEqual(expected_order, received_order)
+
+ def _test_server_sort_binary(self):
+ for attr in self.binary_sorted_keys:
+ for rev in (0, 1):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL, attrs=[attr],
+ controls=["server_sort:1:%d:%s" %
+ (rev, attr)])
+
+ self.assertEqual(len(res), len(self.users))
+ expected_order = self.expected_results_binary[attr][rev]
+ received_order = [str(x[attr][0]) for x in res]
+ if expected_order != received_order:
+ print(attr)
+ print(expected_order)
+ print(received_order)
+ self.assertEqual(expected_order, received_order)
+
+ def _test_server_sort_us_english(self):
+ # Windows doesn't support many matching rules, but does allow
+ # the locale specific sorts -- if it has the locale installed.
+ # The most reliable locale is the default US English, which
+ # won't change the sort order.
+
+ for lang, oid in [('en_US', '1.2.840.113556.1.4.1499'),
+ ]:
+
+ for attr in self.locale_sorted_keys:
+ for rev in (0, 1):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=["server_sort:1:%d:%s:%s" %
+ (rev, attr, oid)])
+
+ self.assertTrue(len(res) == len(self.users))
+ expected_order = self.expected_results[attr][rev]
+ received_order = [norm(x[attr][0]) for x in res]
+ if expected_order != received_order:
+ print(attr, lang)
+ print(['forward', 'reverse'][rev])
+ print("expected: ", expected_order)
+ print("received: ", received_order)
+ print("unnormalised:", [x[attr][0] for x in res])
+ print("unnormalised: «%s»" % '» «'.join(str(x[attr][0])
+ for x in res))
+
+ self.assertEqual(expected_order, received_order)
+
+ def _test_server_sort_different_attr(self):
+
+ def cmp_locale(a, b):
+ return locale.strcoll(a[0], b[0])
+
+ def cmp_binary(a, b):
+ return cmp(a[0], b[0])
+
+ def cmp_numeric(a, b):
+ return cmp(int(a[0]), int(b[0]))
+
+ # For testing simplicity, the attributes in here need to be
+ # unique for each user. Otherwise there are multiple possible
+ # valid answers.
+ sort_functions = {'cn': cmp_binary,
+ "employeeNumber": cmp_locale,
+ "accountExpires": cmp_numeric,
+ "msTSExpireDate4": cmp_binary}
+ attrs = list(sort_functions.keys())
+ attr_pairs = zip(attrs, attrs[1:] + attrs[:1])
+
+ for sort_attr, result_attr in attr_pairs:
+ forward = sorted(((norm(x[sort_attr]), norm(x[result_attr]))
+ for x in self.users),
+ key=cmp_to_key(sort_functions[sort_attr]))
+ reverse = list(reversed(forward))
+
+ for rev in (0, 1):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[result_attr],
+ controls=["server_sort:1:%d:%s" %
+ (rev, sort_attr)])
+ self.assertEqual(len(res), len(self.users))
+ pairs = (forward, reverse)[rev]
+
+ expected_order = [x[1] for x in pairs]
+ received_order = [norm(x[result_attr][0]) for x in res]
+
+ if expected_order != received_order:
+ print(sort_attr, result_attr, ['forward', 'reverse'][rev])
+ print("expected", expected_order)
+ print("received", received_order)
+ print("unnormalised:", [x[result_attr][0] for x in res])
+ print("unnormalised: «%s»" % '» «'.join(str(x[result_attr][0])
+ for x in res))
+ print("pairs:", pairs)
+ # There are bugs in Windows that we don't want (or
+ # know how) to replicate regarding timestamp sorting.
+ # Let's remind ourselves.
+ if result_attr == "msTSExpireDate4":
+ print('-' * 72)
+ print("This test fails against Windows with the "
+ "default number of elements (33).")
+ print("Try with --elements=27 (or similar).")
+ print('-' * 72)
+
+ self.assertEqual(expected_order, received_order)
+ for x in res:
+ if sort_attr in x:
+ self.fail('the search for %s should not return %s' %
+ (result_attr, sort_attr))
+
+
+class SimpleSortTests(BaseSortTests):
+ avoid_tricky_sort = True
+ results_file = os.path.join(datadir, "simplesort.expected")
+ def test_server_sort_different_attr(self):
+ self._test_server_sort_different_attr()
+
+ def test_server_sort_default(self):
+ self._test_server_sort_default()
+
+ def test_server_sort_binary(self):
+ self._test_server_sort_binary()
+
+ def test_server_sort_us_english(self):
+ self._test_server_sort_us_english()
+
+
+class UnicodeSortTests(BaseSortTests):
+ avoid_tricky_sort = False
+ results_file = os.path.join(datadir, "unicodesort.expected")
+
+ def test_server_sort_default(self):
+ self._test_server_sort_default()
+
+ def test_server_sort_us_english(self):
+ self._test_server_sort_us_english()
+
+ def test_server_sort_different_attr(self):
+ self._test_server_sort_different_attr()
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
diff --git a/source4/dsdb/tests/python/subtree_rename.py b/source4/dsdb/tests/python/subtree_rename.py
new file mode 100644
index 0000000..d396055
--- /dev/null
+++ b/source4/dsdb/tests/python/subtree_rename.py
@@ -0,0 +1,417 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Originally based on ./sam.py
+import optparse
+import sys
+import os
+import itertools
+from time import time
+from binascii import hexlify
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+from samba.dcerpc import misc
+from samba import colour
+
+parser = optparse.OptionParser("subtree_rename.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)
+
+parser.add_option('--delete-in-setup', action='store_true',
+ help="cleanup in setup")
+
+parser.add_option('--no-cleanup', action='store_true',
+ help="don't cleanup in teardown")
+
+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)
+
+
+def debug(*args, **kwargs):
+ kwargs['file'] = sys.stderr
+ print(*args, **kwargs)
+
+
+class SubtreeRenameTestException(Exception):
+ pass
+
+
+class SubtreeRenameTests(samba.tests.TestCase):
+
+ def delete_ous(self):
+ for ou in (self.ou1, self.ou2, self.ou3):
+ try:
+ self.samdb.delete(ou, ['tree_delete:1'])
+ except ldb.LdbError as e:
+ pass
+
+ def setUp(self):
+ super(SubtreeRenameTests, self).setUp()
+ self.samdb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.samdb.domain_dn()
+ self.ou1 = "OU=subtree1,%s" % self.base_dn
+ self.ou2 = "OU=subtree2,%s" % self.base_dn
+ self.ou3 = "OU=subtree3,%s" % self.base_dn
+ if opts.delete_in_setup:
+ self.delete_ous()
+ self.samdb.add({'objectclass': 'organizationalUnit',
+ 'dn': self.ou1})
+ self.samdb.add({'objectclass': 'organizationalUnit',
+ 'dn': self.ou2})
+
+ debug(colour.c_REV_RED(self.id()))
+
+ def tearDown(self):
+ super(SubtreeRenameTests, self).tearDown()
+ if not opts.no_cleanup:
+ self.delete_ous()
+
+ def add_object(self, cn, objectclass, ou=None, more_attrs={}):
+ dn = "CN=%s,%s" % (cn, ou)
+ attrs = {'cn': cn,
+ 'objectclass': objectclass,
+ 'dn': dn}
+ attrs.update(more_attrs)
+ self.samdb.add(attrs)
+
+ return dn
+
+ def add_objects(self, n, objectclass, prefix=None, ou=None, more_attrs={}):
+ if prefix is None:
+ prefix = objectclass
+ dns = []
+ for i in range(n):
+ dns.append(self.add_object("%s%d" % (prefix, i + 1),
+ objectclass,
+ more_attrs=more_attrs,
+ ou=ou))
+ return dns
+
+ def add_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_ADD, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def remove_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_DELETE, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def add_binary_link(self, src, dest, binary,
+ attr='msDS-RevealedUsers',
+ controls=None):
+ b = hexlify(str(binary).encode('utf-8')).decode('utf-8').upper()
+ dest = 'B:%d:%s:%s' % (len(b), b, dest)
+ self.add_linked_attribute(src, dest, attr, controls)
+ return dest
+
+ def remove_binary_link(self, src, dest, binary,
+ attr='msDS-RevealedUsers',
+ controls=None):
+ b = str(binary).encode('utf-8')
+ dest = 'B:%s:%s' % (hexlify(b), dest)
+ self.remove_linked_attribute(src, dest, attr, controls)
+
+ def replace_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_REPLACE, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def attr_search(self, obj, attr, scope=ldb.SCOPE_BASE, **controls):
+
+ controls = ['%s:%d' % (k, int(v)) for k, v in controls.items()]
+
+ res = self.samdb.search(obj,
+ scope=scope,
+ attrs=[attr],
+ controls=controls)
+ return res
+
+ def assert_links(self, obj, expected, attr, msg='', **kwargs):
+ res = self.attr_search(obj, attr, **kwargs)
+
+ if len(expected) == 0:
+ if attr in res[0]:
+ self.fail("found attr '%s' in %s" % (attr, res[0]))
+ return
+
+ try:
+ results = [str(x) for x in res[0][attr]]
+ except KeyError:
+ self.fail("missing attr '%s' on %s" % (attr, obj))
+
+ expected = sorted(expected)
+ results = sorted(results)
+
+ if expected != results:
+ debug(msg)
+ debug("expected %s" % expected)
+ debug("received %s" % results)
+ debug("missing %s" % (sorted(set(expected) - set(results))))
+ debug("unexpected %s" % (sorted(set(results) - set(expected))))
+
+
+ self.assertEqual(results, expected)
+
+ def assert_back_links(self, obj, expected, attr='memberOf', **kwargs):
+ self.assert_links(obj, expected, attr=attr,
+ msg='%s back links do not match for %s' %
+ (attr, obj),
+ **kwargs)
+
+ def assert_forward_links(self, obj, expected, attr='member', **kwargs):
+ self.assert_links(obj, expected, attr=attr,
+ msg='%s forward links do not match for %s' %
+ (attr, obj),
+ **kwargs)
+
+ def get_object_guid(self, dn):
+ res = self.samdb.search(dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['objectGUID'])
+ return str(misc.GUID(res[0]['objectGUID'][0]))
+
+ def test_la_move_ou_tree(self):
+ tag = 'move_tree'
+
+ u1, u2 = self.add_objects(2, 'user', '%s_u_' % tag, ou=self.ou1)
+ g1, g2 = self.add_objects(2, 'group', '%s_g_' % tag, ou=self.ou1)
+ c1, c2, c3 = self.add_objects(3, 'computer',
+ '%s_c_' % tag,
+ ou=self.ou1)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g1, g2)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+ c1u1 = self.add_binary_link(c1, u1, 'a').replace(self.ou1, self.ou3)
+ c2u1 = self.add_binary_link(c2, u1, 'b').replace(self.ou1, self.ou3)
+ c3u1 = self.add_binary_link(c3, u1, 124.543).replace(self.ou1, self.ou3)
+ c1g1 = self.add_binary_link(c1, g1, 'd').replace(self.ou1, self.ou3)
+ c2g2 = self.add_binary_link(c2, g2, 'd').replace(self.ou1, self.ou3)
+ c2c1 = self.add_binary_link(c2, c1, 'd').replace(self.ou1, self.ou3)
+ c1u2 = self.add_binary_link(c1, u2, 'd').replace(self.ou1, self.ou3)
+ c1u1_2 = self.add_binary_link(c1, u1, 'b').replace(self.ou1, self.ou3)
+
+ self.assertRaisesLdbError(20,
+ "Attribute msDS-RevealedUsers already exists",
+ self.add_binary_link, c1, u2, 'd')
+
+ self.samdb.rename(self.ou1, self.ou3)
+ debug(colour.c_CYAN("rename FINISHED"))
+ u1, u2, g1, g2, c1, c2, c3 = [x.replace(self.ou1, self.ou3)
+ for x in (u1, u2, g1, g2, c1, c2, c3)]
+
+ self.samdb.delete(g2, ['tree_delete:1'])
+
+ self.assert_forward_links(g1, [u1])
+ self.assert_back_links(u1, [g1])
+ self.assert_back_links(u2, set())
+ self.assert_forward_links(c1, [c1u1, c1u1_2, c1u2, c1g1],
+ attr='msDS-RevealedUsers')
+ self.assert_forward_links(c2, [c2u1, c2c1], attr='msDS-RevealedUsers')
+ self.assert_forward_links(c3, [c3u1], attr='msDS-RevealedUsers')
+ self.assert_back_links(u1, [c1, c1, c2, c3], attr='msDS-RevealedDSAs')
+ self.assert_back_links(u2, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(g1, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(c1, [c2], attr='msDS-RevealedDSAs')
+
+ def test_la_move_ou_groups(self):
+ tag = 'move_groups'
+
+ u1, u2 = self.add_objects(2, 'user', '%s_u_' % tag, ou=self.ou2)
+ g1, g2 = self.add_objects(2, 'group', '%s_g_' % tag, ou=self.ou1)
+ c1, c2, c3 = self.add_objects(3, 'computer',
+ '%s_c_' % tag,
+ ou=self.ou1)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g1, g2)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+ c1u1 = self.add_binary_link(c1, u1, 'a').replace(self.ou1, self.ou3)
+ c2u1 = self.add_binary_link(c2, u1, 'b').replace(self.ou1, self.ou3)
+ c3u1 = self.add_binary_link(c3, u1, 124.543).replace(self.ou1, self.ou3)
+ c1g1 = self.add_binary_link(c1, g1, 'd').replace(self.ou1, self.ou3)
+ c2g2 = self.add_binary_link(c2, g2, 'd').replace(self.ou1, self.ou3)
+ c2c1 = self.add_binary_link(c2, c1, 'd').replace(self.ou1, self.ou3)
+ c1u2 = self.add_binary_link(c1, u2, 'd').replace(self.ou1, self.ou3)
+ c1u1_2 = self.add_binary_link(c1, u1, 'b').replace(self.ou1, self.ou3)
+
+ self.samdb.rename(self.ou1, self.ou3)
+ debug(colour.c_CYAN("rename FINISHED"))
+ u1, u2, g1, g2, c1, c2, c3 = [x.replace(self.ou1, self.ou3)
+ for x in (u1, u2, g1, g2, c1, c2, c3)]
+
+ self.samdb.delete(g2, ['tree_delete:1'])
+
+ self.assert_forward_links(g1, [u1])
+ self.assert_back_links(u1, [g1])
+ self.assert_back_links(u2, set())
+ self.assert_forward_links(c1, [c1u1, c1u1_2, c1u2, c1g1],
+ attr='msDS-RevealedUsers')
+ self.assert_forward_links(c2, [c2u1, c2c1], attr='msDS-RevealedUsers')
+ self.assert_forward_links(c3, [c3u1], attr='msDS-RevealedUsers')
+ self.assert_back_links(u1, [c1, c1, c2, c3], attr='msDS-RevealedDSAs')
+ self.assert_back_links(u2, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(g1, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(c1, [c2], attr='msDS-RevealedDSAs')
+
+ def test_la_move_ou_users(self):
+ tag = 'move_users'
+
+ u1, u2 = self.add_objects(2, 'user', '%s_u_' % tag, ou=self.ou1)
+ g1, g2 = self.add_objects(2, 'group', '%s_g_' % tag, ou=self.ou2)
+ c1, c2 = self.add_objects(2, 'computer', '%s_c_' % tag, ou=self.ou1)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g1, g2)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+ c1u1 = self.add_binary_link(c1, u1, 'a').replace(self.ou1, self.ou3)
+ c2u1 = self.add_binary_link(c2, u1, 'b').replace(self.ou1, self.ou3)
+ c1g1 = self.add_binary_link(c1, g1, 'd').replace(self.ou1, self.ou3)
+ c2g2 = self.add_binary_link(c2, g2, 'd').replace(self.ou1, self.ou3)
+ c2c1 = self.add_binary_link(c2, c1, 'd').replace(self.ou1, self.ou3)
+ c1u2 = self.add_binary_link(c1, u2, 'd').replace(self.ou1, self.ou3)
+ c1u1_2 = self.add_binary_link(c1, u1, 'b').replace(self.ou1, self.ou3)
+
+
+ self.samdb.rename(self.ou1, self.ou3)
+ debug(colour.c_CYAN("rename FINISHED"))
+ u1, u2, g1, g2, c1, c2 = [x.replace(self.ou1, self.ou3)
+ for x in (u1, u2, g1, g2, c1, c2)]
+
+ self.samdb.delete(g2, ['tree_delete:1'])
+
+ self.assert_forward_links(g1, [u1])
+ self.assert_back_links(u1, [g1])
+ self.assert_back_links(u2, set())
+ self.assert_forward_links(c1, [c1u1, c1u1_2, c1u2, c1g1],
+ attr='msDS-RevealedUsers')
+ self.assert_forward_links(c2, [c2u1, c2c1], attr='msDS-RevealedUsers')
+ self.assert_back_links(u1, [c1, c1, c2], attr='msDS-RevealedDSAs')
+ self.assert_back_links(u2, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(g1, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(c1, [c2], attr='msDS-RevealedDSAs')
+
+ def test_la_move_ou_noncomputers(self):
+ """Here we are especially testing the msDS-RevealedDSAs links"""
+ tag = 'move_noncomputers'
+
+ u1, u2 = self.add_objects(2, 'user', '%s_u_' % tag, ou=self.ou1)
+ g1, g2 = self.add_objects(2, 'group', '%s_g_' % tag, ou=self.ou1)
+ c1, c2, c3 = self.add_objects(3, 'computer', '%s_c_' % tag, ou=self.ou2)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g1, g2)
+ c1u1 = self.add_binary_link(c1, u1, 'a').replace(self.ou1, self.ou3)
+ c2u1 = self.add_binary_link(c2, u1, 'b').replace(self.ou1, self.ou3)
+ c2u1_2 = self.add_binary_link(c2, u1, 'c').replace(self.ou1, self.ou3)
+ c3u1 = self.add_binary_link(c3, g1, 'b').replace(self.ou1, self.ou3)
+ c1g1 = self.add_binary_link(c1, g1, 'd').replace(self.ou1, self.ou3)
+ c2g2 = self.add_binary_link(c2, g2, 'd').replace(self.ou1, self.ou3)
+ c2c1 = self.add_binary_link(c2, c1, 'd').replace(self.ou1, self.ou3)
+ c1u2 = self.add_binary_link(c1, u2, 'd').replace(self.ou1, self.ou3)
+ c1u1_2 = self.add_binary_link(c1, u1, 'b').replace(self.ou1, self.ou3)
+ c1u1_3 = self.add_binary_link(c1, u1, 'c').replace(self.ou1, self.ou3)
+ c2u1_3 = self.add_binary_link(c2, u1, 'e').replace(self.ou1, self.ou3)
+ c3u2 = self.add_binary_link(c3, u2, 'b').replace(self.ou1, self.ou3)
+
+ self.samdb.rename(self.ou1, self.ou3)
+ debug(colour.c_CYAN("rename FINISHED"))
+ u1, u2, g1, g2, c1, c2, c3 = [x.replace(self.ou1, self.ou3)
+ for x in (u1, u2, g1, g2, c1, c2, c3)]
+
+ self.samdb.delete(c3, ['tree_delete:1'])
+
+ self.assert_forward_links(g1, [g2, u1])
+ self.assert_back_links(u1, [g1])
+ self.assert_back_links(u2, [])
+ self.assert_forward_links(c1, [c1u1, c1u1_2, c1u1_3, c1u2, c1g1],
+ attr='msDS-RevealedUsers')
+ self.assert_forward_links(c2, [c2u1, c2u1_2, c2u1_3, c2c1, c2g2],
+ attr='msDS-RevealedUsers')
+ self.assert_back_links(u1, [c1, c1, c1, c2, c2, c2],
+ attr='msDS-RevealedDSAs')
+ self.assert_back_links(u2, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(g1, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(c1, [c2], attr='msDS-RevealedDSAs')
+
+ def test_la_move_ou_tree_big(self):
+ tag = 'move_ou_big'
+ USERS, GROUPS, COMPUTERS = 50, 10, 7
+
+ users = self.add_objects(USERS, 'user', '%s_u_' % tag, ou=self.ou1)
+ groups = self.add_objects(GROUPS, 'group', '%s_g_' % tag, ou=self.ou1)
+ computers = self.add_objects(COMPUTERS, 'computer', '%s_c_' % tag,
+ ou=self.ou1)
+
+ start = time()
+ for i in range(USERS):
+ u = users[i]
+ for j in range(i % GROUPS):
+ g = groups[j]
+ self.add_linked_attribute(g, u)
+ for j in range(i % COMPUTERS):
+ c = computers[j]
+ self.add_binary_link(c, u, 'a')
+
+ debug("linking took %.3fs" % (time() - start))
+ start = time()
+ self.samdb.rename(self.ou1, self.ou3)
+ debug("rename ou took %.3fs" % (time() - start))
+
+ g1 = groups[0].replace(self.ou1, self.ou3)
+ start = time()
+ self.samdb.rename(g1, g1.replace(self.ou3, self.ou2))
+ debug("rename group took %.3fs" % (time() - start))
+
+ u1 = users[0].replace(self.ou1, self.ou3)
+ start = time()
+ self.samdb.rename(u1, u1.replace(self.ou3, self.ou2))
+ debug("rename user took %.3fs" % (time() - start))
+
+ c1 = computers[0].replace(self.ou1, self.ou3)
+ start = time()
+ self.samdb.rename(c1, c1.replace(self.ou3, self.ou2))
+ debug("rename computer took %.3fs" % (time() - start))
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/testdata/modify_order_account_locality_device-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_account_locality_device-non-admin.expected
new file mode 100644
index 0000000..572ed5e
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_account_locality_device-non-admin.expected
@@ -0,0 +1,31 @@
+modify_order_account_locality_device-non-admin
+initial attrs:
+ objectclass: 'account'
+ l: 'a'
+== result ===[ 6]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ objectclass replace ['device', 'top']
+ l delete a
+ owner add c
+----------------------------------
+ objectclass replace ['device', 'top']
+ owner add c
+ l delete a
+----------------------------------
+ l delete a
+ objectclass replace ['device', 'top']
+ owner add c
+----------------------------------
+ l delete a
+ owner add c
+ objectclass replace ['device', 'top']
+----------------------------------
+ owner add c
+ objectclass replace ['device', 'top']
+ l delete a
+----------------------------------
+ owner add c
+ l delete a
+ objectclass replace ['device', 'top']
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_account_locality_device.expected b/source4/dsdb/tests/python/testdata/modify_order_account_locality_device.expected
new file mode 100644
index 0000000..dc5e162
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_account_locality_device.expected
@@ -0,0 +1,34 @@
+modify_order_account_locality_device
+initial attrs:
+ objectclass: 'account'
+ l: 'a'
+== result ===[ 3]=======================
+ERR_CONSTRAINT_VIOLATION (19)
+-- operations ---------------------------
+ l delete a
+ owner add c
+ objectclass replace ['device', 'top']
+----------------------------------
+ owner add c
+ objectclass replace ['device', 'top']
+ l delete a
+----------------------------------
+ owner add c
+ l delete a
+ objectclass replace ['device', 'top']
+----------------------------------
+== result ===[ 3]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ objectclass replace ['device', 'top']
+ l delete a
+ owner add c
+----------------------------------
+ objectclass replace ['device', 'top']
+ owner add c
+ l delete a
+----------------------------------
+ l delete a
+ objectclass replace ['device', 'top']
+ owner add c
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_container_flags-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_container_flags-non-admin.expected
new file mode 100644
index 0000000..9a46588
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_container_flags-non-admin.expected
@@ -0,0 +1,129 @@
+modify_order_container_flags-non-admin
+initial attrs:
+ objectclass: 'container'
+== result ===[ 12]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ flags add 0x6
+ flags add 5
+ flags delete c
+ flags replace 101
+----------------------------------
+ flags add 0x6
+ flags replace 101
+ flags delete c
+ flags add 5
+----------------------------------
+ flags add 0x6
+ flags delete c
+ flags add 5
+ flags replace 101
+----------------------------------
+ flags add 0x6
+ flags delete c
+ flags replace 101
+ flags add 5
+----------------------------------
+ flags add 5
+ flags add 0x6
+ flags delete c
+ flags replace 101
+----------------------------------
+ flags add 5
+ flags delete c
+ flags add 0x6
+ flags replace 101
+----------------------------------
+ flags replace 101
+ flags add 0x6
+ flags delete c
+ flags add 5
+----------------------------------
+ flags replace 101
+ flags delete c
+ flags add 0x6
+ flags add 5
+----------------------------------
+ flags delete c
+ flags add 0x6
+ flags add 5
+ flags replace 101
+----------------------------------
+ flags delete c
+ flags add 0x6
+ flags replace 101
+ flags add 5
+----------------------------------
+ flags delete c
+ flags add 5
+ flags add 0x6
+ flags replace 101
+----------------------------------
+ flags delete c
+ flags replace 101
+ flags add 0x6
+ flags add 5
+----------------------------------
+== result ===[ 12]=======================
+ERR_INVALID_ATTRIBUTE_SYNTAX (21)
+-- operations ---------------------------
+ flags add 0x6
+ flags add 5
+ flags replace 101
+ flags delete c
+----------------------------------
+ flags add 0x6
+ flags replace 101
+ flags add 5
+ flags delete c
+----------------------------------
+ flags add 5
+ flags add 0x6
+ flags replace 101
+ flags delete c
+----------------------------------
+ flags add 5
+ flags replace 101
+ flags add 0x6
+ flags delete c
+----------------------------------
+ flags add 5
+ flags replace 101
+ flags delete c
+ flags add 0x6
+----------------------------------
+ flags add 5
+ flags delete c
+ flags replace 101
+ flags add 0x6
+----------------------------------
+ flags replace 101
+ flags add 0x6
+ flags add 5
+ flags delete c
+----------------------------------
+ flags replace 101
+ flags add 5
+ flags add 0x6
+ flags delete c
+----------------------------------
+ flags replace 101
+ flags add 5
+ flags delete c
+ flags add 0x6
+----------------------------------
+ flags replace 101
+ flags delete c
+ flags add 5
+ flags add 0x6
+----------------------------------
+ flags delete c
+ flags add 5
+ flags replace 101
+ flags add 0x6
+----------------------------------
+ flags delete c
+ flags replace 101
+ flags add 5
+ flags add 0x6
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_container_flags.expected b/source4/dsdb/tests/python/testdata/modify_order_container_flags.expected
new file mode 100644
index 0000000..eee3c52
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_container_flags.expected
@@ -0,0 +1,134 @@
+modify_order_container_flags
+initial attrs:
+ objectclass: 'container'
+== result ===[ 6]=======================
+ flags: [b'101']
+ objectClass: [b'container', b'top']
+-- operations ---------------------------
+ flags add 0x6
+ flags add 5
+ flags delete c
+ flags replace 101
+----------------------------------
+ flags add 0x6
+ flags delete c
+ flags add 5
+ flags replace 101
+----------------------------------
+ flags add 5
+ flags add 0x6
+ flags delete c
+ flags replace 101
+----------------------------------
+ flags add 5
+ flags delete c
+ flags add 0x6
+ flags replace 101
+----------------------------------
+ flags delete c
+ flags add 0x6
+ flags add 5
+ flags replace 101
+----------------------------------
+ flags delete c
+ flags add 5
+ flags add 0x6
+ flags replace 101
+----------------------------------
+== result ===[ 6]=======================
+ flags: [b'5']
+ objectClass: [b'container', b'top']
+-- operations ---------------------------
+ flags add 0x6
+ flags replace 101
+ flags delete c
+ flags add 5
+----------------------------------
+ flags add 0x6
+ flags delete c
+ flags replace 101
+ flags add 5
+----------------------------------
+ flags replace 101
+ flags add 0x6
+ flags delete c
+ flags add 5
+----------------------------------
+ flags replace 101
+ flags delete c
+ flags add 0x6
+ flags add 5
+----------------------------------
+ flags delete c
+ flags add 0x6
+ flags replace 101
+ flags add 5
+----------------------------------
+ flags delete c
+ flags replace 101
+ flags add 0x6
+ flags add 5
+----------------------------------
+== result ===[ 12]=======================
+ERR_INVALID_ATTRIBUTE_SYNTAX (21)
+-- operations ---------------------------
+ flags add 0x6
+ flags add 5
+ flags replace 101
+ flags delete c
+----------------------------------
+ flags add 0x6
+ flags replace 101
+ flags add 5
+ flags delete c
+----------------------------------
+ flags add 5
+ flags add 0x6
+ flags replace 101
+ flags delete c
+----------------------------------
+ flags add 5
+ flags replace 101
+ flags add 0x6
+ flags delete c
+----------------------------------
+ flags add 5
+ flags replace 101
+ flags delete c
+ flags add 0x6
+----------------------------------
+ flags add 5
+ flags delete c
+ flags replace 101
+ flags add 0x6
+----------------------------------
+ flags replace 101
+ flags add 0x6
+ flags add 5
+ flags delete c
+----------------------------------
+ flags replace 101
+ flags add 5
+ flags add 0x6
+ flags delete c
+----------------------------------
+ flags replace 101
+ flags add 5
+ flags delete c
+ flags add 0x6
+----------------------------------
+ flags replace 101
+ flags delete c
+ flags add 5
+ flags add 0x6
+----------------------------------
+ flags delete c
+ flags add 5
+ flags replace 101
+ flags add 0x6
+----------------------------------
+ flags delete c
+ flags replace 101
+ flags add 5
+ flags add 0x6
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue-non-admin.expected
new file mode 100644
index 0000000..c6f89c0
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue-non-admin.expected
@@ -0,0 +1,127 @@
+modify_order_container_flags_multivalue-non-admin
+initial attrs:
+ objectclass: 'container'
+ wWWHomePage: 'a'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ flags add ['0', '1']
+ flags add 65355
+ flags delete 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add ['0', '1']
+ flags add 65355
+ flags replace ['2', '101']
+ flags delete 65355
+----------------------------------
+ flags add ['0', '1']
+ flags delete 65355
+ flags add 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add ['0', '1']
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add 65355
+----------------------------------
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags add 65355
+ flags delete 65355
+----------------------------------
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add 65355
+----------------------------------
+ flags add 65355
+ flags add ['0', '1']
+ flags delete 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags delete 65355
+----------------------------------
+ flags add 65355
+ flags delete 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+----------------------------------
+ flags add 65355
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+----------------------------------
+ flags add 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags delete 65355
+----------------------------------
+ flags add 65355
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add ['0', '1']
+----------------------------------
+ flags delete 65355
+ flags add ['0', '1']
+ flags add 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags delete 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags add 65355
+----------------------------------
+ flags delete 65355
+ flags add 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+----------------------------------
+ flags delete 65355
+ flags add 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+----------------------------------
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags add 65355
+----------------------------------
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add 65355
+ flags add ['0', '1']
+----------------------------------
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags add 65355
+ flags delete 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags delete 65355
+ flags add 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add 65355
+ flags add ['0', '1']
+ flags delete 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add 65355
+ flags delete 65355
+ flags add ['0', '1']
+----------------------------------
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add ['0', '1']
+ flags add 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add 65355
+ flags add ['0', '1']
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue.expected b/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue.expected
new file mode 100644
index 0000000..99ee5a7
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue.expected
@@ -0,0 +1,138 @@
+modify_order_container_flags_multivalue
+initial attrs:
+ objectclass: 'container'
+ wWWHomePage: 'a'
+== result ===[ 6]=======================
+ flags: [b'65355']
+ objectClass: [b'container', b'top']
+ wWWHomePage: [b'a']
+-- operations ---------------------------
+ flags add ['0', '1']
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add 65355
+----------------------------------
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add 65355
+----------------------------------
+ flags delete 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags add 65355
+----------------------------------
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags add 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags delete 65355
+ flags add 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add ['0', '1']
+ flags add 65355
+----------------------------------
+== result ===[ 6]=======================
+ERR_ATTRIBUTE_OR_VALUE_EXISTS (20)
+-- operations ---------------------------
+ flags add 65355
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+----------------------------------
+ flags add 65355
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add ['0', '1']
+----------------------------------
+ flags delete 65355
+ flags add 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+----------------------------------
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add 65355
+ flags add ['0', '1']
+----------------------------------
+ flags replace ['2', '101']
+ flags add 65355
+ flags delete 65355
+ flags add ['0', '1']
+----------------------------------
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add 65355
+ flags add ['0', '1']
+----------------------------------
+== result ===[ 6]=======================
+ERR_CONSTRAINT_VIOLATION (19)
+-- operations ---------------------------
+ flags add ['0', '1']
+ flags add 65355
+ flags delete 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add ['0', '1']
+ flags delete 65355
+ flags add 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add 65355
+ flags add ['0', '1']
+ flags delete 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add 65355
+ flags delete 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+----------------------------------
+ flags delete 65355
+ flags add ['0', '1']
+ flags add 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags delete 65355
+ flags add 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+----------------------------------
+== result ===[ 6]=======================
+ERR_NO_SUCH_ATTRIBUTE (16)
+-- operations ---------------------------
+ flags add ['0', '1']
+ flags add 65355
+ flags replace ['2', '101']
+ flags delete 65355
+----------------------------------
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags add 65355
+ flags delete 65355
+----------------------------------
+ flags add 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags delete 65355
+----------------------------------
+ flags add 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags delete 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags add 65355
+ flags delete 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add 65355
+ flags add ['0', '1']
+ flags delete 65355
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_inapplicable-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_inapplicable-non-admin.expected
new file mode 100644
index 0000000..0adb093
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_inapplicable-non-admin.expected
@@ -0,0 +1,31 @@
+modify_order_inapplicable-non-admin
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 6]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ dhcpSites replace b
+ dhcpSites delete b
+ dhcpSites add c
+----------------------------------
+ dhcpSites replace b
+ dhcpSites add c
+ dhcpSites delete b
+----------------------------------
+ dhcpSites delete b
+ dhcpSites replace b
+ dhcpSites add c
+----------------------------------
+ dhcpSites delete b
+ dhcpSites add c
+ dhcpSites replace b
+----------------------------------
+ dhcpSites add c
+ dhcpSites replace b
+ dhcpSites delete b
+----------------------------------
+ dhcpSites add c
+ dhcpSites delete b
+ dhcpSites replace b
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_inapplicable.expected b/source4/dsdb/tests/python/testdata/modify_order_inapplicable.expected
new file mode 100644
index 0000000..f16ef8c
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_inapplicable.expected
@@ -0,0 +1,34 @@
+modify_order_inapplicable
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 2]=======================
+ERR_NO_SUCH_ATTRIBUTE (16)
+-- operations ---------------------------
+ dhcpSites replace b
+ dhcpSites add c
+ dhcpSites delete b
+----------------------------------
+ dhcpSites add c
+ dhcpSites replace b
+ dhcpSites delete b
+----------------------------------
+== result ===[ 4]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ dhcpSites replace b
+ dhcpSites delete b
+ dhcpSites add c
+----------------------------------
+ dhcpSites delete b
+ dhcpSites replace b
+ dhcpSites add c
+----------------------------------
+ dhcpSites delete b
+ dhcpSites add c
+ dhcpSites replace b
+----------------------------------
+ dhcpSites add c
+ dhcpSites delete b
+ dhcpSites replace b
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_member-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_member-non-admin.expected
new file mode 100644
index 0000000..ea7d26b
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_member-non-admin.expected
@@ -0,0 +1,127 @@
+modify_order_member-non-admin
+initial attrs:
+ objectclass: 'group'
+ member: 'cn=modify_order_member_other_group,{base dn}'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_0,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_0,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_0,cn=users,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_1,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_1,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_1,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_2,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_2,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_2,cn=users,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_3,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_3,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_3,cn=users,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_4,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_4,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_4,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_5,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_5,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_5,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_6,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_6,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_6,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_7,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_7,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_7,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_8,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_8,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_8,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_9,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_9,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_9,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_10,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_10,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_10,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_11,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_11,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_11,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_12,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_12,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_12,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_13,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_13,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_13,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_14,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_14,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_14,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_15,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_15,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_15,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_16,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_16,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_16,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_17,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_17,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_17,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_18,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_18,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_18,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_19,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_19,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_19,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_20,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_20,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_20,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_21,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_21,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_21,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_22,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_22,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_22,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_23,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_23,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_23,cn=users,{base dn}
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_member.expected b/source4/dsdb/tests/python/testdata/modify_order_member.expected
new file mode 100644
index 0000000..1882c34
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_member.expected
@@ -0,0 +1,190 @@
+modify_order_member
+initial attrs:
+ objectclass: 'group'
+ member: 'cn=modify_order_member_other_group,{base dn}'
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_0,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_0,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_0,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_0,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_0,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_12,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_12,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_12,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_12,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_12,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_13,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_13,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_13,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_13,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_13,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_14,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_14,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_14,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_14,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_14,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_16,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_16,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_16,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_16,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_16,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_19,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_19,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member add cn=ldaptest_modify_order_member_19,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_19,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_19,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_2,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_2,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_2,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_2,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_2,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_22,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_22,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member add cn=ldaptest_modify_order_member_22,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_22,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_22,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_3,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_3,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_3,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_3,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_3,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_5,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_5,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_5,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_5,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_5,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_6,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_6,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member replace cn=ldaptest_modify_order_member_6,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_6,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_6,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_8,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_8,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member replace cn=ldaptest_modify_order_member_8,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_8,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_8,cn=users,{base dn}
+----------------------------------
+== result ===[ 6]=======================
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_1,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_1,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_1,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member_4,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_4,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_4,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member_7,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_7,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_7,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member_10,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_10,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_10,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member_18,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_18,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_18,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member_20,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_20,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_20,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+== result ===[ 6]=======================
+ERR_UNWILLING_TO_PERFORM (53)
+-- operations ---------------------------
+ member replace cn=ldaptest_modify_order_member_9,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_9,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_9,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member_11,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_11,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_11,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_15,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_15,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_15,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_17,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_17,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_17,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member_21,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_21,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_21,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member_23,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_23,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_23,cn=users,{base dn}
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_mixed-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_mixed-non-admin.expected
new file mode 100644
index 0000000..544c31c
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_mixed-non-admin.expected
@@ -0,0 +1,128 @@
+modify_order_mixed-non-admin
+initial attrs:
+ objectclass: 'user'
+ carLicense: ['1', '2', '3']
+ otherTelephone: '123'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense delete 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense delete 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense delete 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense delete 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_mixed.expected b/source4/dsdb/tests/python/testdata/modify_order_mixed.expected
new file mode 100644
index 0000000..d80f572
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_mixed.expected
@@ -0,0 +1,143 @@
+modify_order_mixed
+initial attrs:
+ objectclass: 'user'
+ carLicense: ['1', '2', '3']
+ otherTelephone: '123'
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2', b'3', b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2', b'3', b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense delete 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense delete 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense delete 3
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense delete 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense delete 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_mixed2-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_mixed2-non-admin.expected
new file mode 100644
index 0000000..7f812cc
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_mixed2-non-admin.expected
@@ -0,0 +1,128 @@
+modify_order_mixed2-non-admin
+initial attrs:
+ objectclass: 'user'
+ carLicense: ['1', '2', '3']
+ ipPhone: '123'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ ipPhone replace 4
+ ipPhone delete 123
+----------------------------------
+ carLicense delete 3
+ carLicense add 4
+ ipPhone delete 123
+ ipPhone replace 4
+----------------------------------
+ carLicense delete 3
+ ipPhone replace 4
+ carLicense add 4
+ ipPhone delete 123
+----------------------------------
+ carLicense delete 3
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense delete 3
+ ipPhone delete 123
+ carLicense add 4
+ ipPhone replace 4
+----------------------------------
+ carLicense delete 3
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense add 4
+ carLicense delete 3
+ ipPhone replace 4
+ ipPhone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense delete 3
+ ipPhone delete 123
+ ipPhone replace 4
+----------------------------------
+ carLicense add 4
+ ipPhone replace 4
+ carLicense delete 3
+ ipPhone delete 123
+----------------------------------
+ carLicense add 4
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense delete 3
+----------------------------------
+ carLicense add 4
+ ipPhone delete 123
+ carLicense delete 3
+ ipPhone replace 4
+----------------------------------
+ carLicense add 4
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense delete 3
+----------------------------------
+ ipPhone replace 4
+ carLicense delete 3
+ carLicense add 4
+ ipPhone delete 123
+----------------------------------
+ ipPhone replace 4
+ carLicense delete 3
+ ipPhone delete 123
+ carLicense add 4
+----------------------------------
+ ipPhone replace 4
+ carLicense add 4
+ carLicense delete 3
+ ipPhone delete 123
+----------------------------------
+ ipPhone replace 4
+ carLicense add 4
+ ipPhone delete 123
+ carLicense delete 3
+----------------------------------
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense add 4
+ carLicense delete 3
+----------------------------------
+ ipPhone delete 123
+ carLicense delete 3
+ carLicense add 4
+ ipPhone replace 4
+----------------------------------
+ ipPhone delete 123
+ carLicense delete 3
+ ipPhone replace 4
+ carLicense add 4
+----------------------------------
+ ipPhone delete 123
+ carLicense add 4
+ carLicense delete 3
+ ipPhone replace 4
+----------------------------------
+ ipPhone delete 123
+ carLicense add 4
+ ipPhone replace 4
+ carLicense delete 3
+----------------------------------
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense add 4
+ carLicense delete 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_mixed2.expected b/source4/dsdb/tests/python/testdata/modify_order_mixed2.expected
new file mode 100644
index 0000000..3500a8c
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_mixed2.expected
@@ -0,0 +1,143 @@
+modify_order_mixed2
+initial attrs:
+ objectclass: 'user'
+ carLicense: ['1', '2', '3']
+ ipPhone: '123'
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2', b'3', b'4']
+ ipPhone: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ ipPhone delete 123
+ ipPhone replace 4
+----------------------------------
+ carLicense delete 3
+ ipPhone delete 123
+ carLicense add 4
+ ipPhone replace 4
+----------------------------------
+ carLicense delete 3
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense add 4
+----------------------------------
+ ipPhone delete 123
+ carLicense delete 3
+ carLicense add 4
+ ipPhone replace 4
+----------------------------------
+ ipPhone delete 123
+ carLicense delete 3
+ ipPhone replace 4
+ carLicense add 4
+----------------------------------
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2', b'3', b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ ipPhone replace 4
+ ipPhone delete 123
+----------------------------------
+ carLicense delete 3
+ ipPhone replace 4
+ carLicense add 4
+ ipPhone delete 123
+----------------------------------
+ carLicense delete 3
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense add 4
+----------------------------------
+ ipPhone replace 4
+ carLicense delete 3
+ carLicense add 4
+ ipPhone delete 123
+----------------------------------
+ ipPhone replace 4
+ carLicense delete 3
+ ipPhone delete 123
+ carLicense add 4
+----------------------------------
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2']
+ ipPhone: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense delete 3
+ ipPhone delete 123
+ ipPhone replace 4
+----------------------------------
+ carLicense add 4
+ ipPhone delete 123
+ carLicense delete 3
+ ipPhone replace 4
+----------------------------------
+ carLicense add 4
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense delete 3
+----------------------------------
+ ipPhone delete 123
+ carLicense add 4
+ carLicense delete 3
+ ipPhone replace 4
+----------------------------------
+ ipPhone delete 123
+ carLicense add 4
+ ipPhone replace 4
+ carLicense delete 3
+----------------------------------
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense add 4
+ carLicense delete 3
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense delete 3
+ ipPhone replace 4
+ ipPhone delete 123
+----------------------------------
+ carLicense add 4
+ ipPhone replace 4
+ carLicense delete 3
+ ipPhone delete 123
+----------------------------------
+ carLicense add 4
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense delete 3
+----------------------------------
+ ipPhone replace 4
+ carLicense add 4
+ carLicense delete 3
+ ipPhone delete 123
+----------------------------------
+ ipPhone replace 4
+ carLicense add 4
+ ipPhone delete 123
+ carLicense delete 3
+----------------------------------
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense add 4
+ carLicense delete 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_objectclass-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_objectclass-non-admin.expected
new file mode 100644
index 0000000..1e9650a
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_objectclass-non-admin.expected
@@ -0,0 +1,31 @@
+modify_order_objectclass-non-admin
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[ 6]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete user
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass delete user
+----------------------------------
+ objectclass delete user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass delete user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass delete user
+----------------------------------
+ objectclass delete person
+ objectclass delete user
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_objectclass.expected b/source4/dsdb/tests/python/testdata/modify_order_objectclass.expected
new file mode 100644
index 0000000..0ec6d4a
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_objectclass.expected
@@ -0,0 +1,35 @@
+modify_order_objectclass
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[ 2]=======================
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'123']
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete user
+ objectclass delete person
+----------------------------------
+ objectclass delete user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+== result ===[ 4]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass delete user
+----------------------------------
+ objectclass delete user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass delete user
+----------------------------------
+ objectclass delete person
+ objectclass delete user
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_objectclass2-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_objectclass2-non-admin.expected
new file mode 100644
index 0000000..2515154
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_objectclass2-non-admin.expected
@@ -0,0 +1,726 @@
+modify_order_objectclass2-non-admin
+initial attrs:
+ objectclass: 'user'
+== result ===[120]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_objectclass2.expected b/source4/dsdb/tests/python/testdata/modify_order_objectclass2.expected
new file mode 100644
index 0000000..4f51708
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_objectclass2.expected
@@ -0,0 +1,735 @@
+modify_order_objectclass2
+initial attrs:
+ objectclass: 'user'
+== result ===[ 24]=======================
+ objectClass: [b'inetOrgPerson', b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+== result ===[ 24]=======================
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+== result ===[ 24]=======================
+ERR_ATTRIBUTE_OR_VALUE_EXISTS (20)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+== result ===[ 48]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_singlevalue-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_singlevalue-non-admin.expected
new file mode 100644
index 0000000..f9d717d
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_singlevalue-non-admin.expected
@@ -0,0 +1,727 @@
+modify_order_singlevalue-non-admin
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[120]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_singlevalue.expected b/source4/dsdb/tests/python/testdata/modify_order_singlevalue.expected
new file mode 100644
index 0000000..9946165
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_singlevalue.expected
@@ -0,0 +1,740 @@
+modify_order_singlevalue
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 24]=======================
+ givenName: [b'a']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+== result ===[ 24]=======================
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+----------------------------------
+== result ===[ 24]=======================
+ERR_ATTRIBUTE_OR_VALUE_EXISTS (20)
+-- operations ---------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+----------------------------------
+== result ===[ 24]=======================
+ERR_CONSTRAINT_VIOLATION (19)
+-- operations ---------------------------
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+== result ===[ 24]=======================
+ERR_NO_SUCH_ATTRIBUTE (16)
+-- operations ---------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable-non-admin.expected
new file mode 100644
index 0000000..fd144d7
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable-non-admin.expected
@@ -0,0 +1,127 @@
+modify_order_sometimes_inapplicable-non-admin
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName add b
+ dnsHostName replace c
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName replace c
+ dnsHostName add b
+----------------------------------
+ objectclass replace computer
+ dnsHostName add b
+ objectclass delete person
+ dnsHostName replace c
+----------------------------------
+ objectclass replace computer
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ dnsHostName replace c
+ objectclass delete person
+ dnsHostName add b
+----------------------------------
+ objectclass replace computer
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass delete person
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName add b
+ dnsHostName replace c
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName replace c
+ dnsHostName add b
+----------------------------------
+ objectclass delete person
+ dnsHostName add b
+ objectclass replace computer
+ dnsHostName replace c
+----------------------------------
+ objectclass delete person
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ dnsHostName replace c
+ objectclass replace computer
+ dnsHostName add b
+----------------------------------
+ objectclass delete person
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass replace computer
+----------------------------------
+ dnsHostName add b
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName replace c
+----------------------------------
+ dnsHostName add b
+ objectclass replace computer
+ dnsHostName replace c
+ objectclass delete person
+----------------------------------
+ dnsHostName add b
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName replace c
+----------------------------------
+ dnsHostName add b
+ objectclass delete person
+ dnsHostName replace c
+ objectclass replace computer
+----------------------------------
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ dnsHostName replace c
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName add b
+----------------------------------
+ dnsHostName replace c
+ objectclass replace computer
+ dnsHostName add b
+ objectclass delete person
+----------------------------------
+ dnsHostName replace c
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName add b
+----------------------------------
+ dnsHostName replace c
+ objectclass delete person
+ dnsHostName add b
+ objectclass replace computer
+----------------------------------
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass delete person
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable.expected b/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable.expected
new file mode 100644
index 0000000..a8af7f0
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable.expected
@@ -0,0 +1,127 @@
+modify_order_sometimes_inapplicable
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 24]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName add b
+ dnsHostName replace c
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName replace c
+ dnsHostName add b
+----------------------------------
+ objectclass replace computer
+ dnsHostName add b
+ objectclass delete person
+ dnsHostName replace c
+----------------------------------
+ objectclass replace computer
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ dnsHostName replace c
+ objectclass delete person
+ dnsHostName add b
+----------------------------------
+ objectclass replace computer
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass delete person
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName add b
+ dnsHostName replace c
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName replace c
+ dnsHostName add b
+----------------------------------
+ objectclass delete person
+ dnsHostName add b
+ objectclass replace computer
+ dnsHostName replace c
+----------------------------------
+ objectclass delete person
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ dnsHostName replace c
+ objectclass replace computer
+ dnsHostName add b
+----------------------------------
+ objectclass delete person
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass replace computer
+----------------------------------
+ dnsHostName add b
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName replace c
+----------------------------------
+ dnsHostName add b
+ objectclass replace computer
+ dnsHostName replace c
+ objectclass delete person
+----------------------------------
+ dnsHostName add b
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName replace c
+----------------------------------
+ dnsHostName add b
+ objectclass delete person
+ dnsHostName replace c
+ objectclass replace computer
+----------------------------------
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ dnsHostName replace c
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName add b
+----------------------------------
+ dnsHostName replace c
+ objectclass replace computer
+ dnsHostName add b
+ objectclass delete person
+----------------------------------
+ dnsHostName replace c
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName add b
+----------------------------------
+ dnsHostName replace c
+ objectclass delete person
+ dnsHostName add b
+ objectclass replace computer
+----------------------------------
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass delete person
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_telephone-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_telephone-non-admin.expected
new file mode 100644
index 0000000..fd46b3a
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_telephone-non-admin.expected
@@ -0,0 +1,727 @@
+modify_order_telephone-non-admin
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[120]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_telephone.expected b/source4/dsdb/tests/python/testdata/modify_order_telephone.expected
new file mode 100644
index 0000000..d17de03
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_telephone.expected
@@ -0,0 +1,752 @@
+modify_order_telephone
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'123', b'4']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'123', b'4']
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete-non-admin.expected
new file mode 100644
index 0000000..96fc4fd
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete-non-admin.expected
@@ -0,0 +1,727 @@
+modify_order_telephone_delete_delete-non-admin
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[120]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete.expected b/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete.expected
new file mode 100644
index 0000000..14983ba
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete.expected
@@ -0,0 +1,736 @@
+modify_order_telephone_delete_delete
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 80]=======================
+ERR_NO_SUCH_ATTRIBUTE (16)
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/simplesort.expected b/source4/dsdb/tests/python/testdata/simplesort.expected
new file mode 100644
index 0000000..045337b
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/simplesort.expected
@@ -0,0 +1,8 @@
+comment = [u'FAVOURITEXCOLOURXISX0', u'FAVOURITEXCOLOURXISX0', u'FAVOURITEXCOLOURXISX0', u'FAVOURITEXCOLOURXISX0', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX10', u'FAVOURITEXCOLOURXISX11', u'FAVOURITEXCOLOURXISX12', u'FAVOURITEXCOLOURXISX13', u'FAVOURITEXCOLOURXISX14', u'FAVOURITEXCOLOURXISX15', u'FAVOURITEXCOLOURXISX16', u'FAVOURITEXCOLOURXISX2', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX4', u'FAVOURITEXCOLOURXISX5', u'FAVOURITEXCOLOURXISX5', u'FAVOURITEXCOLOURXISX5', u'FAVOURITEXCOLOURXISX6', u'FAVOURITEXCOLOURXISX6', u'FAVOURITEXCOLOURXISX7', u'FAVOURITEXCOLOURXISX7', u'FAVOURITEXCOLOURXISX8', u'FAVOURITEXCOLOURXISX9', u'FAVOURITEXCOLOURXISX9']
+msTSExpireDate4 = ['19000101010000.0Z', '19010101010000.0Z', '19020101010000.0Z', '19030101010000.0Z', '19040101010000.0Z', '19050101010000.0Z', '19060101010000.0Z', '19070101010000.0Z', '19080101010000.0Z', '19090101010000.0Z', '19100101010000.0Z', '19110101010000.0Z', '19120101010000.0Z', '19130101010000.0Z', '19140101010000.0Z', '19150101010000.0Z', '19160101010000.0Z', '19170101010000.0Z', '19180101010000.0Z', '19190101010000.0Z', '19200101010000.0Z', '19210101010000.0Z', '19220101010000.0Z', '19230101010000.0Z', '19240101010000.0Z', '19250101010000.0Z', '19260101010000.0Z', '19270101010000.0Z', '19280101010000.0Z', '19290101010000.0Z', '19300101010000.0Z', '19310101010000.0Z', '19320101010000.0Z']
+cn = [u'SORTTEST0', u'SORTTEST1', u'SORTTEST10', u'SORTTEST11', u'SORTTEST12', u'SORTTEST13', u'SORTTEST14', u'SORTTEST15', u'SORTTEST16', u'SORTTEST17', u'SORTTEST18', u'SORTTEST19', u'SORTTEST2', u'SORTTEST20', u'SORTTEST21', u'SORTTEST22', u'SORTTEST23', u'SORTTEST24', u'SORTTEST25', u'SORTTEST26', u'SORTTEST27', u'SORTTEST28', u'SORTTEST29', u'SORTTEST3', u'SORTTEST30', u'SORTTEST31', u'SORTTEST32', u'SORTTEST4', u'SORTTEST5', u'SORTTEST6', u'SORTTEST7', u'SORTTEST8', u'SORTTEST9']
+serialNumber = ['abcXAXX', 'abcXAXX', 'abcXAXX', 'abcXAXX', 'abcXAXX', 'abcXBzX', 'abcXBzX', 'abcXBzX', 'abcXBzX', 'abcXX3X', 'abcXX3X', 'abcXX3X', 'abcXX3X', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXzX', 'abcXXzX', 'abcXXzX', 'abcXXzX', 'abcXa3X', 'abcXa3X', 'abcXa3X', 'abcXa3X', 'abcXbXX', 'abcXbXX', 'abcXbXX', 'abcXbXX']
+roomNumber = [u'10BXC', u'11BXC', u'12BXC', u'13BXC', u'14BXC', u'15BXC', u'16BXC', u'17BXC', u'18BXC', u'19BXC', u'1BXC', u'20BXC', u'21BXC', u'22BXC', u'23BXC', u'24BXC', u'25BXC', u'26BXC', u'27BXC', u'28BXC', u'29BXC', u'2BXC', u'30BXC', u'31BXC', u'32BXC', u'33BXC', u'3BXC', u'4BXC', u'5BXC', u'6BXC', u'7BXC', u'8BXC', u'9BXC']
+carLicense = [u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX']
+employeeNumber = [u'0X', u'1044XXXXXXXXXXXXX', u'1118XXXXXXXXXXXXXX', u'1190XXXXXXXXXXXXXXX', u'1260XXXXXXXXXXXXXXXX', u'1328XXXXXXXXXXXXXXXXX', u'1394XXXXXXXXXXXXXXXXXX', u'1458XXXXXXXXXXXXXXXXXXX', u'1520XXXXXXXXXXXXXXXXXXXX', u'1580XXXXXXXXXXXXXXXXXXXXX', u'1638XXXXXXXXXXXXXXXXXXXXXX', u'1694XXXXXXXXXXXXXXXXXXXXXXX', u'1748XXXXXXXXXXXXXXXXXXXXXXXX', u'1800XXXXXXXXXXXXXXXXXXXXXXXXX', u'1850XXXXXXXXXXXXXXXXXXXXXXXXXX', u'1898XXXXXXXXXXXXXXXXXXXXXXXXXXX', u'1944XXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'194XXX', u'1988XXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'2030XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'2070XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'2108XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'2144XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'288XXXX', u'380XXXXX', u'470XXXXXX', u'558XXXXXXX', u'644XXXXXXXX', u'728XXXXXXXXX', u'810XXXXXXXXXX', u'890XXXXXXXXXXX', u'968XXXXXXXXXXXX', u'98XX']
+givenName = [u'A', u'A', u'B', u'B', u'C', u'C', u'D', u'D', u'E', u'E', u'F', u'F', u'G', u'G', u'H', u'I', u'J', u'K', u'L', u'M', u'N', u'O', u'P', u'Q', u'R', u'S', u'T', u'U', u'V', u'W', u'X', u'Y', u'Z']
diff --git a/source4/dsdb/tests/python/testdata/unicodesort.expected b/source4/dsdb/tests/python/testdata/unicodesort.expected
new file mode 100644
index 0000000..de07cfc
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/unicodesort.expected
@@ -0,0 +1,16 @@
+comment = [u'FAVOURITE COLOUR IS 0', u'FAVOURITE COLOUR IS 0', u'FAVOURITE COLOUR IS 0', u'FAVOURITE COLOUR IS 0', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 10', u'FAVOURITE COLOUR IS 11', u'FAVOURITE COLOUR IS 12', u'FAVOURITE COLOUR IS 13', u'FAVOURITE COLOUR IS 14', u'FAVOURITE COLOUR IS 15', u'FAVOURITE COLOUR IS 16', u'FAVOURITE COLOUR IS 2', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 4', u'FAVOURITE COLOUR IS 5', u'FAVOURITE COLOUR IS 5', u'FAVOURITE COLOUR IS 5', u'FAVOURITE COLOUR IS 6', u'FAVOURITE COLOUR IS 6', u'FAVOURITE COLOUR IS 7', u'FAVOURITE COLOUR IS 7', u'FAVOURITE COLOUR IS 8', u'FAVOURITE COLOUR IS 9', u'FAVOURITE COLOUR IS 9']
+msTSExpireDate4 = ['19000101010000.0Z', '19010101010000.0Z', '19020101010000.0Z', '19030101010000.0Z', '19040101010000.0Z', '19050101010000.0Z', '19060101010000.0Z', '19070101010000.0Z', '19080101010000.0Z', '19090101010000.0Z', '19100101010000.0Z', '19110101010000.0Z', '19120101010000.0Z', '19130101010000.0Z', '19140101010000.0Z', '19150101010000.0Z', '19160101010000.0Z', '19170101010000.0Z', '19180101010000.0Z', '19190101010000.0Z', '19200101010000.0Z', '19210101010000.0Z', '19220101010000.0Z', '19230101010000.0Z', '19240101010000.0Z', '19250101010000.0Z', '19260101010000.0Z', '19270101010000.0Z', '19280101010000.0Z', '19290101010000.0Z', '19300101010000.0Z', '19310101010000.0Z', '19320101010000.0Z']
+audio = ['An octet string \x000 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x022 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x044 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x066 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x088 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \n10 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x0c12 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x0e14 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1016 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1218 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1420 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1622 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1824 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1a26 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1c28 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1e30 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string 32 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x011 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x033 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x055 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x077 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \t9 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x0b11 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \r13 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x0f15 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1117 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1319 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1521 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1723 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1925 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1b27 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1d29 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1f31 \xe2\x99\xab\xe2\x99\xac\x00lalala']
+adminDisplayName = [u'10\x00B', u'11\x00B', u'12\x00B', u'13\x00B', u'14\x00B', u'15\x00B', u'16\x00B', u'17\x00B', u'18\x00B', u'19\x00B', u'1\x00B', u'20\x00B', u'21\x00B', u'22\x00B', u'23\x00B', u'24\x00B', u'25\x00B', u'26\x00B', u'27\x00B', u'28\x00B', u'29\x00B', u'2\x00B', u'30\x00B', u'31\x00B', u'32\x00B', u'33\x00B', u'3\x00B', u'4\x00B', u'5\x00B', u'6\x00B', u'7\x00B', u'8\x00B', u'9\x00B']
+cn = [u'SORTTEST0', u'SORTTEST1', u'SORTTEST10', u'SORTTEST11', u'SORTTEST12', u'SORTTEST13', u'SORTTEST14', u'SORTTEST15', u'SORTTEST16', u'SORTTEST17', u'SORTTEST18', u'SORTTEST19', u'SORTTEST2', u'SORTTEST20', u'SORTTEST21', u'SORTTEST22', u'SORTTEST23', u'SORTTEST24', u'SORTTEST25', u'SORTTEST26', u'SORTTEST27', u'SORTTEST28', u'SORTTEST29', u'SORTTEST3', u'SORTTEST30', u'SORTTEST31', u'SORTTEST32', u'SORTTEST4', u'SORTTEST5', u'SORTTEST6', u'SORTTEST7', u'SORTTEST8', u'SORTTEST9']
+title = [u'10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'13\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'22\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'23\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'24\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'25\x00\x00\x00\x00\x00\x00\x00\x00B', u'26\x00\x00\x00\x00\x00\x00\x00B', u'27\x00\x00\x00\x00\x00\x00B', u'28\x00\x00\x00\x00\x00B', u'29\x00\x00\x00\x00B', u'2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'30\x00\x00\x00B', u'31\x00\x00B', u'32\x00B', u'33B', u'3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B']
+photo = ['\x001', '\x0010', '\x0011', '\x0012', '\x0013', '\x0014', '\x0015', '\x0016', '\x0017', '\x0018', '\x0019', '\x002', '\x0020', '\x0021', '\x0022', '\x0023', '\x0024', '\x0025', '\x0026', '\x0027', '\x0028', '\x0029', '\x003', '\x0030', '\x0031', '\x0032', '\x0033', '\x004', '\x005', '\x006', '\x007', '\x008', '\x009']
+serialNumber = ['abc "', 'abc "', 'abc "', 'abc "', 'abc -z"', 'abc -z"', 'abc -z"', 'abc -z"', 'abc /}@', 'abc /}@', 'abc /}@', 'abc /}@', 'abc A "', 'abc A "', 'abc A "', 'abc A "', 'abc A "', 'abc Bz"', 'abc Bz"', 'abc Bz"', 'abc Bz"', 'abc a3@', 'abc a3@', 'abc a3@', 'abc a3@', 'abc b}@', 'abc b}@', 'abc b}@', 'abc b}@', 'abc |3@', 'abc |3@', 'abc |3@', 'abc |3@']
+roomNumber = [u'10B\x00C', u'11B\x00C', u'12B\x00C', u'13B\x00C', u'14B\x00C', u'15B\x00C', u'16B\x00C', u'17B\x00C', u'18B\x00C', u'19B\x00C', u'1B\x00C', u'20B\x00C', u'21B\x00C', u'22B\x00C', u'23B\x00C', u'24B\x00C', u'25B\x00C', u'26B\x00C', u'27B\x00C', u'28B\x00C', u'29B\x00C', u'2B\x00C', u'30B\x00C', u'31B\x00C', u'32B\x00C', u'33B\x00C', u'3B\x00C', u'4B\x00C', u'5B\x00C', u'6B\x00C', u'7B\x00C', u'8B\x00C', u'9B\x00C']
+carLicense = [u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf']
+streetAddress = [u' ', u' ', u' E', u' E', u'\t-\t', u'\t-\t', u'\n\t\t', u'\n\t\t', u'!@#!@#!', u'1\u20444', u'1', u'1', u'1/4', u'1\u20444', u'1\u20445', u'3', u'ABC', u'K\u014cKAKO', u'\u014a\u01101\u204443\u0166 \u201c\xab\u0110\xd0', u'\u014a\u01101\u204443\u0166\u201c\xab\u0110\xd0', u'SORTTEST', u'SORTT\u0112ST11,', u'\u015aORTTEST2', u'\u015aORTTEST2', u'\u015a-O-R-T-T-E-S-T-2', u'SORTT\u0112ST2,', u'\u1e60ORTTEST4', u'\u1e60ORTTEST4', u'S\xd6RTTEST-5', u'S\xd6RTTEST-5', u'SO-RTTEST7,', u'\u6851\u5df4', u'FO\x00OD']
+street = [u'A ST', u'A ST', u'A ST', u'A ST', u'A ST', u'C ST', u'C ST', u'C ST', u'C ST', u'E ST', u'E ST', u'E ST', u'E ST', u'G ST', u'G ST', u'G ST', u'G ST', u'I ST', u'I ST', u'I ST', u'I ST', u'K ST', u'K ST', u'K ST', u'K ST', u'M ST', u'M ST', u'M ST', u'M ST', u'O ST', u'O ST', u'O ST', u'O ST']
+employeeNumber = [u'0X', u'1044\n\n\n\n\n\n\n\n\n\n\n\nX', u'1118\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1190\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1260\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1328\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1394\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1458\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1520\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1580\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1638\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1694\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1748\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1800\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1850\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1898\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'194\n\nX', u'1944\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1988\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'2030\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'2070\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'2108\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'2144\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'288\n\n\nX', u'380\n\n\n\nX', u'470\n\n\n\n\nX', u'558\n\n\n\n\n\nX', u'644\n\n\n\n\n\n\nX', u'728\n\n\n\n\n\n\n\nX', u'810\n\n\n\n\n\n\n\n\nX', u'890\n\n\n\n\n\n\n\n\n\nX', u'968\n\n\n\n\n\n\n\n\n\n\nX', u'98\nX']
+postalAddress = [u' ', u' ', u' E', u'\t-\t', u'\n\t\t', u'!@#!@#!', u'1\u20444', u'1', u'1', u'1/4', u'1\u20444', u'1\u20445', u'3', u'ABC', u'K\u014cKAKO', u'\u014a\u01101\u204443\u0166 \u201c\xab\u0110\xd0', u'\u014a\u01101\u204443\u0166\u201c\xab\u0110\xd0', u'SORTTEST', u'SORTT\u0112ST11,', u'\u015aORTTEST2', u'\u015aORTTEST2', u'\u015a-O-R-T-T-E-S-T-2', u'SORTT\u0112ST2,', u'\u1e60ORTTEST4', u'\u1e60ORTTEST4', u'S\xd6RTTEST-5', u'S\xd6RTTEST-5', u'SO-RTTEST7,', u'SO-RTTEST7,', u'\u6851\u5df4', u'\u6851\u5df4', u'FO\x00OD', u'FO\x00OD']
+givenName = [u'A', u'A', u'B', u'B', u'C', u'C', u'D', u'D', u'E', u'E', u'F', u'F', u'G', u'G', u'H', u'I', u'J', u'K', u'L', u'M', u'N', u'O', u'P', u'Q', u'R', u'S', u'T', u'U', u'V', u'W', u'X', u'Y', u'Z']
+displayNamePrintable = ['0\x00\x00', '1\x00\x01', '10\x00\n', '11\x00\x0b', '12\x00\x0c', '13\x00\r', '14\x00\x0e', '15\x00\x0f', '16\x00\x10', '17\x00\x11', '18\x00\x12', '19\x00\x13', '2\x00\x02', '20\x00\x14', '21\x00\x15', '22\x00\x16', '23\x00\x17', '24\x00\x18', '25\x00\x19', '26\x00\x1a', '27\x00\x1b', '28\x00\x1c', '29\x00\x1d', '3\x00\x03', '30\x00\x1e', '31\x00\x1f', '32\x00 ', '4\x00\x04', '5\x00\x05', '6\x00\x06', '7\x00\x07', '8\x00\x08', '9\x00\t']
diff --git a/source4/dsdb/tests/python/token_group.py b/source4/dsdb/tests/python/token_group.py
new file mode 100755
index 0000000..3d0cd5d
--- /dev/null
+++ b/source4/dsdb/tests/python/token_group.py
@@ -0,0 +1,736 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# test tokengroups attribute against internal token calculation
+
+import optparse
+import sys
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba import ldb, dsdb
+from samba.samdb import SamDB
+from samba.auth import AuthContext
+from samba.ndr import ndr_unpack
+from samba import gensec
+from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS, AUTO_USE_KERBEROS
+from samba.dsdb import GTYPE_SECURITY_GLOBAL_GROUP, GTYPE_SECURITY_UNIVERSAL_GROUP
+import samba.tests
+from samba.tests import delete_force
+from samba.dcerpc import samr, security
+from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHENTICATED, AUTH_SESSION_INFO_SIMPLE_PRIVILEGES, AUTH_SESSION_INFO_NTLM
+
+
+parser = optparse.OptionParser("token_group.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)
+
+url = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+
+def closure(vSet, wSet, aSet):
+ for edge in aSet:
+ start, end = edge
+ if start in wSet:
+ if end not in wSet and end in vSet:
+ wSet.add(end)
+ closure(vSet, wSet, aSet)
+
+
+class StaticTokenTest(samba.tests.TestCase):
+
+ def setUp(self):
+ super(StaticTokenTest, self).setUp()
+
+ self.assertNotEqual(creds.get_kerberos_state(), AUTO_USE_KERBEROS)
+
+ self.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ self.user_sid_dn = "<SID=%s>" % str(ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["tokenGroups"][0]))
+
+ session_info_flags = (AUTH_SESSION_INFO_DEFAULT_GROUPS |
+ AUTH_SESSION_INFO_AUTHENTICATED |
+ AUTH_SESSION_INFO_SIMPLE_PRIVILEGES)
+ if creds.get_kerberos_state() == DONT_USE_KERBEROS:
+ session_info_flags |= AUTH_SESSION_INFO_NTLM
+
+ session = samba.auth.user_session(self.ldb, lp_ctx=lp, dn=self.user_sid_dn,
+ session_info_flags=session_info_flags)
+
+ token = session.security_token
+ self.user_sids = []
+ for s in token.sids:
+ self.user_sids.append(str(s))
+
+ # Add asserted identity for Kerberos
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ self.user_sids.append(str(security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY))
+
+
+ def test_rootDSE_tokenGroups(self):
+ """Testing rootDSE tokengroups against internal calculation"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ print("Getting tokenGroups from rootDSE")
+ tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ tokengroups.append(str(ndr_unpack(samba.dcerpc.security.dom_sid, sid)))
+
+ sidset1 = set(tokengroups)
+ sidset2 = set(self.user_sids)
+ if len(sidset1.symmetric_difference(sidset2)):
+ print("token sids don't match")
+ print("tokengroups: %s" % tokengroups)
+ print("calculated : %s" % self.user_sids)
+ print("difference : %s" % sidset1.symmetric_difference(sidset2))
+ self.fail(msg="calculated groups don't match against rootDSE tokenGroups")
+
+ def test_dn_tokenGroups(self):
+ print("Getting tokenGroups from user DN")
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ dn_tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ dn_tokengroups.append(str(ndr_unpack(samba.dcerpc.security.dom_sid, sid)))
+
+ sidset1 = set(dn_tokengroups)
+ sidset2 = set(self.user_sids)
+
+ # The tokenGroups is just a subset of the user_sids
+ # so we don't check symmetric_difference() here.
+ if len(sidset1.difference(sidset2)):
+ print("dn token sids no subset of user token")
+ print("tokengroups: %s" % dn_tokengroups)
+ print("user sids : %s" % self.user_sids)
+ print("difference : %s" % sidset1.difference(sidset2))
+ self.fail(msg="DN tokenGroups no subset of full user token")
+
+ missing_sidset = sidset2.difference(sidset1)
+
+ extra_sids = []
+ extra_sids.append(self.user_sids[0])
+ extra_sids.append(security.SID_WORLD)
+ extra_sids.append(security.SID_NT_NETWORK)
+ extra_sids.append(security.SID_NT_AUTHENTICATED_USERS)
+ extra_sids.append(security.SID_BUILTIN_PREW2K)
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ extra_sids.append(security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY)
+ if creds.get_kerberos_state() == DONT_USE_KERBEROS:
+ extra_sids.append(security.SID_NT_NTLM_AUTHENTICATION)
+
+ extra_sidset = set(extra_sids)
+
+ if len(missing_sidset.symmetric_difference(extra_sidset)):
+ print("dn token sids unexpected")
+ print("tokengroups: %s" % dn_tokengroups)
+ print("user sids: %s" % self.user_sids)
+ print("actual difference: %s" % missing_sidset)
+ print("expected difference: %s" % extra_sidset)
+ print("unexpected difference : %s" %
+ missing_sidset.symmetric_difference(extra_sidset))
+ self.fail(msg="DN tokenGroups unexpected difference to full user token")
+
+ def test_pac_groups(self):
+ if creds.get_kerberos_state() != MUST_USE_KERBEROS:
+ self.skipTest("Kerberos disabled, skipping PAC test")
+
+ settings = {}
+ settings["lp_ctx"] = lp
+ settings["target_hostname"] = lp.get("netbios name")
+
+ gensec_client = gensec.Security.start_client(settings)
+ gensec_client.set_credentials(creds)
+ gensec_client.want_feature(gensec.FEATURE_SEAL)
+ gensec_client.start_mech_by_sasl_name("GSSAPI")
+
+ auth_context = AuthContext(lp_ctx=lp, ldb=self.ldb, methods=[])
+
+ gensec_server = gensec.Security.start_server(settings, auth_context)
+ machine_creds = Credentials()
+ machine_creds.guess(lp)
+ machine_creds.set_machine_account(lp)
+ gensec_server.set_credentials(machine_creds)
+
+ gensec_server.want_feature(gensec.FEATURE_SEAL)
+ gensec_server.start_mech_by_sasl_name("GSSAPI")
+
+ client_finished = False
+ server_finished = False
+ server_to_client = b""
+
+ # Run the actual call loop.
+ while client_finished == False and server_finished == False:
+ if not client_finished:
+ print("running client gensec_update")
+ (client_finished, client_to_server) = gensec_client.update(server_to_client)
+ if not server_finished:
+ print("running server gensec_update")
+ (server_finished, server_to_client) = gensec_server.update(client_to_server)
+
+ session = gensec_server.session_info()
+
+ token = session.security_token
+ pac_sids = []
+ for s in token.sids:
+ pac_sids.append(str(s))
+
+ sidset1 = set(pac_sids)
+ sidset2 = set(self.user_sids)
+ if len(sidset1.symmetric_difference(sidset2)):
+ print("token sids don't match")
+ print("pac sids: %s" % pac_sids)
+ print("user sids : %s" % self.user_sids)
+ print("difference : %s" % sidset1.symmetric_difference(sidset2))
+ self.fail(msg="calculated groups don't match against user PAC tokenGroups")
+
+
+class DynamicTokenTest(samba.tests.TestCase):
+
+ def get_creds(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_kerberos_state(creds.get_kerberos_state())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ return creds_tmp
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = self.get_creds(target_username, target_password)
+ ldb_target = SamDB(url=url, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ def setUp(self):
+ super(DynamicTokenTest, self).setUp()
+
+ self.assertNotEqual(creds.get_kerberos_state(), AUTO_USE_KERBEROS)
+
+ self.admin_ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.admin_ldb.domain_dn()
+
+ self.test_user = "tokengroups_user1"
+ self.test_user_pass = "samba123@"
+ self.admin_ldb.newuser(self.test_user, self.test_user_pass)
+ self.test_group0 = "tokengroups_group0"
+ self.admin_ldb.newgroup(self.test_group0, grouptype=dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group0, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group0_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group0, [self.test_user],
+ add_members_operation=True)
+
+ self.test_group1 = "tokengroups_group1"
+ self.admin_ldb.newgroup(self.test_group1, grouptype=dsdb.GTYPE_SECURITY_GLOBAL_GROUP)
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group1, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group1_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group1, [self.test_user],
+ add_members_operation=True)
+
+ self.test_group2 = "tokengroups_group2"
+ self.admin_ldb.newgroup(self.test_group2, grouptype=dsdb.GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group2, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group2_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group2, [self.test_user],
+ add_members_operation=True)
+
+ self.test_group3 = "tokengroups_group3"
+ self.admin_ldb.newgroup(self.test_group3, grouptype=dsdb.GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group3, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group3_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group3, [self.test_group1],
+ add_members_operation=True)
+
+ self.test_group4 = "tokengroups_group4"
+ self.admin_ldb.newgroup(self.test_group4, grouptype=dsdb.GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group4, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group4_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group4, [self.test_group3],
+ add_members_operation=True)
+
+ self.test_group5 = "tokengroups_group5"
+ self.admin_ldb.newgroup(self.test_group5, grouptype=dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group5, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group5_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group5, [self.test_group4],
+ add_members_operation=True)
+
+ self.test_group6 = "tokengroups_group6"
+ self.admin_ldb.newgroup(self.test_group6, grouptype=dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group6, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group6_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group6, [self.test_user],
+ add_members_operation=True)
+
+ self.ldb = self.get_ldb_connection(self.test_user, self.test_user_pass)
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ self.user_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["tokenGroups"][0])
+ self.user_sid_dn = "<SID=%s>" % str(self.user_sid)
+
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=[])
+ self.assertEqual(len(res), 1)
+
+ self.test_user_dn = res[0].dn
+
+ session_info_flags = (AUTH_SESSION_INFO_DEFAULT_GROUPS |
+ AUTH_SESSION_INFO_AUTHENTICATED |
+ AUTH_SESSION_INFO_SIMPLE_PRIVILEGES)
+
+ if creds.get_kerberos_state() == DONT_USE_KERBEROS:
+ session_info_flags |= AUTH_SESSION_INFO_NTLM
+
+ session = samba.auth.user_session(self.ldb, lp_ctx=lp, dn=self.user_sid_dn,
+ session_info_flags=session_info_flags)
+
+ token = session.security_token
+ self.user_sids = []
+ for s in token.sids:
+ self.user_sids.append(str(s))
+
+ # Add asserted identity for Kerberos
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ self.user_sids.append(str(security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY))
+
+ def tearDown(self):
+ super(DynamicTokenTest, self).tearDown()
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_user, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group0, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group1, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group2, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group3, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group4, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group5, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group6, "cn=users", self.base_dn))
+
+ def test_rootDSE_tokenGroups(self):
+ """Testing rootDSE tokengroups against internal calculation"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ print("Getting tokenGroups from rootDSE")
+ tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ tokengroups.append(str(ndr_unpack(samba.dcerpc.security.dom_sid, sid)))
+
+ sidset1 = set(tokengroups)
+ sidset2 = set(self.user_sids)
+ if len(sidset1.symmetric_difference(sidset2)):
+ print("token sids don't match")
+ print("tokengroups: %s" % tokengroups)
+ print("calculated : %s" % self.user_sids)
+ print("difference : %s" % sidset1.symmetric_difference(sidset2))
+ self.fail(msg="calculated groups don't match against rootDSE tokenGroups")
+
+ def test_dn_tokenGroups(self):
+ print("Getting tokenGroups from user DN")
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ dn_tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ dn_tokengroups.append(str(ndr_unpack(samba.dcerpc.security.dom_sid, sid)))
+
+ sidset1 = set(dn_tokengroups)
+ sidset2 = set(self.user_sids)
+
+ # The tokenGroups is just a subset of the user_sids
+ # so we don't check symmetric_difference() here.
+ if len(sidset1.difference(sidset2)):
+ print("dn token sids no subset of user token")
+ print("tokengroups: %s" % dn_tokengroups)
+ print("user sids : %s" % self.user_sids)
+ print("difference : %s" % sidset1.difference(sidset2))
+ self.fail(msg="DN tokenGroups no subset of full user token")
+
+ missing_sidset = sidset2.difference(sidset1)
+
+ extra_sids = []
+ extra_sids.append(self.user_sids[0])
+ extra_sids.append(security.SID_WORLD)
+ extra_sids.append(security.SID_NT_NETWORK)
+ extra_sids.append(security.SID_NT_AUTHENTICATED_USERS)
+ extra_sids.append(security.SID_BUILTIN_PREW2K)
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ extra_sids.append(security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY)
+ if creds.get_kerberos_state() == DONT_USE_KERBEROS:
+ extra_sids.append(security.SID_NT_NTLM_AUTHENTICATION)
+
+ extra_sidset = set(extra_sids)
+
+ if len(missing_sidset.symmetric_difference(extra_sidset)):
+ print("dn token sids unexpected")
+ print("tokengroups: %s" % dn_tokengroups)
+ print("user sids: %s" % self.user_sids)
+ print("actual difference: %s" % missing_sidset)
+ print("expected difference: %s" % extra_sidset)
+ print("unexpected difference : %s" %
+ missing_sidset.symmetric_difference(extra_sidset))
+ self.fail(msg="DN tokenGroups unexpected difference to full user token")
+
+ def test_pac_groups(self):
+ if creds.get_kerberos_state() != MUST_USE_KERBEROS:
+ self.skipTest("Kerberos disabled, skipping PAC test")
+
+ settings = {}
+ settings["lp_ctx"] = lp
+ settings["target_hostname"] = lp.get("netbios name")
+
+ gensec_client = gensec.Security.start_client(settings)
+ gensec_client.set_credentials(self.get_creds(self.test_user, self.test_user_pass))
+ gensec_client.want_feature(gensec.FEATURE_SEAL)
+ gensec_client.start_mech_by_sasl_name("GSSAPI")
+
+ auth_context = AuthContext(lp_ctx=lp, ldb=self.ldb, methods=[])
+
+ gensec_server = gensec.Security.start_server(settings, auth_context)
+ machine_creds = Credentials()
+ machine_creds.guess(lp)
+ machine_creds.set_machine_account(lp)
+ gensec_server.set_credentials(machine_creds)
+
+ gensec_server.want_feature(gensec.FEATURE_SEAL)
+ gensec_server.start_mech_by_sasl_name("GSSAPI")
+
+ client_finished = False
+ server_finished = False
+ server_to_client = b""
+
+ # Run the actual call loop.
+ while client_finished == False and server_finished == False:
+ if not client_finished:
+ print("running client gensec_update")
+ (client_finished, client_to_server) = gensec_client.update(server_to_client)
+ if not server_finished:
+ print("running server gensec_update")
+ (server_finished, server_to_client) = gensec_server.update(client_to_server)
+
+ session = gensec_server.session_info()
+
+ token = session.security_token
+ pac_sids = []
+ for s in token.sids:
+ pac_sids.append(str(s))
+
+ sidset1 = set(pac_sids)
+ sidset2 = set(self.user_sids)
+ if len(sidset1.symmetric_difference(sidset2)):
+ print("token sids don't match")
+ print("pac sids: %s" % pac_sids)
+ print("user sids : %s" % self.user_sids)
+ print("difference : %s" % sidset1.symmetric_difference(sidset2))
+ self.fail(msg="calculated groups don't match against user PAC tokenGroups")
+
+ def test_tokenGroups_manual(self):
+ # Manually run the tokenGroups algorithm from MS-ADTS 3.1.1.4.5.19 and MS-DRSR 4.1.8.3
+ # and compare the result
+ res = self.admin_ldb.search(base=self.base_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(|(objectclass=user)(objectclass=group))",
+ attrs=["memberOf"])
+ aSet = set()
+ aSetR = set()
+ vSet = set()
+ for obj in res:
+ if "memberOf" in obj:
+ for dn in obj["memberOf"]:
+ first = obj.dn.get_casefold()
+ second = ldb.Dn(self.admin_ldb, dn.decode('utf8')).get_casefold()
+ aSet.add((first, second))
+ aSetR.add((second, first))
+ vSet.add(first)
+ vSet.add(second)
+
+ res = self.admin_ldb.search(base=self.base_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(objectclass=user)",
+ attrs=["primaryGroupID"])
+ for obj in res:
+ if "primaryGroupID" in obj:
+ sid = "%s-%d" % (self.admin_ldb.get_domain_sid(), int(obj["primaryGroupID"][0]))
+ res2 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ first = obj.dn.get_casefold()
+ second = res2[0].dn.get_casefold()
+
+ aSet.add((first, second))
+ aSetR.add((second, first))
+ vSet.add(first)
+ vSet.add(second)
+
+ wSet = set()
+ wSet.add(self.test_user_dn.get_casefold())
+ closure(vSet, wSet, aSet)
+ wSet.remove(self.test_user_dn.get_casefold())
+
+ tokenGroupsSet = set()
+
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ dn_tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ sid = ndr_unpack(samba.dcerpc.security.dom_sid, sid)
+ res3 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ tokenGroupsSet.add(res3[0].dn.get_casefold())
+
+ if len(wSet.difference(tokenGroupsSet)):
+ self.fail(msg="additional calculated: %s" % wSet.difference(tokenGroupsSet))
+
+ if len(tokenGroupsSet.difference(wSet)):
+ self.fail(msg="additional tokenGroups: %s" % tokenGroupsSet.difference(wSet))
+
+ def filtered_closure(self, wSet, filter_grouptype):
+ res = self.admin_ldb.search(base=self.base_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(|(objectclass=user)(objectclass=group))",
+ attrs=["memberOf"])
+ aSet = set()
+ aSetR = set()
+ vSet = set()
+ for obj in res:
+ vSet.add(obj.dn.get_casefold())
+ if "memberOf" in obj:
+ for dn in obj["memberOf"]:
+ first = obj.dn.get_casefold()
+ second = ldb.Dn(self.admin_ldb, dn.decode('utf8')).get_casefold()
+ aSet.add((first, second))
+ aSetR.add((second, first))
+ vSet.add(first)
+ vSet.add(second)
+
+ res = self.admin_ldb.search(base=self.base_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(objectclass=user)",
+ attrs=["primaryGroupID"])
+ for obj in res:
+ if "primaryGroupID" in obj:
+ sid = "%s-%d" % (self.admin_ldb.get_domain_sid(), int(obj["primaryGroupID"][0]))
+ res2 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ first = obj.dn.get_casefold()
+ second = res2[0].dn.get_casefold()
+
+ aSet.add((first, second))
+ aSetR.add((second, first))
+ vSet.add(first)
+ vSet.add(second)
+
+ uSet = set()
+ for v in vSet:
+ res_group = self.admin_ldb.search(base=v, scope=ldb.SCOPE_BASE,
+ attrs=["groupType"],
+ expression="objectClass=group")
+ if len(res_group) == 1:
+ if hex(int(res_group[0]["groupType"][0]) & 0x00000000FFFFFFFF) == hex(filter_grouptype):
+ uSet.add(v)
+ else:
+ uSet.add(v)
+
+ closure(uSet, wSet, aSet)
+
+ def test_tokenGroupsGlobalAndUniversal_manual(self):
+ # Manually run the tokenGroups algorithm from MS-ADTS 3.1.1.4.5.19 and MS-DRSR 4.1.8.3
+ # and compare the result
+
+ # The variable names come from MS-ADTS May 15, 2014
+
+ S = set()
+ S.add(self.test_user_dn.get_casefold())
+
+ self.filtered_closure(S, GTYPE_SECURITY_GLOBAL_GROUP)
+
+ T = set()
+ # Not really a SID, we do this on DNs...
+ for sid in S:
+ X = set()
+ X.add(sid)
+ self.filtered_closure(X, GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ T = T.union(X)
+
+ T.remove(self.test_user_dn.get_casefold())
+
+ tokenGroupsSet = set()
+
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroupsGlobalAndUniversal"])
+ self.assertEqual(len(res), 1)
+
+ dn_tokengroups = []
+ for sid in res[0]['tokenGroupsGlobalAndUniversal']:
+ sid = ndr_unpack(samba.dcerpc.security.dom_sid, sid)
+ res3 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ tokenGroupsSet.add(res3[0].dn.get_casefold())
+
+ if len(T.difference(tokenGroupsSet)):
+ self.fail(msg="additional calculated: %s" % T.difference(tokenGroupsSet))
+
+ if len(tokenGroupsSet.difference(T)):
+ self.fail(msg="additional tokenGroupsGlobalAndUniversal: %s" % tokenGroupsSet.difference(T))
+
+ def test_samr_GetGroupsForUser(self):
+ # Confirm that we get the correct results against SAMR also
+ if not url.startswith("ldap://"):
+ self.fail(msg="This test is only valid on ldap (so we an find the hostname and use SAMR)")
+ host = url.split("://")[1]
+ (domain_sid, user_rid) = self.user_sid.split()
+ samr_conn = samba.dcerpc.samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, creds)
+ samr_handle = samr_conn.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ samr_domain = samr_conn.OpenDomain(samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED,
+ domain_sid)
+ user_handle = samr_conn.OpenUser(samr_domain, security.SEC_FLAG_MAXIMUM_ALLOWED, user_rid)
+ rids = samr_conn.GetGroupsForUser(user_handle)
+ samr_dns = set()
+ for rid in rids.rids:
+ self.assertEqual(rid.attributes, security.SE_GROUP_MANDATORY | security.SE_GROUP_ENABLED_BY_DEFAULT | security.SE_GROUP_ENABLED)
+ sid = "%s-%d" % (domain_sid, rid.rid)
+ res = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ samr_dns.add(res[0].dn.get_casefold())
+
+ user_info = samr_conn.QueryUserInfo(user_handle, 1)
+ self.assertEqual(rids.rids[0].rid, user_info.primary_gid)
+
+ tokenGroupsSet = set()
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroupsGlobalAndUniversal"])
+ for sid in res[0]['tokenGroupsGlobalAndUniversal']:
+ sid = ndr_unpack(samba.dcerpc.security.dom_sid, sid)
+ res3 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[],
+ expression="(&(|(grouptype=%d)(grouptype=%d))(objectclass=group))"
+ % (GTYPE_SECURITY_GLOBAL_GROUP, GTYPE_SECURITY_UNIVERSAL_GROUP))
+ if len(res) == 1:
+ tokenGroupsSet.add(res3[0].dn.get_casefold())
+
+ if len(samr_dns.difference(tokenGroupsSet)):
+ self.fail(msg="additional samr_GetUserGroups over tokenGroups: %s" % samr_dns.difference(tokenGroupsSet))
+
+ memberOf = set()
+ # Add the primary group
+ primary_group_sid = "%s-%d" % (domain_sid, user_info.primary_gid)
+ res2 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+
+ memberOf.add(res2[0].dn.get_casefold())
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["memberOf"])
+ for dn in res[0]['memberOf']:
+ res3 = self.admin_ldb.search(base=dn, scope=ldb.SCOPE_BASE,
+ attrs=[],
+ expression="(&(|(grouptype=%d)(grouptype=%d))(objectclass=group))"
+ % (GTYPE_SECURITY_GLOBAL_GROUP, GTYPE_SECURITY_UNIVERSAL_GROUP))
+ if len(res3) == 1:
+ memberOf.add(res3[0].dn.get_casefold())
+
+ if len(memberOf.difference(samr_dns)):
+ self.fail(msg="additional memberOf over samr_GetUserGroups: %s" % memberOf.difference(samr_dns))
+
+ if len(samr_dns.difference(memberOf)):
+ self.fail(msg="additional samr_GetUserGroups over memberOf: %s" % samr_dns.difference(memberOf))
+
+ S = set()
+ S.add(self.test_user_dn.get_casefold())
+
+ self.filtered_closure(S, GTYPE_SECURITY_GLOBAL_GROUP)
+ self.filtered_closure(S, GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ # Now remove the user DN and primary group
+ S.remove(self.test_user_dn.get_casefold())
+
+ if len(samr_dns.difference(S)):
+ self.fail(msg="additional samr_GetUserGroups over filtered_closure: %s" % samr_dns.difference(S))
+
+ def test_samr_GetGroupsForUser_nomember(self):
+ # Confirm that we get the correct results against SAMR also
+ if not url.startswith("ldap://"):
+ self.fail(msg="This test is only valid on ldap (so we an find the hostname and use SAMR)")
+ host = url.split("://")[1]
+
+ test_user = "tokengroups_user2"
+ self.admin_ldb.newuser(test_user, self.test_user_pass)
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (test_user, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ user_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ (domain_sid, user_rid) = user_sid.split()
+ samr_conn = samba.dcerpc.samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, creds)
+ samr_handle = samr_conn.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ samr_domain = samr_conn.OpenDomain(samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED,
+ domain_sid)
+ user_handle = samr_conn.OpenUser(samr_domain, security.SEC_FLAG_MAXIMUM_ALLOWED, user_rid)
+ rids = samr_conn.GetGroupsForUser(user_handle)
+ user_info = samr_conn.QueryUserInfo(user_handle, 1)
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (test_user, "cn=users", self.base_dn))
+ self.assertEqual(len(rids.rids), 1)
+ self.assertEqual(rids.rids[0].rid, user_info.primary_gid)
+
+
+if "://" not in url:
+ if os.path.isfile(url):
+ url = "tdb://%s" % url
+ else:
+ url = "ldap://%s" % url
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/tombstone_reanimation.py b/source4/dsdb/tests/python/tombstone_reanimation.py
new file mode 100755
index 0000000..ce3555a
--- /dev/null
+++ b/source4/dsdb/tests/python/tombstone_reanimation.py
@@ -0,0 +1,956 @@
+#!/usr/bin/env python3
+#
+# Tombstone reanimation tests
+#
+# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2014
+# Copyright (C) Nadezhda Ivanova <nivanova@symas.com> 2014
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import unittest
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.ndr import ndr_unpack, ndr_print
+from samba.dcerpc import misc
+from samba.dcerpc import security
+from samba.dcerpc import drsblobs
+from samba.dcerpc.drsuapi import *
+from samba.tests.password_test import PasswordCommon
+from samba.common import get_string
+
+import samba.tests
+from ldb import (SCOPE_BASE, FLAG_MOD_ADD, FLAG_MOD_DELETE, FLAG_MOD_REPLACE, Dn, Message,
+ MessageElement, LdbError,
+ ERR_ATTRIBUTE_OR_VALUE_EXISTS, ERR_NO_SUCH_OBJECT, ERR_ENTRY_ALREADY_EXISTS,
+ ERR_OPERATIONS_ERROR, ERR_UNWILLING_TO_PERFORM)
+
+
+class RestoredObjectAttributesBaseTestCase(samba.tests.TestCase):
+ """ verify Samba restores required attributes when
+ user restores a Deleted object
+ """
+
+ def setUp(self):
+ super(RestoredObjectAttributesBaseTestCase, self).setUp()
+ self.samdb = samba.tests.connect_samdb_env("TEST_SERVER", "TEST_USERNAME", "TEST_PASSWORD")
+ self.base_dn = self.samdb.domain_dn()
+ self.schema_dn = self.samdb.get_schema_basedn().get_linearized()
+ self.configuration_dn = self.samdb.get_config_basedn().get_linearized()
+
+ # permit password changes during this test
+ PasswordCommon.allow_password_changes(self, self.samdb)
+
+ def tearDown(self):
+ super(RestoredObjectAttributesBaseTestCase, self).tearDown()
+
+ def GUID_string(self, guid):
+ return get_string(self.samdb.schema_format_value("objectGUID", guid))
+
+ def search_guid(self, guid, attrs=["*"]):
+ res = self.samdb.search(base="<GUID=%s>" % self.GUID_string(guid),
+ scope=SCOPE_BASE, attrs=attrs,
+ controls=["show_deleted:1"])
+ self.assertEqual(len(res), 1)
+ return res[0]
+
+ def search_dn(self, dn):
+ res = self.samdb.search(expression="(objectClass=*)",
+ base=dn,
+ scope=SCOPE_BASE,
+ controls=["show_recycled:1"])
+ self.assertEqual(len(res), 1)
+ return res[0]
+
+ def _create_object(self, msg):
+ """:param msg: dict with dn and attributes to create an object from"""
+ # delete an object if leftover from previous test
+ samba.tests.delete_force(self.samdb, msg['dn'])
+ self.samdb.add(msg)
+ return self.search_dn(msg['dn'])
+
+ def assertNamesEqual(self, attrs_expected, attrs_extra):
+ self.assertEqual(attrs_expected, attrs_extra,
+ "Actual object does not have expected attributes, missing from expected (%s), extra (%s)"
+ % (str(attrs_expected.difference(attrs_extra)), str(attrs_extra.difference(attrs_expected))))
+
+ def assertAttributesEqual(self, obj_orig, attrs_orig, obj_restored, attrs_rest):
+ self.assertNamesEqual(attrs_orig, attrs_rest)
+ # remove volatile attributes, they can't be equal
+ attrs_orig -= set(["uSNChanged", "dSCorePropagationData", "whenChanged"])
+ for attr in attrs_orig:
+ # convert original attr value to ldif
+ orig_val = obj_orig.get(attr)
+ if orig_val is None:
+ continue
+ if not isinstance(orig_val, MessageElement):
+ orig_val = MessageElement(str(orig_val), 0, attr)
+ m = Message()
+ m.add(orig_val)
+ orig_ldif = self.samdb.write_ldif(m, 0)
+ # convert restored attr value to ldif
+ rest_val = obj_restored.get(attr)
+ self.assertFalse(rest_val is None)
+ m = Message()
+ if not isinstance(rest_val, MessageElement):
+ rest_val = MessageElement(str(rest_val), 0, attr)
+ m.add(rest_val)
+ rest_ldif = self.samdb.write_ldif(m, 0)
+ # compare generated ldif's
+ self.assertEqual(orig_ldif, rest_ldif)
+
+ def assertAttributesExists(self, attr_expected, obj_msg):
+ """Check object contains at least expected attrbigutes
+ :param attr_expected: dict of expected attributes with values. ** is any value
+ :param obj_msg: Ldb.Message for the object under test
+ """
+ actual_names = set(obj_msg.keys())
+ # Samba does not use 'dSCorePropagationData', so skip it
+ actual_names -= set(['dSCorePropagationData'])
+ expected_names = set(attr_expected.keys())
+ self.assertNamesEqual(expected_names, actual_names)
+ for name in attr_expected.keys():
+ expected_val = attr_expected[name]
+ actual_val = obj_msg.get(name)
+ self.assertFalse(actual_val is None, "No value for attribute '%s'" % name)
+ if expected_val == "**":
+ # "**" values means "any"
+ continue
+ # if expected_val is e.g. ldb.bytes we can't depend on
+ # str(actual_value) working, we may just get a decoding
+ # error. Better to just compare raw values
+ if not isinstance(expected_val, str):
+ actual_val = actual_val[0]
+ else:
+ actual_val = str(actual_val)
+ self.assertEqual(expected_val, actual_val,
+ "Unexpected value (%s) for '%s', expected (%s)" % (
+ repr(actual_val), name, repr(expected_val)))
+
+ def _check_metadata(self, metadata, expected):
+ repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, metadata[0])
+
+ repl_array = []
+ for o in repl.ctr.array:
+ repl_array.append((o.attid, o.version))
+ repl_set = set(repl_array)
+
+ expected_set = set(expected)
+ self.assertEqual(len(repl_set), len(expected),
+ "Unexpected metadata, missing from expected (%s), extra (%s)), repl: \n%s" % (
+ str(expected_set.difference(repl_set)),
+ str(repl_set.difference(expected_set)),
+ ndr_print(repl)))
+
+ i = 0
+ for o in repl.ctr.array:
+ e = expected[i]
+ (attid, version) = e
+ self.assertEqual(attid, o.attid,
+ "(LDAP) Wrong attid "
+ "for expected value %d, wanted 0x%08x got 0x%08x, "
+ "repl: \n%s"
+ % (i, attid, o.attid, ndr_print(repl)))
+ # Allow version to be skipped when it does not matter
+ if version is not None:
+ self.assertEqual(o.version, version,
+ "(LDAP) Wrong version for expected value %d, "
+ "attid 0x%08x, "
+ "wanted %d got %d, repl: \n%s"
+ % (i, o.attid,
+ version, o.version, ndr_print(repl)))
+ i = i + 1
+
+ @staticmethod
+ def restore_deleted_object(samdb, del_dn, new_dn, new_attrs=None):
+ """Restores a deleted object
+ :param samdb: SamDB connection to SAM
+ :param del_dn: str Deleted object DN
+ :param new_dn: str Where to restore the object
+ :param new_attrs: dict Additional attributes to set
+ """
+ msg = Message()
+ msg.dn = Dn(samdb, str(del_dn))
+ msg["isDeleted"] = MessageElement([], FLAG_MOD_DELETE, "isDeleted")
+ msg["distinguishedName"] = MessageElement([str(new_dn)], FLAG_MOD_REPLACE, "distinguishedName")
+ if new_attrs is not None:
+ assert isinstance(new_attrs, dict)
+ for attr in new_attrs:
+ msg[attr] = MessageElement(new_attrs[attr], FLAG_MOD_REPLACE, attr)
+ samdb.modify(msg, ["show_deleted:1"])
+
+
+class BaseRestoreObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ def setUp(self):
+ super(BaseRestoreObjectTestCase, self).setUp()
+
+ def enable_recycle_bin(self):
+ msg = Message()
+ msg.dn = Dn(self.samdb, "")
+ msg["enableOptionalFeature"] = MessageElement(
+ "CN=Partitions," + self.configuration_dn + ":766ddcd8-acd0-445e-f3b9-a7f9b6744f2a",
+ FLAG_MOD_ADD, "enableOptionalFeature")
+ try:
+ self.samdb.modify(msg)
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ def test_undelete(self):
+ print("Testing standard undelete operation")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ samba.tests.delete_force(self.samdb, usr1)
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ objDeleted1 = self.search_guid(guid1)
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, usr1)
+ objLive2 = self.search_dn(usr1)
+ self.assertEqual(str(objLive2.dn).lower(), str(objLive1.dn).lower())
+ samba.tests.delete_force(self.samdb, usr1)
+
+ def test_rename(self):
+ print("Testing attempt to rename deleted object")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ objDeleted1 = self.search_guid(guid1)
+ # just to make sure we get the correct error if the show deleted is missing
+ try:
+ self.samdb.rename(str(objDeleted1.dn), usr1)
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ try:
+ self.samdb.rename(str(objDeleted1.dn), usr1, ["show_deleted:1"])
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ def test_undelete_with_mod(self):
+ print("Testing standard undelete operation with modification of additional attributes")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ objDeleted1 = self.search_guid(guid1)
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, usr1, {"url": "www.samba.org"})
+ objLive2 = self.search_dn(usr1)
+ self.assertEqual(str(objLive2["url"][0]), "www.samba.org")
+ samba.tests.delete_force(self.samdb, usr1)
+
+ def test_undelete_newuser(self):
+ print("Testing undelete user with a different dn")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ usr2 = "cn=testuser2,cn=users," + self.base_dn
+ samba.tests.delete_force(self.samdb, usr1)
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ objDeleted1 = self.search_guid(guid1)
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, usr2)
+ objLive2 = self.search_dn(usr2)
+ samba.tests.delete_force(self.samdb, usr1)
+ samba.tests.delete_force(self.samdb, usr2)
+
+ def test_undelete_existing(self):
+ print("Testing undelete user after a user with the same dn has been created")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objDeleted1 = self.search_guid(guid1)
+ try:
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, usr1)
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ def test_undelete_cross_nc(self):
+ print("Cross NC undelete")
+ c1 = "cn=ldaptestcontainer," + self.base_dn
+ c2 = "cn=ldaptestcontainer2," + self.configuration_dn
+ c3 = "cn=ldaptestcontainer," + self.configuration_dn
+ c4 = "cn=ldaptestcontainer2," + self.base_dn
+ samba.tests.delete_force(self.samdb, c1)
+ samba.tests.delete_force(self.samdb, c2)
+ samba.tests.delete_force(self.samdb, c3)
+ samba.tests.delete_force(self.samdb, c4)
+ self.samdb.add({
+ "dn": c1,
+ "objectclass": "container"})
+ self.samdb.add({
+ "dn": c2,
+ "objectclass": "container"})
+ objLive1 = self.search_dn(c1)
+ objLive2 = self.search_dn(c2)
+ guid1 = objLive1["objectGUID"][0]
+ guid2 = objLive2["objectGUID"][0]
+ self.samdb.delete(c1)
+ self.samdb.delete(c2)
+ objDeleted1 = self.search_guid(guid1)
+ objDeleted2 = self.search_guid(guid2)
+ # try to undelete from base dn to config
+ try:
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, c3)
+ self.fail()
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ # try to undelete from config to base dn
+ try:
+ self.restore_deleted_object(self.samdb, objDeleted2.dn, c4)
+ self.fail()
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ # assert undeletion will work in same nc
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, c4)
+ self.restore_deleted_object(self.samdb, objDeleted2.dn, c3)
+
+
+class RestoreUserObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ """Test cases for delete/reanimate user objects"""
+
+ def _expected_user_add_attributes(self, username, user_dn, category):
+ return {'dn': user_dn,
+ 'objectClass': '**',
+ 'cn': username,
+ 'distinguishedName': user_dn,
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': username,
+ 'objectGUID': '**',
+ 'userAccountControl': '546',
+ 'badPwdCount': '0',
+ 'badPasswordTime': '0',
+ 'codePage': '0',
+ 'countryCode': '0',
+ 'lastLogon': '0',
+ 'lastLogoff': '0',
+ 'pwdLastSet': '0',
+ 'primaryGroupID': '513',
+ 'objectSid': '**',
+ 'accountExpires': '9223372036854775807',
+ 'logonCount': '0',
+ 'sAMAccountName': username,
+ 'sAMAccountType': '805306368',
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn)
+ }
+
+ def _expected_user_add_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 1),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 1),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 1),
+ (DRSUAPI_ATTID_countryCode, 1),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 1),
+ (DRSUAPI_ATTID_ntPwdHistory, 1),
+ (DRSUAPI_ATTID_pwdLastSet, 1),
+ (DRSUAPI_ATTID_primaryGroupID, 1),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_accountExpires, 1),
+ (DRSUAPI_ATTID_lmPwdHistory, 1),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 1),
+ (DRSUAPI_ATTID_objectCategory, 1)]
+
+ def _expected_user_del_attributes(self, username, _guid, _sid):
+ guid = ndr_unpack(misc.GUID, _guid)
+ dn = "CN=%s\\0ADEL:%s,CN=Deleted Objects,%s" % (username, guid, self.base_dn)
+ cn = "%s\nDEL:%s" % (username, guid)
+ return {'dn': dn,
+ 'objectClass': '**',
+ 'cn': cn,
+ 'distinguishedName': dn,
+ 'isDeleted': 'TRUE',
+ 'isRecycled': 'TRUE',
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': cn,
+ 'objectGUID': _guid,
+ 'userAccountControl': '546',
+ 'objectSid': _sid,
+ 'sAMAccountName': username,
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ }
+
+ def _expected_user_del_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 2),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_isDeleted, 1),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 2),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 2),
+ (DRSUAPI_ATTID_countryCode, 2),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 1),
+ (DRSUAPI_ATTID_ntPwdHistory, 1),
+ (DRSUAPI_ATTID_pwdLastSet, 2),
+ (DRSUAPI_ATTID_primaryGroupID, 2),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_accountExpires, 2),
+ (DRSUAPI_ATTID_lmPwdHistory, 1),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 2),
+ (DRSUAPI_ATTID_lastKnownParent, 1),
+ (DRSUAPI_ATTID_objectCategory, 2),
+ (DRSUAPI_ATTID_isRecycled, 1)]
+
+ def _expected_user_restore_attributes(self, username, guid, sid, user_dn, category):
+ return {'dn': user_dn,
+ 'objectClass': '**',
+ 'cn': username,
+ 'distinguishedName': user_dn,
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': username,
+ 'objectGUID': guid,
+ 'userAccountControl': '546',
+ 'badPwdCount': '0',
+ 'badPasswordTime': '0',
+ 'codePage': '0',
+ 'countryCode': '0',
+ 'lastLogon': '0',
+ 'lastLogoff': '0',
+ 'pwdLastSet': '0',
+ 'primaryGroupID': '513',
+ 'operatorCount': '0',
+ 'objectSid': sid,
+ 'adminCount': '0',
+ 'accountExpires': '0',
+ 'logonCount': '0',
+ 'sAMAccountName': username,
+ 'sAMAccountType': '805306368',
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn)
+ }
+
+ def _expected_user_restore_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 3),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_isDeleted, 2),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 3),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 3),
+ (DRSUAPI_ATTID_countryCode, 3),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 1),
+ (DRSUAPI_ATTID_ntPwdHistory, 1),
+ (DRSUAPI_ATTID_pwdLastSet, 3),
+ (DRSUAPI_ATTID_primaryGroupID, 3),
+ (DRSUAPI_ATTID_operatorCount, 1),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_adminCount, 1),
+ (DRSUAPI_ATTID_accountExpires, 3),
+ (DRSUAPI_ATTID_lmPwdHistory, 1),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 3),
+ (DRSUAPI_ATTID_lastKnownParent, 1),
+ (DRSUAPI_ATTID_objectCategory, 3),
+ (DRSUAPI_ATTID_isRecycled, 2)]
+
+ def test_restore_user(self):
+ print("Test restored user attributes")
+ username = "restore_user"
+ usr_dn = "CN=%s,CN=Users,%s" % (username, self.base_dn)
+ samba.tests.delete_force(self.samdb, usr_dn)
+ self.samdb.add({
+ "dn": usr_dn,
+ "objectClass": "user",
+ "sAMAccountName": username})
+ obj = self.search_dn(usr_dn)
+ guid = obj["objectGUID"][0]
+ sid = obj["objectSID"][0]
+ obj_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ self.assertAttributesExists(self._expected_user_add_attributes(username, usr_dn, "Person"), obj)
+ self._check_metadata(obj_rmd["replPropertyMetaData"],
+ self._expected_user_add_metadata())
+ self.samdb.delete(usr_dn)
+ obj_del = self.search_guid(guid)
+ obj_del_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ orig_attrs = set(obj.keys())
+ del_attrs = set(obj_del.keys())
+ self.assertAttributesExists(self._expected_user_del_attributes(username, guid, sid), obj_del)
+ self._check_metadata(obj_del_rmd["replPropertyMetaData"],
+ self._expected_user_del_metadata())
+ # restore the user and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, usr_dn)
+ obj_restore = self.search_guid(guid)
+ obj_restore_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ # check original attributes and restored one are same
+ orig_attrs = set(obj.keys())
+ # windows restore more attributes that originally we have
+ orig_attrs.update(['adminCount', 'operatorCount', 'lastKnownParent'])
+ rest_attrs = set(obj_restore.keys())
+ self.assertAttributesExists(self._expected_user_restore_attributes(username, guid, sid, usr_dn, "Person"), obj_restore)
+ self._check_metadata(obj_restore_rmd["replPropertyMetaData"],
+ self._expected_user_restore_metadata())
+
+
+class RestoreUserPwdObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ """Test cases for delete/reanimate user objects with password"""
+
+ def _expected_userpw_add_attributes(self, username, user_dn, category):
+ return {'dn': user_dn,
+ 'objectClass': '**',
+ 'cn': username,
+ 'distinguishedName': user_dn,
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': username,
+ 'objectGUID': '**',
+ 'userAccountControl': '546',
+ 'badPwdCount': '0',
+ 'badPasswordTime': '0',
+ 'codePage': '0',
+ 'countryCode': '0',
+ 'lastLogon': '0',
+ 'lastLogoff': '0',
+ 'pwdLastSet': '**',
+ 'primaryGroupID': '513',
+ 'objectSid': '**',
+ 'accountExpires': '9223372036854775807',
+ 'logonCount': '0',
+ 'sAMAccountName': username,
+ 'sAMAccountType': '805306368',
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn)
+ }
+
+ def _expected_userpw_add_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 1),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 1),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 1),
+ (DRSUAPI_ATTID_countryCode, 1),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 1),
+ (DRSUAPI_ATTID_ntPwdHistory, 1),
+ (DRSUAPI_ATTID_pwdLastSet, 1),
+ (DRSUAPI_ATTID_primaryGroupID, 1),
+ (DRSUAPI_ATTID_supplementalCredentials, 1),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_accountExpires, 1),
+ (DRSUAPI_ATTID_lmPwdHistory, 1),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 1),
+ (DRSUAPI_ATTID_objectCategory, 1)]
+
+ def _expected_userpw_del_attributes(self, username, _guid, _sid):
+ guid = ndr_unpack(misc.GUID, _guid)
+ dn = "CN=%s\\0ADEL:%s,CN=Deleted Objects,%s" % (username, guid, self.base_dn)
+ cn = "%s\nDEL:%s" % (username, guid)
+ return {'dn': dn,
+ 'objectClass': '**',
+ 'cn': cn,
+ 'distinguishedName': dn,
+ 'isDeleted': 'TRUE',
+ 'isRecycled': 'TRUE',
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': cn,
+ 'objectGUID': _guid,
+ 'userAccountControl': '546',
+ 'objectSid': _sid,
+ 'sAMAccountName': username,
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ }
+
+ def _expected_userpw_del_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 2),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_isDeleted, 1),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 2),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 2),
+ (DRSUAPI_ATTID_countryCode, 2),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 2),
+ (DRSUAPI_ATTID_ntPwdHistory, 2),
+ (DRSUAPI_ATTID_pwdLastSet, 2),
+ (DRSUAPI_ATTID_primaryGroupID, 2),
+ (DRSUAPI_ATTID_supplementalCredentials, 2),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_accountExpires, 2),
+ (DRSUAPI_ATTID_lmPwdHistory, None),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 2),
+ (DRSUAPI_ATTID_lastKnownParent, 1),
+ (DRSUAPI_ATTID_objectCategory, 2),
+ (DRSUAPI_ATTID_isRecycled, 1)]
+
+ def _expected_userpw_restore_attributes(self, username, guid, sid, user_dn, category):
+ return {'dn': user_dn,
+ 'objectClass': '**',
+ 'cn': username,
+ 'distinguishedName': user_dn,
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': username,
+ 'objectGUID': guid,
+ 'userAccountControl': '546',
+ 'badPwdCount': '0',
+ 'badPasswordTime': '0',
+ 'codePage': '0',
+ 'countryCode': '0',
+ 'lastLogon': '0',
+ 'lastLogoff': '0',
+ 'pwdLastSet': '**',
+ 'primaryGroupID': '513',
+ 'operatorCount': '0',
+ 'objectSid': sid,
+ 'adminCount': '0',
+ 'accountExpires': '0',
+ 'logonCount': '0',
+ 'sAMAccountName': username,
+ 'sAMAccountType': '805306368',
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn)
+ }
+
+ def _expected_userpw_restore_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 3),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_isDeleted, 2),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 3),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 3),
+ (DRSUAPI_ATTID_countryCode, 3),
+ (DRSUAPI_ATTID_dBCSPwd, 2),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 3),
+ (DRSUAPI_ATTID_ntPwdHistory, 3),
+ (DRSUAPI_ATTID_pwdLastSet, 4),
+ (DRSUAPI_ATTID_primaryGroupID, 3),
+ (DRSUAPI_ATTID_supplementalCredentials, 3),
+ (DRSUAPI_ATTID_operatorCount, 1),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_adminCount, 1),
+ (DRSUAPI_ATTID_accountExpires, 3),
+ (DRSUAPI_ATTID_lmPwdHistory, None),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 3),
+ (DRSUAPI_ATTID_lastKnownParent, 1),
+ (DRSUAPI_ATTID_objectCategory, 3),
+ (DRSUAPI_ATTID_isRecycled, 2)]
+
+ def test_restorepw_user(self):
+ print("Test restored user attributes")
+ username = "restorepw_user"
+ usr_dn = "CN=%s,CN=Users,%s" % (username, self.base_dn)
+ samba.tests.delete_force(self.samdb, usr_dn)
+ self.samdb.add({
+ "dn": usr_dn,
+ "objectClass": "user",
+ "userPassword": "thatsAcomplPASS0",
+ "sAMAccountName": username})
+ obj = self.search_dn(usr_dn)
+ guid = obj["objectGUID"][0]
+ sid = obj["objectSID"][0]
+ obj_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ self.assertAttributesExists(self._expected_userpw_add_attributes(username, usr_dn, "Person"), obj)
+ self._check_metadata(obj_rmd["replPropertyMetaData"],
+ self._expected_userpw_add_metadata())
+ self.samdb.delete(usr_dn)
+ obj_del = self.search_guid(guid)
+ obj_del_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ orig_attrs = set(obj.keys())
+ del_attrs = set(obj_del.keys())
+ self.assertAttributesExists(self._expected_userpw_del_attributes(username, guid, sid), obj_del)
+ self._check_metadata(obj_del_rmd["replPropertyMetaData"],
+ self._expected_userpw_del_metadata())
+ # restore the user and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, usr_dn, {"userPassword": ["thatsAcomplPASS1"]})
+ obj_restore = self.search_guid(guid)
+ obj_restore_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ # check original attributes and restored one are same
+ orig_attrs = set(obj.keys())
+ # windows restore more attributes that originally we have
+ orig_attrs.update(['adminCount', 'operatorCount', 'lastKnownParent'])
+ rest_attrs = set(obj_restore.keys())
+ self.assertAttributesExists(self._expected_userpw_restore_attributes(username, guid, sid, usr_dn, "Person"), obj_restore)
+ self._check_metadata(obj_restore_rmd["replPropertyMetaData"],
+ self._expected_userpw_restore_metadata())
+
+
+class RestoreGroupObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ """Test different scenarios for delete/reanimate group objects"""
+
+ def _make_object_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def _create_test_user(self, user_name):
+ user_dn = self._make_object_dn(user_name)
+ ldif = {
+ "dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name,
+ }
+ # delete an object if leftover from previous test
+ samba.tests.delete_force(self.samdb, user_dn)
+ # finally, create the group
+ self.samdb.add(ldif)
+ return self.search_dn(user_dn)
+
+ def _create_test_group(self, group_name, members=None):
+ group_dn = self._make_object_dn(group_name)
+ ldif = {
+ "dn": group_dn,
+ "objectClass": "group",
+ "sAMAccountName": group_name,
+ }
+ try:
+ ldif["member"] = [str(usr_dn) for usr_dn in members]
+ except TypeError:
+ pass
+ # delete an object if leftover from previous test
+ samba.tests.delete_force(self.samdb, group_dn)
+ # finally, create the group
+ self.samdb.add(ldif)
+ return self.search_dn(group_dn)
+
+ def _expected_group_attributes(self, groupname, group_dn, category):
+ return {'dn': group_dn,
+ 'groupType': '-2147483646',
+ 'distinguishedName': group_dn,
+ 'sAMAccountName': groupname,
+ 'name': groupname,
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn),
+ 'objectClass': '**',
+ 'objectGUID': '**',
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ 'whenChanged': '**',
+ 'sAMAccountType': '268435456',
+ 'objectSid': '**',
+ 'whenCreated': '**',
+ 'uSNCreated': '**',
+ 'operatorCount': '0',
+ 'uSNChanged': '**',
+ 'instanceType': '4',
+ 'adminCount': '0',
+ 'cn': groupname}
+
+ def test_plain_group(self):
+ print("Test restored Group attributes")
+ # create test group
+ obj = self._create_test_group("r_group")
+ guid = obj["objectGUID"][0]
+ # delete the group
+ self.samdb.delete(str(obj.dn))
+ obj_del = self.search_guid(guid)
+ # restore the Group and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, obj.dn)
+ obj_restore = self.search_guid(guid)
+ # check original attributes and restored one are same
+ attr_orig = set(obj.keys())
+ # windows restore more attributes that originally we have
+ attr_orig.update(['adminCount', 'operatorCount', 'lastKnownParent'])
+ attr_rest = set(obj_restore.keys())
+ self.assertAttributesEqual(obj, attr_orig, obj_restore, attr_rest)
+ self.assertAttributesExists(self._expected_group_attributes("r_group", str(obj.dn), "Group"), obj_restore)
+
+ def test_group_with_members(self):
+ print("Test restored Group with members attributes")
+ # create test group
+ usr1 = self._create_test_user("r_user_1")
+ usr2 = self._create_test_user("r_user_2")
+ obj = self._create_test_group("r_group", [usr1.dn, usr2.dn])
+ guid = obj["objectGUID"][0]
+ # delete the group
+ self.samdb.delete(str(obj.dn))
+ obj_del = self.search_guid(guid)
+ # restore the Group and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, obj.dn)
+ obj_restore = self.search_guid(guid)
+ # check original attributes and restored one are same
+ attr_orig = set(obj.keys())
+ # windows restore more attributes that originally we have
+ attr_orig.update(['adminCount', 'operatorCount', 'lastKnownParent'])
+ # and does not restore following attributes
+ attr_orig.remove("member")
+ attr_rest = set(obj_restore.keys())
+ self.assertAttributesEqual(obj, attr_orig, obj_restore, attr_rest)
+ self.assertAttributesExists(self._expected_group_attributes("r_group", str(obj.dn), "Group"), obj_restore)
+
+
+class RestoreContainerObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ """Test different scenarios for delete/reanimate OU/container objects"""
+
+ def _expected_container_attributes(self, rdn, name, dn, category):
+ if rdn == 'OU':
+ lastKnownParent = '%s' % self.base_dn
+ else:
+ lastKnownParent = 'CN=Users,%s' % self.base_dn
+ return {'dn': dn,
+ 'distinguishedName': dn,
+ 'name': name,
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn),
+ 'objectClass': '**',
+ 'objectGUID': '**',
+ 'lastKnownParent': lastKnownParent,
+ 'whenChanged': '**',
+ 'whenCreated': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'instanceType': '4',
+ rdn.lower(): name}
+
+ def _create_test_ou(self, rdn, name=None, description=None):
+ ou_dn = "OU=%s,%s" % (rdn, self.base_dn)
+ # delete an object if leftover from previous test
+ samba.tests.delete_force(self.samdb, ou_dn)
+ # create ou and return created object
+ self.samdb.create_ou(ou_dn, name=name, description=description)
+ return self.search_dn(ou_dn)
+
+ def test_ou_with_name_description(self):
+ print("Test OU reanimation")
+ # create OU to test with
+ obj = self._create_test_ou(rdn="r_ou",
+ name="r_ou name",
+ description="r_ou description")
+ guid = obj["objectGUID"][0]
+ # delete the object
+ self.samdb.delete(str(obj.dn))
+ obj_del = self.search_guid(guid)
+ # restore the Object and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, obj.dn)
+ obj_restore = self.search_guid(guid)
+ # check original attributes and restored one are same
+ attr_orig = set(obj.keys())
+ attr_rest = set(obj_restore.keys())
+ # windows restore more attributes that originally we have
+ attr_orig.update(["lastKnownParent"])
+ # and does not restore following attributes
+ attr_orig -= set(["description"])
+ self.assertAttributesEqual(obj, attr_orig, obj_restore, attr_rest)
+ expected_attrs = self._expected_container_attributes("OU", "r_ou", str(obj.dn), "Organizational-Unit")
+ self.assertAttributesExists(expected_attrs, obj_restore)
+
+ def test_container(self):
+ print("Test Container reanimation")
+ # create test Container
+ obj = self._create_object({
+ "dn": "CN=r_container,CN=Users,%s" % self.base_dn,
+ "objectClass": "container"
+ })
+ guid = obj["objectGUID"][0]
+ # delete the object
+ self.samdb.delete(str(obj.dn))
+ obj_del = self.search_guid(guid)
+ # restore the Object and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, obj.dn)
+ obj_restore = self.search_guid(guid)
+ # check original attributes and restored one are same
+ attr_orig = set(obj.keys())
+ attr_rest = set(obj_restore.keys())
+ # windows restore more attributes that originally we have
+ attr_orig.update(["lastKnownParent"])
+ # and does not restore following attributes
+ attr_orig -= set(["showInAdvancedViewOnly"])
+ self.assertAttributesEqual(obj, attr_orig, obj_restore, attr_rest)
+ expected_attrs = self._expected_container_attributes("CN", "r_container",
+ str(obj.dn), "Container")
+ self.assertAttributesExists(expected_attrs, obj_restore)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/source4/dsdb/tests/python/urgent_replication.py b/source4/dsdb/tests/python/urgent_replication.py
new file mode 100755
index 0000000..485b1fd
--- /dev/null
+++ b/source4/dsdb/tests/python/urgent_replication.py
@@ -0,0 +1,339 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+from ldb import (LdbError, ERR_NO_SUCH_OBJECT, Message,
+ MessageElement, Dn, FLAG_MOD_REPLACE)
+import samba.tests
+import samba.dsdb as dsdb
+import samba.getopt as options
+import random
+
+parser = optparse.OptionParser("urgent_replication.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]
+
+
+class UrgentReplicationTests(samba.tests.TestCase):
+
+ def delete_force(self, ldb, dn):
+ try:
+ ldb.delete(dn, ["relax:0"])
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ def setUp(self):
+ super(UrgentReplicationTests, self).setUp()
+ self.ldb = samba.tests.connect_samdb(host, global_schema=False)
+ self.base_dn = self.ldb.domain_dn()
+
+ print("baseDN: %s\n" % self.base_dn)
+
+ def test_nonurgent_object(self):
+ """Test if the urgent replication is not activated when handling a non urgent object."""
+ self.ldb.add({
+ "dn": "cn=nonurgenttest,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "samaccountname": "nonurgenttest",
+ "description": "nonurgenttest description"})
+
+ # urgent replication should not be enabled when creating
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should not be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=nonurgenttest,cn=users," + self.base_dn)
+ m["description"] = MessageElement("new description", FLAG_MOD_REPLACE,
+ "description")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should not be enabled when deleting
+ self.delete_force(self.ldb, "cn=nonurgenttest,cn=users," + self.base_dn)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_nTDSDSA_object(self):
+ """Test if the urgent replication is activated when handling a nTDSDSA object."""
+ self.ldb.add({
+ "dn": "cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,%s" %
+ self.ldb.get_config_basedn(),
+ "objectclass": "server",
+ "cn": "test server",
+ "name": "test server",
+ "systemFlags": "50000000"}, ["relax:0"])
+
+ self.ldb.add_ldif(
+ """dn: cn=NTDS Settings test,cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,cn=Configuration,%s""" % (self.base_dn) + """
+objectclass: nTDSDSA
+cn: NTDS Settings test
+options: 1
+instanceType: 4
+systemFlags: 33554432""", ["relax:0"])
+
+ # urgent replication should be enabled when creation
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=NTDS Settings test,cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,cn=Configuration," + self.base_dn)
+ m["options"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "options")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when deleting
+ self.delete_force(self.ldb, "cn=NTDS Settings test,cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,cn=Configuration," + self.base_dn)
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ self.delete_force(self.ldb, "cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,cn=Configuration," + self.base_dn)
+
+ def test_crossRef_object(self):
+ """Test if the urgent replication is activated when handling a crossRef object."""
+ self.ldb.add({
+ "dn": "CN=test crossRef,CN=Partitions,CN=Configuration," + self.base_dn,
+ "objectClass": "crossRef",
+ "cn": "test crossRef",
+ "dnsRoot": self.get_loadparm().get("realm").lower(),
+ "instanceType": "4",
+ "nCName": self.base_dn,
+ "showInAdvancedViewOnly": "TRUE",
+ "name": "test crossRef",
+ "systemFlags": "1"}, ["relax:0"])
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=test crossRef,CN=Partitions,CN=Configuration," + self.base_dn)
+ m["systemFlags"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "systemFlags")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when deleting
+ self.delete_force(self.ldb, "cn=test crossRef,CN=Partitions,CN=Configuration," + self.base_dn)
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_attributeSchema_object(self):
+ """Test if the urgent replication is activated when handling an attributeSchema object"""
+
+ self.ldb.add_ldif(
+ """dn: CN=test attributeSchema,cn=Schema,CN=Configuration,%s""" % self.base_dn + """
+objectClass: attributeSchema
+cn: test attributeSchema
+instanceType: 4
+isSingleValued: FALSE
+showInAdvancedViewOnly: FALSE
+attributeID: 1.3.6.1.4.1.7165.4.6.1.4.""" + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.12
+adminDisplayName: test attributeSchema
+adminDescription: test attributeSchema
+oMSyntax: 64
+systemOnly: FALSE
+searchFlags: 8
+lDAPDisplayName: testAttributeSchema
+name: test attributeSchema""")
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn("cn=Schema,cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=test attributeSchema,CN=Schema,CN=Configuration," + self.base_dn)
+ m["lDAPDisplayName"] = MessageElement("updatedTestAttributeSchema", FLAG_MOD_REPLACE,
+ "lDAPDisplayName")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn("cn=Schema,cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_classSchema_object(self):
+ """Test if the urgent replication is activated when handling a classSchema object."""
+ try:
+ self.ldb.add_ldif(
+ """dn: CN=test classSchema,CN=Schema,CN=Configuration,%s""" % self.base_dn + """
+objectClass: classSchema
+cn: test classSchema
+instanceType: 4
+subClassOf: top
+governsId: 1.3.6.1.4.1.7165.4.6.2.4.""" + str(random.randint(1, 100000)) + """
+rDNAttID: cn
+showInAdvancedViewOnly: TRUE
+adminDisplayName: test classSchema
+adminDescription: test classSchema
+objectClassCategory: 1
+lDAPDisplayName: testClassSchema
+name: test classSchema
+systemOnly: FALSE
+systemPossSuperiors: dfsConfiguration
+systemMustContain: msDFS-SchemaMajorVersion
+defaultSecurityDescriptor: D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)(A;;RPWPCRCCD
+ CLCLORCWOWDSDDTSW;;;SY)(A;;RPLCLORC;;;AU)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;CO)
+systemFlags: 16
+defaultHidingValue: TRUE""")
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn("cn=Schema,cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ except LdbError:
+ print("Not testing urgent replication when creating classSchema object ...\n")
+
+ # urgent replication should be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=test classSchema,CN=Schema,CN=Configuration," + self.base_dn)
+ m["lDAPDisplayName"] = MessageElement("updatedTestClassSchema", FLAG_MOD_REPLACE,
+ "lDAPDisplayName")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn("cn=Schema,cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_secret_object(self):
+ """Test if the urgent replication is activated when handling a secret object."""
+
+ self.ldb.add({
+ "dn": "cn=test secret,cn=System," + self.base_dn,
+ "objectClass": "secret",
+ "cn": "test secret",
+ "name": "test secret",
+ "currentValue": "xxxxxxx"})
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=test secret,cn=System," + self.base_dn)
+ m["currentValue"] = MessageElement("yyyyyyyy", FLAG_MOD_REPLACE,
+ "currentValue")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when deleting
+ self.delete_force(self.ldb, "cn=test secret,cn=System," + self.base_dn)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_rIDManager_object(self):
+ """Test if the urgent replication is activated when handling a rIDManager object."""
+ self.ldb.add_ldif(
+ """dn: CN=RID Manager test,CN=System,%s""" % self.base_dn + """
+objectClass: rIDManager
+cn: RID Manager test
+instanceType: 4
+showInAdvancedViewOnly: TRUE
+name: RID Manager test
+systemFlags: -1946157056
+isCriticalSystemObject: TRUE
+rIDAvailablePool: 133001-1073741823""", ["relax:0"])
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=RID Manager test,CN=System," + self.base_dn)
+ m["systemFlags"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "systemFlags")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when deleting
+ self.delete_force(self.ldb, "CN=RID Manager test,CN=System," + self.base_dn)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_urgent_attributes(self):
+ """Test if the urgent replication is activated when handling urgent attributes of an object."""
+
+ self.ldb.add({
+ "dn": "cn=user UrgAttr test,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "samaccountname": "user UrgAttr test",
+ "userAccountControl": str(dsdb.UF_NORMAL_ACCOUNT),
+ "lockoutTime": "0",
+ "pwdLastSet": "0",
+ "description": "urgent attributes test description"})
+
+ # urgent replication should NOT be enabled when creating
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying userAccountControl
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(dsdb.UF_NORMAL_ACCOUNT + dsdb.UF_DONT_EXPIRE_PASSWD), FLAG_MOD_REPLACE,
+ "userAccountControl")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying lockoutTime
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ m["lockoutTime"] = MessageElement("1", FLAG_MOD_REPLACE,
+ "lockoutTime")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying pwdLastSet
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ m["pwdLastSet"] = MessageElement("-1", FLAG_MOD_REPLACE,
+ "pwdLastSet")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when modifying a not-urgent
+ # attribute
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ m["description"] = MessageElement("updated urgent attributes test description",
+ FLAG_MOD_REPLACE, "description")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when deleting
+ self.delete_force(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEquals(res["uSNHighest"], res["uSNUrgent"])
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/user_account_control.py b/source4/dsdb/tests/python/user_account_control.py
new file mode 100755
index 0000000..8f57568
--- /dev/null
+++ b/source4/dsdb/tests/python/user_account_control.py
@@ -0,0 +1,1308 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This tests the restrictions on userAccountControl that apply even if write access is permitted
+#
+# Copyright Samuel Cabrero 2014 <samuelcabrero@kernevil.me>
+# Copyright Andrew Bartlett 2014 <abartlet@samba.org>
+#
+# Licenced under the GPLv3
+#
+
+import optparse
+import sys
+import unittest
+import samba
+import samba.getopt as options
+import samba.tests
+import ldb
+import base64
+
+sys.path.insert(0, "bin/python")
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+from samba.subunit.run import SubunitTestRunner
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.dcerpc import samr, security, lsa
+from samba.credentials import Credentials
+from samba.ndr import ndr_unpack, ndr_pack
+from samba.tests import delete_force, DynamicTestCase
+from samba import gensec, sd_utils
+from samba.credentials import DONT_USE_KERBEROS
+from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from samba.dsdb import UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED, \
+ UF_LOCKOUT, UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,\
+ UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400, UF_INTERDOMAIN_TRUST_ACCOUNT, \
+ UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000, \
+ UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED, \
+ UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY, UF_DONT_REQUIRE_PREAUTH, \
+ UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_NO_AUTH_DATA_REQUIRED, \
+ UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS
+from samba import dsdb
+
+
+parser = optparse.OptionParser("user_account_control.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)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+host = args[0]
+
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+bits = [UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED,
+ UF_LOCKOUT, UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE,
+ UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
+ UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400,
+ UF_INTERDOMAIN_TRUST_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000,
+ UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED,
+ UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY,
+ UF_DONT_REQUIRE_PREAUTH,
+ UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
+ UF_NO_AUTH_DATA_REQUIRED,
+ UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS,
+ int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)]
+
+account_types = set([UF_NORMAL_ACCOUNT, UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_INTERDOMAIN_TRUST_ACCOUNT])
+
+
+@DynamicTestCase
+class UserAccountControlTests(samba.tests.TestCase):
+ @classmethod
+ def setUpDynamicTestCases(cls):
+ for priv in [(True, "priv"), (False, "cc")]:
+ for account_type in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT]:
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+ for objectclass in ["computer", "user"]:
+ for name in [("oc_uac_lock$", "withdollar"), \
+ ("oc_uac_lock", "plain")]:
+ test_name = f"{account_type_str}_{objectclass}_{priv[1]}_{name[1]}"
+ cls.generate_dynamic_test("test_objectclass_uac_dollar_lock",
+ test_name,
+ account_type,
+ objectclass,
+ name[0],
+ priv[0])
+
+ for priv in [(True, "priv"), (False, "wp")]:
+ for account_type in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT]:
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+ for account_type2 in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT]:
+ for how in ["replace", "deladd"]:
+ account_type2_str = dsdb.user_account_control_flag_bit_to_string(account_type2)
+ test_name = f"{account_type_str}_{account_type2_str}_{how}_{priv[1]}"
+ cls.generate_dynamic_test("test_objectclass_uac_mod_lock",
+ test_name,
+ account_type,
+ account_type2,
+ how,
+ priv[0])
+
+ for objectclass in ["computer", "user"]:
+ account_types = [UF_NORMAL_ACCOUNT]
+ if objectclass == "computer":
+ account_types.append(UF_WORKSTATION_TRUST_ACCOUNT)
+ account_types.append(UF_SERVER_TRUST_ACCOUNT)
+
+ for account_type in account_types:
+ account_type_str = (
+ dsdb.user_account_control_flag_bit_to_string(
+ account_type))
+ for account_type2 in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT,
+ UF_PARTIAL_SECRETS_ACCOUNT,
+ None]:
+ if account_type2 is None:
+ account_type2_str = None
+ else:
+ account_type2_str = (
+ dsdb.user_account_control_flag_bit_to_string(
+ account_type2))
+
+ for objectclass2 in ["computer", "user", None]:
+ for name2 in [("oc_uac_lock", "remove_dollar"),
+ (None, "keep_dollar")]:
+ test_name = (f"{priv[1]}_{objectclass}_"
+ f"{account_type_str}_to_"
+ f"{objectclass2}_"
+ f"{account_type2_str}_"
+ f"{name2[1]}")
+ cls.generate_dynamic_test("test_mod_lock",
+ test_name,
+ objectclass,
+ objectclass2,
+ account_type,
+ account_type2,
+ name2[0],
+ priv[0])
+
+ for account_type in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT]:
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+ for objectclass in ["user", "computer"]:
+ for how in ["replace", "deladd"]:
+ test_name = f"{account_type_str}_{objectclass}_{how}"
+ cls.generate_dynamic_test("test_objectclass_mod_lock",
+ test_name,
+ account_type,
+ objectclass,
+ how)
+
+ for account_type in [UF_NORMAL_ACCOUNT, UF_WORKSTATION_TRUST_ACCOUNT]:
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+ cls.generate_dynamic_test("test_uac_bits_unrelated_modify",
+ account_type_str, account_type)
+
+ for bit in bits:
+ try:
+ bit_str = dsdb.user_account_control_flag_bit_to_string(bit)
+ except KeyError:
+ bit_str = hex(bit)
+
+ cls.generate_dynamic_test("test_uac_bits_add",
+ bit_str, bit, bit_str)
+
+ cls.generate_dynamic_test("test_uac_bits_set",
+ bit_str, bit, bit_str)
+
+ cls.generate_dynamic_test("test_uac_bits_add",
+ "UF_NORMAL_ACCOUNT_UF_PASSWD_NOTREQD",
+ UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD,
+ "UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD")
+
+
+ def add_computer_ldap(self, computername, others=None, samdb=None):
+ if samdb is None:
+ samdb = self.samdb
+ dn = "CN=%s,%s" % (computername, self.OU)
+ domainname = ldb.Dn(self.samdb, self.samdb.domain_dn()).canonical_str().replace("/", "")
+ samaccountname = "%s$" % computername
+ dnshostname = "%s.%s" % (computername, domainname)
+ msg_dict = {
+ "dn": dn,
+ "objectclass": "computer"}
+ if others is not None:
+ msg_dict = dict(list(msg_dict.items()) + list(others.items()))
+
+ msg = ldb.Message.from_dict(self.samdb, msg_dict)
+ msg["sAMAccountName"] = samaccountname
+
+ print("Adding computer account %s" % computername)
+ samdb.add(msg)
+
+ def add_user_ldap(self, username, others=None, samdb=None):
+ if samdb is None:
+ samdb = self.samdb
+ dn = "CN=%s,%s" % (username, self.OU)
+ samaccountname = "%s" % username
+ msg_dict = {
+ "dn": dn,
+ "objectclass": "user"}
+ if others is not None:
+ msg_dict = dict(list(msg_dict.items()) + list(others.items()))
+
+ msg = ldb.Message.from_dict(self.samdb, msg_dict)
+ msg["sAMAccountName"] = samaccountname
+
+ print("Adding user account %s" % username)
+ samdb.add(msg)
+
+ def get_creds(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ return creds_tmp
+
+ def setUp(self):
+ super(UserAccountControlTests, self).setUp()
+ self.admin_creds = creds
+ self.admin_samdb = SamDB(url=ldaphost,
+ session_info=system_session(),
+ credentials=self.admin_creds, lp=lp)
+ self.domain_sid = security.dom_sid(self.admin_samdb.get_domain_sid())
+ self.base_dn = self.admin_samdb.domain_dn()
+
+ self.unpriv_user = "testuser1"
+ self.unpriv_user_pw = "samba123@"
+ self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw)
+
+ self.OU = "OU=test_computer_ou1,%s" % (self.base_dn)
+
+ delete_force(self.admin_samdb, self.OU, controls=["tree_delete:0"])
+ delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn))
+
+ self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw)
+ res = self.admin_samdb.search("CN=%s,CN=Users,%s" % (self.unpriv_user, self.admin_samdb.domain_dn()),
+ scope=SCOPE_BASE,
+ attrs=["objectSid"])
+ self.assertEqual(1, len(res))
+
+ self.unpriv_user_sid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0])
+ self.unpriv_user_dn = res[0].dn
+ self.addCleanup(self.admin_samdb.delete, self.unpriv_user_dn)
+
+ self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
+
+ self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, self.unpriv_creds)
+ self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
+
+ self.sd_utils = sd_utils.SDUtils(self.admin_samdb)
+ self.admin_samdb.create_ou(self.OU)
+ self.addCleanup(self.admin_samdb.delete, self.OU, ["tree_delete:1"])
+
+ self.unpriv_user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid)
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ self.add_computer_ldap("testcomputer-t")
+
+ self.sd_utils.modify_sd_on_dn(self.OU, old_sd)
+
+ self.computernames = ["testcomputer-0"]
+
+ # Get the SD of the template account, then force it to match
+ # what we expect for SeMachineAccountPrivilege accounts, so we
+ # can confirm we created the accounts correctly
+ self.sd_reference_cc = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,%s" % (self.OU))
+
+ self.sd_reference_modify = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,%s" % (self.OU))
+ for ace in self.sd_reference_modify.dacl.aces:
+ if ace.type == security.SEC_ACE_TYPE_ACCESS_ALLOWED and ace.trustee == self.unpriv_user_sid:
+ ace.access_mask = ace.access_mask | security.SEC_ADS_SELF_WRITE | security.SEC_ADS_WRITE_PROP
+
+ # Now reconnect without domain admin rights
+ self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
+
+ def test_add_computer_sd_cc(self):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ computername = self.computernames[0]
+ sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)),
+ ldb.FLAG_MOD_ADD,
+ "nTSecurityDescriptor")
+ self.add_computer_ldap(computername,
+ others={"nTSecurityDescriptor": sd})
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["ntSecurityDescriptor"])
+
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc, allow_remaining=True)
+
+ sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl to be a DC on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
+ samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl to be a RODC on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl to be a Workstation on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.samdb.modify(m)
+ except LdbError as e:
+ (enum, estr) = e.args
+ self.fail(f"got {estr} setting userAccountControl to UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD")
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["primaryGroupID"] = ldb.MessageElement(str(security.DOMAIN_RID_ADMINS),
+ ldb.FLAG_MOD_REPLACE, "primaryGroupID")
+ self.assertRaisesLdbError(ldb.ERR_UNWILLING_TO_PERFORM,
+ f"Unexpectedly able to set primaryGroupID on {m.dn}",
+ self.samdb.modify, m)
+
+
+ def test_mod_computer_cc(self):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ computername = self.computernames[0]
+ self.add_computer_ldap(computername)
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[])
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
+ samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl as RODC on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl as DC on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+
+ self.assertRaisesLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION,
+ f"Unexpectedly able to set userAccountControl as to UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl to be a workstation on {m.dn}",
+ self.samdb.modify, m)
+
+
+ def test_add_computer_cc_normal_bare(self):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ computername = self.computernames[0]
+ sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)),
+ ldb.FLAG_MOD_ADD,
+ "nTSecurityDescriptor")
+ self.add_computer_ldap(computername,
+ others={"nTSecurityDescriptor": sd})
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["ntSecurityDescriptor"])
+
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc, allow_remaining=True)
+
+ sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError([ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_UNWILLING_TO_PERFORM],
+ f"Unexpectedly able to set userAccountControl to be an Normal "
+ "account without |UF_PASSWD_NOTREQD Unexpectedly able to "
+ "set userAccountControl to be a workstation on {m.dn}",
+ self.samdb.modify, m)
+
+
+ def test_admin_mod_uac(self):
+ computername = self.computernames[0]
+ self.add_computer_ldap(computername, samdb=self.admin_samdb)
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ self.assertEqual(int(res[0]["userAccountControl"][0]), (UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_ACCOUNTDISABLE |
+ UF_PASSWD_NOTREQD))
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PARTIAL_SECRETS_ACCOUNT |
+ UF_TRUSTED_FOR_DELEGATION),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_OTHER,
+ f"Unexpectedly able to set userAccountControl to "
+ "UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT|"
+ "UF_TRUSTED_FOR_DELEGATION on {m.dn}",
+ self.admin_samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PARTIAL_SECRETS_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.admin_samdb.modify(m)
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ self.assertEqual(int(res[0]["userAccountControl"][0]), (UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PARTIAL_SECRETS_ACCOUNT))
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(UF_ACCOUNTDISABLE),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.admin_samdb.modify(m)
+ except LdbError as e:
+ (enum, estr) = e.args
+ self.fail(f"got {estr} setting userAccountControl to UF_ACCOUNTDISABLE (as admin)")
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE)
+
+ def _test_uac_bits_set_with_args(self, bit, bit_str):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ # Allow the creation of any children and write to any
+ # attributes (this is not a test of ACLs, this is a test of
+ # non-ACL userAccountControl rules
+ mod = f"(OA;CI;WP;;;{user_sid})(OA;;CC;;;{user_sid})"
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ # We want to start with UF_NORMAL_ACCOUNT, so we make a user
+ computername = self.computernames[0]
+ self.add_user_ldap(computername)
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[])
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test
+ priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
+ UF_DONT_EXPIRE_PASSWD])
+
+ # These bits really are privileged, or can't be changed from UF_NORMAL as a non-admin
+ priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
+ UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
+ UF_WORKSTATION_TRUST_ACCOUNT])
+
+ invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.samdb.modify(m)
+ if (bit in priv_bits):
+ self.fail("Unexpectedly able to set userAccountControl bit 0x%08X (%s), on %s"
+ % (bit, bit_str, m.dn))
+ if (bit in account_types and bit != UF_NORMAL_ACCOUNT):
+ self.fail("Unexpectedly able to set userAccountControl bit 0x%08X (%s), on %s"
+ % (bit, bit_str, m.dn))
+ except LdbError as e:
+ (enum, estr) = e.args
+ if bit in invalid_bits:
+ self.assertEqual(enum,
+ ldb.ERR_OTHER,
+ "was not able to set 0x%08X (%s) on %s"
+ % (bit, bit_str, m.dn))
+ elif (bit in account_types):
+ self.assertIn(enum, [ldb.ERR_OBJECT_CLASS_VIOLATION, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS])
+ elif (bit in priv_bits):
+ self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
+ else:
+ self.fail("Unable to set userAccountControl bit 0x%08X (%s) on %s: %s"
+ % (bit, bit_str, m.dn, estr))
+
+ def _test_uac_bits_unrelated_modify_with_args(self, account_type):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+
+ # Allow the creation of any children and write to any
+ # attributes (this is not a test of ACLs, this is a test of
+ # non-ACL userAccountControl rules
+ mod = f"(OA;CI;WP;;;{user_sid})(OA;;CC;;;{user_sid})"
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ computername = self.computernames[0]
+ if account_type == UF_WORKSTATION_TRUST_ACCOUNT:
+ self.add_computer_ldap(computername)
+ else:
+ self.add_user_ldap(computername)
+
+ res = self.admin_samdb.search(self.OU,
+ expression=f"(&(objectclass=user)(cn={computername}))",
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+ self.assertEqual(len(res), 1)
+
+ orig_uac = int(res[0]["userAccountControl"][0])
+ self.assertEqual(orig_uac & account_type,
+ account_type)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
+
+ # UF_LOCKOUT isn't actually ignored, it changes other
+ # attributes but does not stick here. See MS-SAMR 2.2.1.13
+ # UF_FLAG Codes clarification that UF_SCRIPT and
+ # UF_PASSWD_CANT_CHANGE are simply ignored by both clients and
+ # servers. Other bits are ignored as they are undefined, or
+ # are not set into the attribute (instead triggering other
+ # events).
+ ignored_bits = set([UF_SCRIPT, UF_00000004, UF_LOCKOUT, UF_PASSWD_CANT_CHANGE,
+ UF_00000400, UF_00004000, UF_00008000, UF_PASSWORD_EXPIRED,
+ int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)])
+ super_priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT])
+
+ priv_to_remove_bits = set([UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_WORKSTATION_TRUST_ACCOUNT])
+
+ for bit in bits:
+ # Reset this to the initial position, just to be sure
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(orig_uac),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.admin_samdb.modify(m)
+ except LdbError as e:
+ (enum, estr) = e.args
+ self.fail(f"got {estr} resetting userAccountControl to initial value {orig_uac:#08x}")
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ self.assertEqual(len(res), 1)
+ reset_uac = int(res[0]["userAccountControl"][0])
+ self.assertEqual(orig_uac, reset_uac)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.admin_samdb.modify(m)
+
+ if bit in invalid_bits:
+ self.fail("Should have been unable to set userAccountControl bit 0x%08X on %s" % (bit, m.dn))
+
+ except LdbError as e1:
+ (enum, estr) = e1.args
+ if bit in invalid_bits:
+ self.assertEqual(enum, ldb.ERR_OTHER)
+ # No point going on, try the next bit
+ continue
+ elif bit in super_priv_bits:
+ self.assertIn(enum, (ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ldb.ERR_OBJECT_CLASS_VIOLATION))
+ # No point going on, try the next bit
+ continue
+
+ elif (account_type == UF_NORMAL_ACCOUNT) \
+ and (bit in account_types) \
+ and (bit != account_type):
+ self.assertIn(enum, (ldb.ERR_UNWILLING_TO_PERFORM,
+ ldb.ERR_OBJECT_CLASS_VIOLATION))
+ continue
+
+ elif (account_type == UF_WORKSTATION_TRUST_ACCOUNT) \
+ and (bit != UF_NORMAL_ACCOUNT) \
+ and (bit != account_type):
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+ continue
+
+ else:
+ self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ if bit in ignored_bits:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD,
+ "Bit 0x%08x shouldn't stick" % bit)
+ else:
+ if bit in account_types:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_PASSWD_NOTREQD,
+ "Bit 0x%08x didn't stick" % bit)
+ else:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD,
+ "Bit 0x%08x didn't stick" % bit)
+
+ try:
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.samdb.modify(m)
+
+ except LdbError as e2:
+ (enum, estr) = e2.args
+ self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ if bit in account_types:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
+ % (bit, int(res[0]["userAccountControl"][0]),
+ bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
+ elif bit in ignored_bits:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
+ % (bit, int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
+
+ else:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
+ % (bit, int(res[0]["userAccountControl"][0]),
+ bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
+
+ try:
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.samdb.modify(m)
+ if bit in priv_to_remove_bits:
+ self.fail("Should have been unable to remove userAccountControl bit 0x%08X on %s" % (bit, m.dn))
+
+ except LdbError as e3:
+ (enum, estr) = e3.args
+ if account_type == UF_WORKSTATION_TRUST_ACCOUNT:
+ # Because removing any bit would change the account back to a user, which is locked by objectclass
+ self.assertIn(enum, (ldb.ERR_OBJECT_CLASS_VIOLATION, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS))
+ elif bit in priv_to_remove_bits:
+ self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail("Unexpectedly unable to remove userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ if bit in priv_to_remove_bits:
+ if bit in account_types:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should not have been removed" % bit)
+ else:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should not have been removed" % bit)
+ elif account_type != UF_WORKSTATION_TRUST_ACCOUNT:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should have been removed" % bit)
+
+ def _test_uac_bits_add_with_args(self, bit, bit_str):
+ computername = self.computernames[0]
+
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT])
+ # UF_NORMAL_ACCOUNT is invalid alone, needs UF_PASSWD_NOTREQD
+ unwilling_bits = set([UF_NORMAL_ACCOUNT])
+
+ # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test
+ priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
+ UF_DONT_EXPIRE_PASSWD])
+
+ # These bits really are privileged
+ priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
+ UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
+ UF_PARTIAL_SECRETS_ACCOUNT])
+
+ if bit not in account_types and ((bit & UF_NORMAL_ACCOUNT) == 0):
+ bit_add = bit|UF_WORKSTATION_TRUST_ACCOUNT
+ else:
+ bit_add = bit
+
+ try:
+
+ self.add_computer_ldap(computername, others={"userAccountControl": [str(bit_add)]})
+ delete_force(self.admin_samdb, "CN=%s,%s" % (computername, self.OU))
+ if bit in priv_bits:
+ self.fail("Unexpectedly able to set userAccountControl bit 0x%08X (%s) on %s"
+ % (bit, bit_str, computername))
+
+ except LdbError as e4:
+ (enum, estr) = e4.args
+ if bit in invalid_bits:
+ self.assertEqual(enum,
+ ldb.ERR_OTHER,
+ "Invalid bit 0x%08X (%s) was able to be set on %s"
+ % (bit,
+ bit_str,
+ computername))
+ elif bit in priv_bits:
+ if bit == UF_INTERDOMAIN_TRUST_ACCOUNT:
+ self.assertIn(enum, (ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS))
+ else:
+ self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ elif bit in unwilling_bits:
+ # This can fail early as user in a computer is not permitted as non-admin
+ self.assertIn(enum, (ldb.ERR_UNWILLING_TO_PERFORM,
+ ldb.ERR_OBJECT_CLASS_VIOLATION))
+ elif bit & UF_NORMAL_ACCOUNT:
+ # This can fail early as user in a computer is not permitted as non-admin
+ self.assertIn(enum, (ldb.ERR_UNWILLING_TO_PERFORM,
+ ldb.ERR_OBJECT_CLASS_VIOLATION))
+ else:
+ self.fail("Unable to set userAccountControl bit 0x%08X (%s) on %s: %s"
+ % (bit,
+ bit_str,
+ computername,
+ estr))
+
+ def test_primarygroupID_cc_add(self):
+ computername = self.computernames[0]
+
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+ try:
+ # When creating a new object, you can not ever set the primaryGroupID
+ self.add_computer_ldap(computername, others={"primaryGroupID": [str(security.DOMAIN_RID_ADMINS)]})
+ self.fail("Unexpectedly able to set primaryGruopID to be an admin on %s" % computername)
+ except LdbError as e13:
+ (enum, estr) = e13.args
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+
+ def test_primarygroupID_priv_DC_modify(self):
+ computername = self.computernames[0]
+
+ self.add_computer_ldap(computername,
+ others={"userAccountControl": [str(UF_SERVER_TRUST_ACCOUNT)]},
+ samdb=self.admin_samdb)
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[""])
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
+ security.DOMAIN_RID_USERS))
+ m["member"] = ldb.MessageElement(
+ [str(res[0].dn)], ldb.FLAG_MOD_ADD,
+ "member")
+ self.admin_samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["primaryGroupID"] = ldb.MessageElement(
+ [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE,
+ "primaryGroupID")
+ try:
+ self.admin_samdb.modify(m)
+
+ # When creating a new object, you can not ever set the primaryGroupID
+ self.fail("Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername)
+ except LdbError as e14:
+ (enum, estr) = e14.args
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+
+ def test_primarygroupID_priv_member_modify(self):
+ computername = self.computernames[0]
+
+ self.add_computer_ldap(computername,
+ others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)]},
+ samdb=self.admin_samdb)
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[""])
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
+ security.DOMAIN_RID_USERS))
+ m["member"] = ldb.MessageElement(
+ [str(res[0].dn)], ldb.FLAG_MOD_ADD,
+ "member")
+ self.admin_samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["primaryGroupID"] = ldb.MessageElement(
+ [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE,
+ "primaryGroupID")
+
+ self.assertRaisesLdbError(ldb.ERR_UNWILLING_TO_PERFORM,
+ f"Unexpectedly able to set primaryGroupID to be other than DCS on {m.dn}",
+ self.admin_samdb.modify, m)
+
+ def test_primarygroupID_priv_user_modify(self):
+ computername = self.computernames[0]
+
+ self.add_computer_ldap(computername,
+ others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT)]},
+ samdb=self.admin_samdb)
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[""])
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
+ security.DOMAIN_RID_ADMINS))
+ m["member"] = ldb.MessageElement(
+ [str(res[0].dn)], ldb.FLAG_MOD_ADD,
+ "member")
+ self.admin_samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["primaryGroupID"] = ldb.MessageElement(
+ [str(security.DOMAIN_RID_ADMINS)], ldb.FLAG_MOD_REPLACE,
+ "primaryGroupID")
+ self.admin_samdb.modify(m)
+
+ def _test_objectclass_uac_dollar_lock_with_args(self,
+ account_type,
+ objectclass,
+ name,
+ priv):
+ dn = "CN=%s,%s" % (name, self.OU)
+ msg_dict = {
+ "dn": dn,
+ "objectclass": objectclass,
+ "samAccountName": name,
+ "userAccountControl": str(account_type | UF_PASSWD_NOTREQD)}
+
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+
+ print(f"Adding account {name} as {account_type_str} with objectclass {objectclass}")
+
+ if priv:
+ samdb = self.admin_samdb
+ else:
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;;;%s)" % str(user_sid)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+ samdb = self.samdb
+
+ enum = ldb.SUCCESS
+ try:
+ samdb.add(msg_dict)
+ except ldb.LdbError as e:
+ (enum, msg) = e.args
+
+ if (account_type == UF_SERVER_TRUST_ACCOUNT
+ and objectclass != "computer"):
+ self.assertEqual(enum, ldb.ERR_OBJECT_CLASS_VIOLATION)
+ return
+
+ if priv == False and account_type == UF_SERVER_TRUST_ACCOUNT:
+ self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ return
+
+ if (objectclass == "user"
+ and account_type != UF_NORMAL_ACCOUNT):
+ self.assertEqual(enum, ldb.ERR_OBJECT_CLASS_VIOLATION)
+ return
+
+ if (not priv and objectclass == "computer"
+ and account_type == UF_NORMAL_ACCOUNT):
+ self.assertEqual(enum, ldb.ERR_OBJECT_CLASS_VIOLATION)
+ return
+
+ if priv and account_type == UF_NORMAL_ACCOUNT:
+ self.assertEqual(enum, 0)
+ return
+
+ if (priv == False and
+ account_type != UF_NORMAL_ACCOUNT and
+ name[-1] != '$'):
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+ return
+
+ self.assertEqual(enum, 0)
+
+ def _test_mod_lock_with_args(self,
+ objectclass,
+ objectclass2,
+ account_type,
+ account_type2,
+ name2,
+ priv):
+ name = "oc_uac_lock$"
+
+ dn = "CN=%s,%s" % (name, self.OU)
+ msg_dict = {
+ "dn": dn,
+ "objectclass": objectclass,
+ "samAccountName": name,
+ "userAccountControl": str(account_type | UF_PASSWD_NOTREQD)}
+
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(
+ account_type)
+
+ print(f"Adding account {name} as {account_type_str} "
+ f"with objectclass {objectclass}")
+
+ # Create the object as admin
+ self.admin_samdb.add(msg_dict)
+
+ if priv:
+ samdb = self.admin_samdb
+ else:
+ samdb = self.samdb
+
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+
+ # We want to test what the underlying rules for non-admins regardless
+ # of security descriptors are, so set this very, dangerously, broadly
+ mod = f"(OA;;WP;;;{user_sid})"
+
+ self.sd_utils.dacl_add_ace(dn, mod)
+
+ msg = "Modifying account"
+ if name2 is not None:
+ msg += f" to {name2}"
+ if account_type2 is not None:
+ account_type2_str = dsdb.user_account_control_flag_bit_to_string(
+ account_type2)
+ msg += f" as {account_type2_str}"
+ else:
+ account_type2_str = None
+ if objectclass2 is not None:
+ msg += f" with objectClass {objectclass2}"
+ print(msg)
+
+ msg = ldb.Message(ldb.Dn(samdb, dn))
+ if objectclass2 is not None:
+ msg["objectClass"] = ldb.MessageElement(objectclass2,
+ ldb.FLAG_MOD_REPLACE,
+ "objectClass")
+ if name2 is not None:
+ msg["sAMAccountName"] = ldb.MessageElement(name2,
+ ldb.FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ if account_type2 is not None:
+ msg["userAccountControl"] = ldb.MessageElement(
+ str(account_type2 | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE,
+ "userAccountControl")
+ enum = ldb.SUCCESS
+ try:
+ samdb.modify(msg)
+ except ldb.LdbError as e:
+ enum, _ = e.args
+
+ # Setting userAccountControl to be an RODC is not allowed.
+ if account_type2 == UF_PARTIAL_SECRETS_ACCOUNT:
+ self.assertEqual(enum, ldb.ERR_OTHER)
+ return
+
+ # Unprivileged users cannot change userAccountControl. The exception is
+ # changing a non-normal account to UF_WORKSTATION_TRUST_ACCOUNT, which
+ # is allowed.
+ if (not priv
+ and account_type2 is not None
+ and account_type != account_type2
+ and (account_type == UF_NORMAL_ACCOUNT
+ or account_type2 != UF_WORKSTATION_TRUST_ACCOUNT)):
+ self.assertIn(enum, [ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ldb.ERR_OBJECT_CLASS_VIOLATION])
+ return
+
+ # A non-computer account cannot have UF_SERVER_TRUST_ACCOUNT.
+ if objectclass == "user" and account_type2 == UF_SERVER_TRUST_ACCOUNT:
+ self.assertIn(enum, [ldb.ERR_UNWILLING_TO_PERFORM,
+ ldb.ERR_OBJECT_CLASS_VIOLATION])
+ return
+
+ # The objectClass is not allowed to change.
+ if objectclass2 is not None and objectclass != objectclass2:
+ self.assertIn(enum, [ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_UNWILLING_TO_PERFORM])
+ return
+
+ # Unprivileged users cannot remove the trailing dollar from a computer
+ # account.
+ if not priv and objectclass == "computer" and (
+ name2 is not None and name2[-1] != "$"):
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+ return
+
+ self.assertEqual(enum, 0)
+ return
+
+ def _test_objectclass_uac_mod_lock_with_args(self,
+ account_type,
+ account_type2,
+ how,
+ priv):
+ name = "uac_mod_lock$"
+ dn = "CN=%s,%s" % (name, self.OU)
+ if account_type == UF_NORMAL_ACCOUNT:
+ objectclass = "user"
+ else:
+ objectclass = "computer"
+
+ msg_dict = {
+ "dn": dn,
+ "objectclass": objectclass,
+ "samAccountName": name,
+ "userAccountControl": str(account_type | UF_PASSWD_NOTREQD)}
+
+ account_type_str \
+ = dsdb.user_account_control_flag_bit_to_string(account_type)
+ account_type2_str \
+ = dsdb.user_account_control_flag_bit_to_string(account_type2)
+
+ print(f"Adding account {name} as {account_type_str} with objectclass {objectclass}")
+
+ if priv:
+ samdb = self.admin_samdb
+ else:
+ samdb = self.samdb
+
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+
+ # Create the object as admin
+ self.admin_samdb.add(msg_dict)
+
+ # We want to test what the underlying rules for non-admins
+ # regardless of security descriptors are, so set this very,
+ # dangerously, broadly
+ mod = "(OA;;WP;;;%s)" % str(user_sid)
+
+ self.sd_utils.dacl_add_ace(dn, mod)
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, dn)
+ if how == "replace":
+ m["userAccountControl"] = ldb.MessageElement(str(account_type2 | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ elif how == "deladd":
+ m["0userAccountControl"] = ldb.MessageElement([],
+ ldb.FLAG_MOD_DELETE, "userAccountControl")
+ m["1userAccountControl"] = ldb.MessageElement(str(account_type2 | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_ADD, "userAccountControl")
+ else:
+ raise ValueError(f"{how} was not a valid argument")
+
+ if (account_type == account_type2):
+ samdb.modify(m)
+ elif (account_type == UF_NORMAL_ACCOUNT) and \
+ (account_type2 == UF_SERVER_TRUST_ACCOUNT) and not priv:
+ self.assertRaisesLdbError([ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS],
+ f"Should have been unable to change {account_type_str} to {account_type2_str}",
+ samdb.modify, m)
+ elif (account_type == UF_NORMAL_ACCOUNT) and \
+ (account_type2 == UF_SERVER_TRUST_ACCOUNT) and priv:
+ self.assertRaisesLdbError([ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_UNWILLING_TO_PERFORM],
+ f"Should have been unable to change {account_type_str} to {account_type2_str}",
+ samdb.modify, m)
+ elif (account_type == UF_WORKSTATION_TRUST_ACCOUNT) and \
+ (account_type2 == UF_SERVER_TRUST_ACCOUNT) and not priv:
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Should have been unable to change {account_type_str} to {account_type2_str}",
+ samdb.modify, m)
+ elif priv:
+ samdb.modify(m)
+ elif (account_type in [UF_SERVER_TRUST_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT]) and \
+ (account_type2 in [UF_SERVER_TRUST_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT]):
+ samdb.modify(m)
+ elif (account_type == account_type2):
+ samdb.modify(m)
+ else:
+ self.assertRaisesLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION,
+ f"Should have been unable to change {account_type_str} to {account_type2_str}",
+ samdb.modify, m)
+
+ def _test_objectclass_mod_lock_with_args(self,
+ account_type,
+ objectclass,
+ how):
+ name = "uac_mod_lock$"
+ dn = "CN=%s,%s" % (name, self.OU)
+ if objectclass == "computer":
+ new_objectclass = ["top",
+ "person",
+ "organizationalPerson",
+ "user"]
+ elif objectclass == "user":
+ new_objectclass = ["top",
+ "person",
+ "organizationalPerson",
+ "user",
+ "computer"]
+
+ msg_dict = {
+ "dn": dn,
+ "objectclass": objectclass,
+ "samAccountName": name,
+ "userAccountControl": str(account_type | UF_PASSWD_NOTREQD)}
+
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+
+ print(f"Adding account {name} as {account_type_str} with objectclass {objectclass}")
+
+ try:
+ self.admin_samdb.add(msg_dict)
+ if (objectclass == "user" \
+ and account_type != UF_NORMAL_ACCOUNT):
+ self.fail("Able to create {account_type_str} on {objectclass}")
+ except LdbError as e:
+ (enum, estr) = e.args
+ self.assertEqual(enum, ldb.ERR_OBJECT_CLASS_VIOLATION)
+
+ if objectclass == "user" and account_type != UF_NORMAL_ACCOUNT:
+ return
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.admin_samdb, dn)
+ if how == "replace":
+ m["objectclass"] = ldb.MessageElement(new_objectclass,
+ ldb.FLAG_MOD_REPLACE, "objectclass")
+ elif how == "adddel":
+ m["0objectclass"] = ldb.MessageElement([],
+ ldb.FLAG_MOD_DELETE, "objectclass")
+ m["1objectclass"] = ldb.MessageElement(new_objectclass,
+ ldb.FLAG_MOD_ADD, "objectclass")
+
+ self.assertRaisesLdbError([ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_UNWILLING_TO_PERFORM],
+ "Should have been unable Able to change objectclass of a {objectclass}",
+ self.admin_samdb.modify, m)
+
+runner = SubunitTestRunner()
+rc = 0
+if not runner.run(unittest.makeSuite(UserAccountControlTests)).wasSuccessful():
+ rc = 1
+sys.exit(rc)
diff --git a/source4/dsdb/tests/python/vlv.py b/source4/dsdb/tests/python/vlv.py
new file mode 100644
index 0000000..3d5782b
--- /dev/null
+++ b/source4/dsdb/tests/python/vlv.py
@@ -0,0 +1,1765 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Originally based on ./sam.py
+import optparse
+import sys
+import os
+import base64
+import random
+import re
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+from samba.common import get_bytes
+from samba.common import get_string
+
+import time
+
+parser = optparse.OptionParser("vlv.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)
+
+parser.add_option('--elements', type='int', default=20,
+ help="use this many elements in the tests")
+
+parser.add_option('--delete-in-setup', action='store_true',
+ help="cleanup in next setup rather than teardown")
+
+parser.add_option('--skip-attr-regex',
+ help="ignore attributes matching this regex")
+
+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)
+
+N_ELEMENTS = opts.elements
+
+
+class VlvTestException(Exception):
+ pass
+
+
+def encode_vlv_control(critical=1,
+ before=0, after=0,
+ offset=None,
+ gte=None,
+ n=0, cookie=None):
+
+ s = "vlv:%d:%d:%d:" % (critical, before, after)
+
+ if offset is not None:
+ m = "%d:%d" % (offset, n)
+ elif b':' in gte or b'\x00' in gte:
+ gte = get_string(base64.b64encode(gte))
+ m = "base64>=%s" % gte
+ else:
+ m = ">=%s" % get_string(gte)
+
+ if cookie is None:
+ return s + m
+
+ return s + m + ':' + cookie
+
+
+def get_cookie(controls, expected_n=None):
+ """Get the cookie, STILL base64 encoded, or raise ValueError."""
+ for c in list(controls):
+ cstr = str(c)
+ if cstr.startswith('vlv_resp'):
+ head, n, _, cookie = cstr.rsplit(':', 3)
+ if expected_n is not None and int(n) != expected_n:
+ raise ValueError("Expected %s items, server said %s" %
+ (expected_n, n))
+ return cookie
+ raise ValueError("there is no VLV response")
+
+
+class TestsWithUserOU(samba.tests.TestCase):
+
+ def create_user(self, i, n, prefix='vlvtest', suffix='', attrs=None):
+ name = "%s%d%s" % (prefix, i, suffix)
+ user = {
+ 'cn': name,
+ "objectclass": "user",
+ 'givenName': "abcdefghijklmnopqrstuvwxyz"[i % 26],
+ "roomNumber": "%sbc" % (n - i),
+ "carLicense": "后来经",
+ "facsimileTelephoneNumber": name,
+ "employeeNumber": "%s%sx" % (abs(i * (99 - i)), '\n' * (i & 255)),
+ "accountExpires": "%s" % (10 ** 9 + 1000000 * i),
+ "msTSExpireDate4": "19%02d0101010000.0Z" % (i % 100),
+ "flags": str(i * (n - i)),
+ "serialNumber": "abc %s%s%s" % ('AaBb |-/'[i & 7],
+ ' 3z}'[i & 3],
+ '"@'[i & 1],),
+ }
+
+ # _user_broken_attrs tests are broken due to problems outside
+ # of VLV.
+ _user_broken_attrs = {
+ # Sort doesn't look past a NUL byte.
+ "photo": "\x00%d" % (n - i),
+ "audio": "%sn octet string %s%s ♫♬\x00lalala" % ('Aa'[i & 1],
+ chr(i & 255), i),
+ "displayNamePrintable": "%d\x00%c" % (i, i & 255),
+ "adminDisplayName": "%d\x00b" % (n - i),
+ "title": "%d%sb" % (n - i, '\x00' * i),
+ "comment": "Favourite colour is %d" % (n % (i + 1)),
+
+ # Names that vary only in case. Windows returns
+ # equivalent addresses in the order they were put
+ # in ('a st', 'A st',...).
+ "street": "%s st" % (chr(65 | (i & 14) | ((i & 1) * 32))),
+ }
+
+ if attrs is not None:
+ user.update(attrs)
+
+ user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
+
+ if opts.skip_attr_regex:
+ match = re.compile(opts.skip_attr_regex).search
+ for k in user.keys():
+ if match(k):
+ del user[k]
+
+ self.users.append(user)
+ self.ldb.add(user)
+ return user
+
+ def setUp(self):
+ super(TestsWithUserOU, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.ldb_ro = self.ldb
+ self.base_dn = self.ldb.domain_dn()
+ self.tree_dn = "ou=vlvtesttree,%s" % self.base_dn
+ self.ou = "ou=vlvou,%s" % self.tree_dn
+ if opts.delete_in_setup:
+ try:
+ self.ldb.delete(self.tree_dn, ['tree_delete:1'])
+ except ldb.LdbError as e:
+ print("tried deleting %s, got error %s" % (self.tree_dn, e))
+ self.ldb.add({
+ "dn": self.tree_dn,
+ "objectclass": "organizationalUnit"})
+ self.ldb.add({
+ "dn": self.ou,
+ "objectclass": "organizationalUnit"})
+
+ self.users = []
+ for i in range(N_ELEMENTS):
+ self.create_user(i, N_ELEMENTS)
+
+ attrs = self.users[0].keys()
+ self.binary_sorted_keys = ['audio',
+ 'photo',
+ "msTSExpireDate4",
+ 'serialNumber',
+ "displayNamePrintable"]
+
+ self.numeric_sorted_keys = ['flags',
+ 'accountExpires']
+
+ self.timestamp_keys = ['msTSExpireDate4']
+
+ self.int64_keys = set(['accountExpires'])
+
+ self.locale_sorted_keys = [x for x in attrs if
+ x not in (self.binary_sorted_keys +
+ self.numeric_sorted_keys)]
+
+ # don't try spaces, etc in cn
+ self.delicate_keys = ['cn']
+
+ def tearDown(self):
+ super(TestsWithUserOU, self).tearDown()
+ if not opts.delete_in_setup:
+ self.ldb.delete(self.tree_dn, ['tree_delete:1'])
+
+
+class VLVTestsBase(TestsWithUserOU):
+
+ # Run a vlv search and return important fields of the response control
+ def vlv_search(self, attr, expr, cookie="", after_count=0, offset=1):
+ sort_ctrl = "server_sort:1:0:%s" % attr
+ ctrl = "vlv:1:0:%d:%d:0" % (after_count, offset)
+ if cookie:
+ ctrl += ":" + cookie
+
+ res = self.ldb_ro.search(self.ou,
+ expression=expr,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[ctrl, sort_ctrl])
+ results = [str(x[attr][0]) for x in res]
+
+ ctrls = [str(c) for c in res.controls if
+ str(c).startswith('vlv')]
+ self.assertEqual(len(ctrls), 1)
+
+ spl = ctrls[0].rsplit(':')
+ cookie = ""
+ if len(spl) == 6:
+ cookie = spl[-1]
+
+ return results, cookie
+
+
+class VLVTestsRO(VLVTestsBase):
+ def test_vlv_simple_double_run(self):
+ """Do the simplest possible VLV query to confirm if VLV
+ works at all. Useful for showing VLV as a whole works
+ on Global Catalog (for example)"""
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users))
+ expected_results = full_results
+ self.assertEqual(results, expected_results)
+
+
+class VLVTestsGC(VLVTestsRO):
+ def setUp(self):
+ super(VLVTestsRO, self).setUp()
+ self.ldb_ro = SamDB(host + ":3268", credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+
+class VLVTests(VLVTestsBase):
+ def get_full_list(self, attr, include_cn=False):
+ """Fetch the whole list sorted on the attribute, using the VLV.
+ This way you get a VLV cookie."""
+ n_users = len(self.users)
+ sort_control = "server_sort:1:0:%s" % attr
+ half_n = n_users // 2
+ vlv_search = "vlv:1:%d:%d:%d:0" % (half_n, half_n, half_n + 1)
+ attrs = [attr]
+ if include_cn:
+ attrs.append('cn')
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=attrs,
+ controls=[sort_control,
+ vlv_search])
+ if include_cn:
+ full_results = [(str(x[attr][0]), str(x['cn'][0])) for x in res]
+ else:
+ full_results = [str(x[attr][0]).lower() for x in res]
+ controls = res.controls
+ return full_results, controls, sort_control
+
+ def get_expected_order(self, attr, expression=None):
+ """Fetch the whole list sorted on the attribute, using sort only."""
+ sort_control = "server_sort:1:0:%s" % attr
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ expression=expression,
+ attrs=[attr],
+ controls=[sort_control])
+ results = [x[attr][0] for x in res]
+ return results
+
+ def delete_user(self, user):
+ self.ldb.delete(user['dn'])
+ del self.users[self.users.index(user)]
+
+ def get_gte_tests_and_order(self, attr, expression=None):
+ expected_order = self.get_expected_order(attr, expression=expression)
+ gte_users = []
+ if attr in self.delicate_keys:
+ gte_keys = [
+ '3',
+ 'abc',
+ '¹',
+ 'ŋđ¼³ŧ“«đð',
+ '桑巴',
+ ]
+ elif attr in self.timestamp_keys:
+ gte_keys = [
+ '18560101010000.0Z',
+ '19140103010000.0Z',
+ '19560101010010.0Z',
+ '19700101000000.0Z',
+ '19991231211234.3Z',
+ '20061111211234.0Z',
+ '20390901041234.0Z',
+ '25560101010000.0Z',
+ ]
+ elif attr not in self.numeric_sorted_keys:
+ gte_keys = [
+ '3',
+ 'abc',
+ ' ',
+ '!@#!@#!',
+ 'kōkako',
+ '¹',
+ 'ŋđ¼³ŧ“«đð',
+ '\n\t\t',
+ '桑巴',
+ 'zzzz',
+ ]
+ if expected_order:
+ gte_keys.append(expected_order[len(expected_order) // 2] + b' tail')
+
+ else:
+ # "numeric" means positive integers
+ # doesn't work with -1, 3.14, ' 3', '9' * 20
+ gte_keys = ['3',
+ '1' * 10,
+ '1',
+ '9' * 7,
+ '0']
+
+ if attr in self.int64_keys:
+ gte_keys += ['3' * 12, '71' * 8]
+
+ for i, x in enumerate(gte_keys):
+ user = self.create_user(i, N_ELEMENTS,
+ prefix='gte',
+ attrs={attr: x})
+ gte_users.append(user)
+
+ gte_order = self.get_expected_order(attr)
+ for user in gte_users:
+ self.delete_user(user)
+
+ # for sanity's sake
+ expected_order_2 = self.get_expected_order(attr, expression=expression)
+ self.assertEqual(expected_order, expected_order_2)
+
+ # Map gte tests to indexes in expected order. This will break
+ # if gte_order and expected_order are differently ordered (as
+ # it should).
+ gte_map = {}
+
+ # index to the first one with each value
+ index_map = {}
+ for i, k in enumerate(expected_order):
+ if k not in index_map:
+ index_map[k] = i
+
+ keys = []
+ for k in gte_order:
+ if k in index_map:
+ i = index_map[k]
+ gte_map[k] = i
+ for k in keys:
+ gte_map[k] = i
+ keys = []
+ else:
+ keys.append(k)
+
+ for k in keys:
+ gte_map[k] = len(expected_order)
+
+ if False:
+ print("gte_map:")
+ for k in gte_order:
+ print(" %10s => %10s" % (k, gte_map[k]))
+
+ return gte_order, expected_order, gte_map
+
+ def assertCorrectResults(self, results, expected_order,
+ offset, before, after):
+ """A helper to calculate offsets correctly and say as much as possible
+ when something goes wrong."""
+
+ start = max(offset - before - 1, 0)
+ end = offset + after
+ expected_results = expected_order[start: end]
+
+ # if it is a tuple with the cn, drop the cn
+ if expected_results and isinstance(expected_results[0], tuple):
+ expected_results = [x[0] for x in expected_results]
+
+ if expected_results == results:
+ return
+
+ if expected_order is not None:
+ print("expected order: %s" % expected_order[:20])
+ if len(expected_order) > 20:
+ print("... and %d more not shown" % (len(expected_order) - 20))
+
+ print("offset %d before %d after %d" % (offset, before, after))
+ print("start %d end %d" % (start, end))
+ print("expected: %s" % expected_results)
+ print("got : %s" % results)
+ self.assertEqual(expected_results, results)
+
+ def test_server_vlv_with_cookie(self):
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ for attr in attrs:
+ expected_order = self.get_expected_order(attr)
+ sort_control = "server_sort:1:0:%s" % attr
+ res = None
+ n = len(self.users)
+ for before in [10, 0, 3, 1, 4, 5, 2]:
+ for after in [0, 3, 1, 4, 5, 2, 7]:
+ for offset in range(max(1, before - 2),
+ min(n - after + 2, n)):
+ if res is None:
+ vlv_search = "vlv:1:%d:%d:%d:0" % (before, after,
+ offset)
+ else:
+ cookie = get_cookie(res.controls, n)
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset, n,
+ cookie))
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ results = [x[attr][0] for x in res]
+
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def run_index_tests_with_expressions(self, expressions):
+ # Here we don't test every before/after combination.
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ for attr in attrs:
+ for expression in expressions:
+ expected_order = self.get_expected_order(attr, expression)
+ sort_control = "server_sort:1:0:%s" % attr
+ res = None
+ n = len(expected_order)
+ for before in range(0, 11):
+ after = before
+ for offset in range(max(1, before - 2),
+ min(n - after + 2, n)):
+ if res is None:
+ vlv_search = "vlv:1:%d:%d:%d:0" % (before, after,
+ offset)
+ else:
+ cookie = get_cookie(res.controls)
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset, n,
+ cookie))
+
+ res = self.ldb.search(self.ou,
+ expression=expression,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ results = [x[attr][0] for x in res]
+
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def test_server_vlv_with_expression(self):
+ """What happens when we run the VLV with an expression?"""
+ expressions = ["(objectClass=*)",
+ "(cn=%s)" % self.users[-1]['cn'],
+ "(roomNumber=%s)" % self.users[0]['roomNumber'],
+ ]
+ self.run_index_tests_with_expressions(expressions)
+
+ def test_server_vlv_with_failing_expression(self):
+ """What happens when we run the VLV on an expression that matches
+ nothing?"""
+ expressions = ["(samaccountname=testferf)",
+ "(cn=hefalump)",
+ ]
+ self.run_index_tests_with_expressions(expressions)
+
+ def run_gte_tests_with_expressions(self, expressions):
+ # Here we don't test every before/after combination.
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ for expression in expressions:
+ for attr in attrs:
+ gte_order, expected_order, gte_map = \
+ self.get_gte_tests_and_order(attr, expression)
+ # In case there is some order dependency, disorder tests
+ gte_tests = gte_order[:]
+ random.seed(2)
+ random.shuffle(gte_tests)
+ res = None
+ sort_control = "server_sort:1:0:%s" % attr
+ expected_order = self.get_expected_order(attr, expression)
+
+ for before in range(0, 11):
+ after = before
+ for gte in gte_tests:
+ if res is not None:
+ cookie = get_cookie(res.controls)
+ else:
+ cookie = None
+ vlv_search = encode_vlv_control(before=before,
+ after=after,
+ gte=get_bytes(gte),
+ cookie=cookie)
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ expression=expression,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ results = [x[attr][0] for x in res]
+ offset = gte_map.get(gte, len(expected_order))
+
+ # here offset is 0-based
+ start = max(offset - before, 0)
+ end = offset + 1 + after
+
+ expected_results = expected_order[start: end]
+
+ self.assertEqual(expected_results, results)
+
+ def test_vlv_gte_with_expression(self):
+ """What happens when we run the VLV with an expression?"""
+ expressions = ["(objectClass=*)",
+ "(cn=%s)" % self.users[-1]['cn'],
+ "(roomNumber=%s)" % self.users[0]['roomNumber'],
+ ]
+ self.run_gte_tests_with_expressions(expressions)
+
+ def test_vlv_gte_with_failing_expression(self):
+ """What happens when we run the VLV on an expression that matches
+ nothing?"""
+ expressions = ["(samaccountname=testferf)",
+ "(cn=hefalump)",
+ ]
+ self.run_gte_tests_with_expressions(expressions)
+
+ def test_server_vlv_with_cookie_while_adding_and_deleting(self):
+ """What happens if we add or remove items in the middle of the VLV?
+
+ Nothing. The search and the sort is not repeated, and we only
+ deal with the objects originally found.
+ """
+ attrs = ['cn'] + [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ user_number = 0
+ iteration = 0
+ for attr in attrs:
+ full_results, controls, sort_control = \
+ self.get_full_list(attr, True)
+ original_n = len(self.users)
+
+ expected_order = full_results
+ random.seed(1)
+
+ for before in list(range(0, 3)) + [6, 11, 19]:
+ for after in list(range(0, 3)) + [6, 11, 19]:
+ start = max(before - 1, 1)
+ end = max(start + 4, original_n - after + 2)
+ for offset in range(start, end):
+ # if iteration > 2076:
+ # return
+ cookie = get_cookie(controls, original_n)
+ vlv_search = encode_vlv_control(before=before,
+ after=after,
+ offset=offset,
+ n=original_n,
+ cookie=cookie)
+
+ iteration += 1
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ controls = res.controls
+ results = [x[attr][0] for x in res]
+ real_offset = max(1, min(offset, len(expected_order)))
+
+ expected_results = []
+ skipped = 0
+ begin_offset = max(real_offset - before - 1, 0)
+ real_before = min(before, real_offset - 1)
+ real_after = min(after,
+ len(expected_order) - real_offset)
+
+ for x in expected_order[begin_offset:]:
+ if x is not None:
+ expected_results.append(get_bytes(x[0]))
+ if (len(expected_results) ==
+ real_before + real_after + 1):
+ break
+ else:
+ skipped += 1
+
+ if expected_results != results:
+ print("attr %s before %d after %d offset %d" %
+ (attr, before, after, offset))
+ self.assertEqual(expected_results, results)
+
+ n = len(self.users)
+ if random.random() < 0.1 + (n < 5) * 0.05:
+ if n == 0:
+ i = 0
+ else:
+ i = random.randrange(n)
+ user = self.create_user(i, n, suffix='-%s' %
+ user_number)
+ user_number += 1
+ if random.random() < 0.1 + (n > 50) * 0.02 and n:
+ index = random.randrange(n)
+ user = self.users.pop(index)
+
+ self.ldb.delete(user['dn'])
+
+ replaced = (user[attr], user['cn'])
+ if replaced in expected_order:
+ i = expected_order.index(replaced)
+ expected_order[i] = None
+
+ def test_server_vlv_with_cookie_while_changing(self):
+ """What happens if we modify items in the middle of the VLV?
+
+ The expected behaviour (as found on Windows) is the sort is
+ not repeated, but the changes in attributes are reflected.
+ """
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass', 'cn')]
+ for attr in attrs:
+ n_users = len(self.users)
+ expected_order = [x.upper() for x in self.get_expected_order(attr)]
+ sort_control = "server_sort:1:0:%s" % attr
+ res = None
+ i = 0
+
+ # First we'll fetch the whole list so we know the original
+ # sort order. This is necessary because we don't know how
+ # the server will order equivalent items. We are using the
+ # dn as a key.
+ half_n = n_users // 2
+ vlv_search = "vlv:1:%d:%d:%d:0" % (half_n, half_n, half_n + 1)
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=['dn', attr],
+ controls=[sort_control, vlv_search])
+
+ results = [x[attr][0].upper() for x in res]
+ #self.assertEqual(expected_order, results)
+
+ dn_order = [str(x['dn']) for x in res]
+ values = results[:]
+
+ for before in range(0, 3):
+ for after in range(0, 3):
+ for offset in range(1 + before, n_users - after):
+ cookie = get_cookie(res.controls, len(self.users))
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset, len(self.users),
+ cookie))
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=['dn', attr],
+ controls=[sort_control,
+ vlv_search])
+
+ dn_results = [str(x['dn']) for x in res]
+ dn_expected = dn_order[offset - before - 1:
+ offset + after]
+
+ self.assertEqual(dn_expected, dn_results)
+
+ results = [x[attr][0].upper() for x in res]
+
+ self.assertCorrectResults(results, values,
+ offset, before, after)
+
+ i += 1
+ if i % 3 == 2:
+ if (attr in self.locale_sorted_keys or
+ attr in self.binary_sorted_keys):
+ i1 = i % n_users
+ i2 = (i ^ 255) % n_users
+ dn1 = dn_order[i1]
+ dn2 = dn_order[i2]
+ v2 = values[i2]
+
+ if v2 in self.locale_sorted_keys:
+ v2 += '-%d' % i
+ cn1 = dn1.split(',', 1)[0][3:]
+ cn2 = dn2.split(',', 1)[0][3:]
+
+ values[i1] = v2
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, dn1)
+ m[attr] = ldb.MessageElement(v2,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+
+ self.ldb.modify(m)
+
+ def test_server_vlv_fractions_with_cookie(self):
+ """What happens when the count is set to an arbitrary number?
+
+ In that case the offset and the count form a fraction, and the
+ VLV should be centred at a point offset/count of the way
+ through. For example, if offset is 3 and count is 6, the VLV
+ should be looking around halfway. The actual algorithm is a
+ bit fiddlier than that, because of the one-basedness of VLV.
+ """
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+
+ n_users = len(self.users)
+
+ random.seed(4)
+
+ for attr in attrs:
+ full_results, controls, sort_control = self.get_full_list(attr)
+ self.assertEqual(len(full_results), n_users)
+ for before in range(0, 2):
+ for after in range(0, 2):
+ for denominator in range(1, 20):
+ for offset in range(1, denominator + 3):
+ cookie = get_cookie(controls, len(self.users))
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset,
+ denominator,
+ cookie))
+ try:
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+ except ldb.LdbError as e:
+ if offset != 0:
+ raise
+ print("offset %d denominator %d raised error "
+ "expected error %s\n"
+ "(offset zero is illegal unless "
+ "content count is zero)" %
+ (offset, denominator, e))
+ continue
+
+ results = [str(x[attr][0]).lower() for x in res]
+
+ if denominator == 0:
+ denominator = n_users
+ if offset == 0:
+ offset = denominator
+ elif denominator == 1:
+ # the offset can only be 1, but the 1/1 case
+ # means something special
+ if offset == 1:
+ real_offset = n_users
+ else:
+ real_offset = 1
+ else:
+ if offset > denominator:
+ offset = denominator
+ real_offset = (1 +
+ int(round((n_users - 1) *
+ (offset - 1) /
+ (denominator - 1.0)))
+ )
+
+ self.assertCorrectResults(results, full_results,
+ real_offset, before,
+ after)
+
+ controls = res.controls
+ if False:
+ for c in list(controls):
+ cstr = str(c)
+ if cstr.startswith('vlv_resp'):
+ bits = cstr.rsplit(':')
+ print("the answer is %s; we said %d" %
+ (bits[2], real_offset))
+ break
+
+ def test_server_vlv_no_cookie(self):
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+
+ for attr in attrs:
+ expected_order = self.get_expected_order(attr)
+ sort_control = "server_sort:1:0:%s" % attr
+ for before in range(0, 5):
+ for after in range(0, 7):
+ for offset in range(1 + before, len(self.users) - after):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:%d:%d:%d:0" %
+ (before, after,
+ offset)])
+ results = [x[attr][0] for x in res]
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def get_expected_order_showing_deleted(self, attr,
+ expression="(|(cn=vlvtest*)(cn=vlv-deleted*))",
+ base=None,
+ scope=ldb.SCOPE_SUBTREE
+ ):
+ """Fetch the whole list sorted on the attribute, using sort only,
+ searching in the entire tree, not just our OU. This is the
+ way to find deleted objects.
+ """
+ if base is None:
+ base = self.base_dn
+ sort_control = "server_sort:1:0:%s" % attr
+ controls = [sort_control, "show_deleted:1"]
+
+ res = self.ldb.search(base,
+ scope=scope,
+ expression=expression,
+ attrs=[attr],
+ controls=controls)
+ results = [x[attr][0] for x in res]
+ return results
+
+ def add_deleted_users(self, n):
+ deleted_users = [self.create_user(i, n, prefix='vlv-deleted')
+ for i in range(n)]
+
+ for user in deleted_users:
+ self.delete_user(user)
+
+ def test_server_vlv_no_cookie_show_deleted(self):
+ """What do we see with the show_deleted control?"""
+ attrs = ['objectGUID',
+ 'cn',
+ 'sAMAccountName',
+ 'objectSid',
+ 'name',
+ 'whenChanged',
+ 'usnChanged'
+ ]
+
+ # add some deleted users first, just in case there are none
+ self.add_deleted_users(6)
+ random.seed(22)
+ expression = "(|(cn=vlvtest*)(cn=vlv-deleted*))"
+
+ for attr in attrs:
+ show_deleted_control = "show_deleted:1"
+ expected_order = self.get_expected_order_showing_deleted(attr,
+ expression)
+ n = len(expected_order)
+ sort_control = "server_sort:1:0:%s" % attr
+ for before in [3, 1, 0]:
+ for after in [0, 2]:
+ # don't test every position, because there could be hundreds.
+ # jump back and forth instead
+ for i in range(20):
+ offset = random.randrange(max(1, before - 2),
+ min(n - after + 2, n))
+ res = self.ldb.search(self.base_dn,
+ expression=expression,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr],
+ controls=[sort_control,
+ show_deleted_control,
+ "vlv:1:%d:%d:%d:0" %
+ (before, after,
+ offset)
+ ]
+ )
+ results = [x[attr][0] for x in res]
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def test_server_vlv_no_cookie_show_deleted_only(self):
+ """What do we see with the show_deleted control when we're not looking
+ at any non-deleted things"""
+ attrs = ['objectGUID',
+ 'cn',
+ 'sAMAccountName',
+ 'objectSid',
+ 'whenChanged',
+ ]
+
+ # add some deleted users first, just in case there are none
+ self.add_deleted_users(4)
+ base = 'CN=Deleted Objects,%s' % self.base_dn
+ expression = "(cn=vlv-deleted*)"
+ for attr in attrs:
+ show_deleted_control = "show_deleted:1"
+ expected_order = self.get_expected_order_showing_deleted(attr,
+ expression=expression,
+ base=base,
+ scope=ldb.SCOPE_ONELEVEL)
+ print("searching for attr %s amongst %d deleted objects" %
+ (attr, len(expected_order)))
+ sort_control = "server_sort:1:0:%s" % attr
+ step = max(len(expected_order) // 10, 1)
+ for before in [3, 0]:
+ for after in [0, 2]:
+ for offset in range(1 + before,
+ len(expected_order) - after,
+ step):
+ res = self.ldb.search(base,
+ expression=expression,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ show_deleted_control,
+ "vlv:1:%d:%d:%d:0" %
+ (before, after,
+ offset)])
+ results = [x[attr][0] for x in res]
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def test_server_vlv_with_cookie_show_deleted(self):
+ """What do we see with the show_deleted control?"""
+ attrs = ['objectGUID',
+ 'cn',
+ 'sAMAccountName',
+ 'objectSid',
+ 'name',
+ 'whenChanged',
+ 'usnChanged'
+ ]
+ self.add_deleted_users(6)
+ random.seed(23)
+ for attr in attrs:
+ expected_order = self.get_expected_order(attr)
+ sort_control = "server_sort:1:0:%s" % attr
+ res = None
+ show_deleted_control = "show_deleted:1"
+ expected_order = self.get_expected_order_showing_deleted(attr)
+ n = len(expected_order)
+ expression = "(|(cn=vlvtest*)(cn=vlv-deleted*))"
+ for before in [3, 2, 1, 0]:
+ after = before
+ for i in range(20):
+ offset = random.randrange(max(1, before - 2),
+ min(n - after + 2, n))
+ if res is None:
+ vlv_search = "vlv:1:%d:%d:%d:0" % (before, after,
+ offset)
+ else:
+ cookie = get_cookie(res.controls, n)
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset, n,
+ cookie))
+
+ res = self.ldb.search(self.base_dn,
+ expression=expression,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search,
+ show_deleted_control])
+
+ results = [x[attr][0] for x in res]
+
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def test_server_vlv_gte_with_cookie(self):
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ for attr in attrs:
+ gte_order, expected_order, gte_map = \
+ self.get_gte_tests_and_order(attr)
+ # In case there is some order dependency, disorder tests
+ gte_tests = gte_order[:]
+ random.seed(1)
+ random.shuffle(gte_tests)
+ res = None
+ sort_control = "server_sort:1:0:%s" % attr
+ for before in [0, 1, 2, 4]:
+ for after in [0, 1, 3, 6]:
+ for gte in gte_tests:
+ if res is not None:
+ cookie = get_cookie(res.controls, len(self.users))
+ else:
+ cookie = None
+ vlv_search = encode_vlv_control(before=before,
+ after=after,
+ gte=get_bytes(gte),
+ cookie=cookie)
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ results = [x[attr][0] for x in res]
+ offset = gte_map.get(gte, len(expected_order))
+
+ # here offset is 0-based
+ start = max(offset - before, 0)
+ end = offset + 1 + after
+
+ expected_results = expected_order[start: end]
+
+ self.assertEqual(expected_results, results)
+
+ def test_server_vlv_gte_no_cookie(self):
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ iteration = 0
+ for attr in attrs:
+ gte_order, expected_order, gte_map = \
+ self.get_gte_tests_and_order(attr)
+ # In case there is some order dependency, disorder tests
+ gte_tests = gte_order[:]
+ random.seed(1)
+ random.shuffle(gte_tests)
+
+ sort_control = "server_sort:1:0:%s" % attr
+ for before in [0, 1, 3]:
+ for after in [0, 4]:
+ for gte in gte_tests:
+ vlv_search = encode_vlv_control(before=before,
+ after=after,
+ gte=get_bytes(gte))
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+ results = [x[attr][0] for x in res]
+
+ # here offset is 0-based
+ offset = gte_map.get(gte, len(expected_order))
+ start = max(offset - before, 0)
+ end = offset + after + 1
+ expected_results = expected_order[start: end]
+ iteration += 1
+ if expected_results != results:
+ middle = expected_order[len(expected_order) // 2]
+ print(expected_results, results)
+ print(middle)
+ print(expected_order)
+ print()
+ print("\nattr %s offset %d before %d "
+ "after %d gte %s" %
+ (attr, offset, before, after, gte))
+ self.assertEqual(expected_results, results)
+
+ def test_multiple_searches(self):
+ """The maximum number of concurrent vlv searches per connection is
+ currently set at 3. That means if you open 4 VLV searches the
+ cookie on the first one should fail.
+ """
+ # Windows has a limit of 10 VLVs where there are low numbers
+ # of objects in each search.
+ attrs = ([x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')] * 2)[:12]
+
+ vlv_cookies = []
+ for attr in attrs:
+ sort_control = "server_sort:1:0:%s" % attr
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:1:1:1:0"])
+
+ cookie = get_cookie(res.controls, len(self.users))
+ vlv_cookies.append(cookie)
+ time.sleep(0.2)
+
+ # now this one should fail
+ self.assertRaises(ldb.LdbError,
+ self.ldb.search,
+ self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:1:1:1:0:%s" % vlv_cookies[0]])
+
+ # and this one should succeed
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:1:1:1:0:%s" % vlv_cookies[-1]])
+
+ # this one should fail because it is a new connection and
+ # doesn't share cookies
+ new_ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+ self.assertRaises(ldb.LdbError,
+ new_ldb.search, self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:1:1:1:0:%s" % vlv_cookies[-1]])
+
+ # but now without the critical flag it just does no VLV.
+ new_ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:0:1:1:1:0:%s" % vlv_cookies[-1]])
+
+ def test_vlv_modify_during_view(self):
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ # Edit a user
+ edit_index = len(self.users)//2
+ edit_attr = full_results[edit_index]
+ users_with_attr = [u for u in self.users if u[attr] == edit_attr]
+ self.assertEqual(len(users_with_attr), 1)
+ edit_user = users_with_attr[0]
+
+ # Put z at the front of the val so it comes last in ordering
+ edit_val = "z_" + edit_user[attr]
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, edit_user['dn'])
+ m[attr] = ldb.MessageElement(edit_val, ldb.FLAG_MOD_REPLACE, attr)
+ self.ldb.modify(m)
+
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users))
+
+ # Make expected_results by copying and editing full_results
+ expected_results = full_results[:]
+ expected_results[edit_index] = edit_val
+ self.assertEqual(results, expected_results)
+
+ # Test changing the search expression in a request on an initialised view
+ # Expected failure on samba, passes on windows
+ def test_vlv_change_search_expr(self):
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ middle_index = len(full_results)//2
+ # Search that excludes the old value but includes the new one
+ expr = "%s>=%s" % (attr, full_results[middle_index])
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users))
+ self.assertEqual(results, full_results[middle_index:])
+
+ # Check you can't add a value to a vlv view
+ def test_vlv_add_during_view(self):
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ # Add a user at the end of the sort order
+ add_val = "z_addedval"
+ user = {'cn': add_val, "objectclass": "user", attr: add_val}
+ user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
+ self.ldb.add(user)
+
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users)+1)
+ self.assertEqual(results, full_results)
+
+ def test_vlv_delete_during_view(self):
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ # Delete one of the users
+ del_index = len(self.users)//2
+ del_user = self.users[del_index]
+ self.ldb.delete(del_user['dn'])
+
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users))
+ expected_results = [r for r in full_results if r != del_user[attr]]
+ self.assertEqual(results, expected_results)
+
+ def test_vlv_change_during_search(self):
+ attr = 'facsimileTelephoneNumber'
+ prefix = "change_during_search_"
+ expr = "(&(objectClass=user)(cn=%s*))" % (prefix)
+ num_users = 3
+ users = [self.create_user(i, num_users, prefix=prefix)
+ for i in range(num_users)]
+ expr = "(&(objectClass=user)(facsimileTelephoneNumber=%s*))" % (prefix)
+
+ # Start the VLV, change the searched attribute and try the
+ # cookie.
+ results, cookie = self.vlv_search(attr, expr)
+
+ for u in users:
+ self.ldb.modify_ldif("dn: %s\n"
+ "changetype: modify\n"
+ "replace: facsimileTelephoneNumber\n"
+ "facsimileTelephoneNumber: 123" % u['dn'])
+
+ for i in range(2):
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ offset=i+1)
+
+
+
+class PagedResultsTests(TestsWithUserOU):
+
+ def paged_search(self, expr, cookie="", page_size=0, extra_ctrls=None,
+ attrs=None, ou=None, subtree=False, sort=True):
+ ou = ou or self.ou
+ if cookie:
+ cookie = ":" + cookie
+ ctrl = "paged_results:1:" + str(page_size) + cookie
+ controls = [ctrl]
+
+ # If extra controls are provided then add them, else default to
+ # sort control on 'cn' attribute
+ if extra_ctrls is not None:
+ controls += extra_ctrls
+ elif sort:
+ sort_ctrl = "server_sort:1:0:cn"
+ controls.append(sort_ctrl)
+
+ kwargs = {}
+ if attrs is not None:
+ kwargs = {"attrs": attrs}
+
+ scope = ldb.SCOPE_ONELEVEL
+ if subtree:
+ scope = ldb.SCOPE_SUBTREE
+
+ res = self.ldb_ro.search(ou,
+ expression=expr,
+ scope=scope,
+ controls=controls,
+ **kwargs)
+ results = [str(r['cn'][0]) for r in res]
+
+ ctrls = [str(c) for c in res.controls if
+ str(c).startswith("paged_results")]
+ assert len(ctrls) == 1, "no paged_results response"
+
+ spl = ctrls[0].rsplit(':', 3)
+ cookie = ""
+ if len(spl) == 3:
+ cookie = spl[-1]
+ return results, cookie
+
+
+class PagedResultsTestsRO(PagedResultsTests):
+
+ def test_paged_search_lockstep(self):
+ expr = "(objectClass=*)"
+ ps = 3
+
+ all_results, _ = self.paged_search(expr, page_size=len(self.users)+1)
+
+ # Run two different but overlapping paged searches simultaneously.
+ set_1_index = int((len(all_results))//3)
+ set_2_index = int((2*len(all_results))//3)
+ set_1 = all_results[set_1_index:]
+ set_2 = all_results[:set_2_index+1]
+ set_1_expr = "(cn>=%s)" % (all_results[set_1_index])
+ set_2_expr = "(cn<=%s)" % (all_results[set_2_index])
+
+ results, cookie1 = self.paged_search(set_1_expr, page_size=ps)
+ self.assertEqual(results, set_1[:ps])
+ results, cookie2 = self.paged_search(set_2_expr, page_size=ps)
+ self.assertEqual(results, set_2[:ps])
+
+ results, cookie1 = self.paged_search(set_1_expr, cookie=cookie1,
+ page_size=ps)
+ self.assertEqual(results, set_1[ps:ps*2])
+ results, cookie2 = self.paged_search(set_2_expr, cookie=cookie2,
+ page_size=ps)
+ self.assertEqual(results, set_2[ps:ps*2])
+
+ results, _ = self.paged_search(set_1_expr, cookie=cookie1,
+ page_size=len(self.users))
+ self.assertEqual(results, set_1[ps*2:])
+ results, _ = self.paged_search(set_2_expr, cookie=cookie2,
+ page_size=len(self.users))
+ self.assertEqual(results, set_2[ps*2:])
+
+
+class PagedResultsTestsGC(PagedResultsTestsRO):
+
+ def setUp(self):
+ super(PagedResultsTestsRO, self).setUp()
+ self.ldb_ro = SamDB(host + ":3268", credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+
+class PagedResultsTestsRW(PagedResultsTests):
+
+ def test_paged_delete_during_search(self, sort=True):
+ expr = "(objectClass=*)"
+
+ # Start new search
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, sort=sort,
+ page_size=first_page_size)
+
+ # Run normal search to get expected results
+ unedited_results, _ = self.paged_search(expr, sort=sort,
+ page_size=len(self.users))
+
+ # Get remaining users not returned by the search above
+ unreturned_users = [u for u in self.users if u['cn'] not in results]
+
+ # Delete one of the users
+ del_index = len(self.users)//2
+ del_user = unreturned_users[del_index]
+ self.ldb.delete(del_user['dn'])
+
+ # Run test
+ results, _ = self.paged_search(expr, cookie=cookie, sort=sort,
+ page_size=len(self.users))
+ expected_results = [r for r in unedited_results[first_page_size:]
+ if r != del_user['cn']]
+ self.assertEqual(results, expected_results)
+
+ def test_paged_delete_during_search_unsorted(self):
+ self.test_paged_delete_during_search(sort=False)
+
+ def test_paged_show_deleted(self):
+ unique = time.strftime("%s", time.gmtime())[-5:]
+ prefix = "show_deleted_test_%s_" % (unique)
+ expr = "(&(objectClass=user)(cn=%s*))" % (prefix)
+ del_ctrl = "show_deleted:1"
+
+ num_users = 10
+ users = []
+ for i in range(num_users):
+ user = self.create_user(i, num_users, prefix=prefix)
+ users.append(user)
+
+ first_user = users[0]
+ self.ldb.delete(first_user['dn'])
+
+ # Start new search
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, page_size=first_page_size,
+ extra_ctrls=[del_ctrl],
+ ou=self.base_dn,
+ subtree=True)
+
+ # Get remaining users not returned by the search above
+ unreturned_users = [u for u in users if u['cn'] not in results]
+
+ # Delete one of the users
+ del_index = len(users)//2
+ del_user = unreturned_users[del_index]
+ self.ldb.delete(del_user['dn'])
+
+ results2, _ = self.paged_search(expr, cookie=cookie,
+ page_size=len(users)*2,
+ extra_ctrls=[del_ctrl],
+ ou=self.base_dn,
+ subtree=True)
+
+ user_cns = {str(u['cn']) for u in users}
+ deleted_cns = {first_user['cn'], del_user['cn']}
+
+ all_results = results + results2
+ normal_results = {r for r in all_results if "DEL:" not in r}
+ self.assertEqual(normal_results, user_cns - deleted_cns)
+
+ # Deleted results get "\nDEL:<GUID>" added to the CN, so cut it out.
+ deleted_results = {r[:r.index('\n')] for r in all_results
+ if "DEL:" in r}
+ self.assertEqual(deleted_results, deleted_cns)
+
+ def test_paged_add_during_search(self, sort=True):
+ expr = "(objectClass=*)"
+
+ # Start new search
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, sort=sort,
+ page_size=first_page_size)
+
+ unedited_results, _ = self.paged_search(expr, sort=sort,
+ page_size=len(self.users)+1)
+
+ # Get remaining users not returned by the search above
+ unwalked_users = [cn for cn in unedited_results if cn not in results]
+
+ # Add a user in the middle of the sort order
+ middle_index = len(unwalked_users)//2
+ middle_user = unwalked_users[middle_index]
+
+ user = {'cn': middle_user + '_2', "objectclass": "user"}
+ user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
+ self.ldb.add(user)
+
+ results, _ = self.paged_search(expr, sort=sort, cookie=cookie,
+ page_size=len(self.users)+1)
+ expected_results = unwalked_users[:]
+
+ # Uncomment this line to assert that adding worked.
+ # expected_results.insert(middle_index+1, user['cn'])
+
+ self.assertEqual(results, expected_results)
+
+ # On Windows, when server_sort ctrl is NOT provided in the initial search,
+ # adding a record during the search will cause the modified record to
+ # be returned in a future page if it belongs there in the ordering.
+ # When server_sort IS provided, the added record will not be returned.
+ # Samba implements the latter behaviour. This test confirms Samba's
+ # implementation and will fail on Windows.
+ def test_paged_add_during_search_unsorted(self):
+ self.test_paged_add_during_search(sort=False)
+
+ def test_paged_modify_during_search(self, sort=True):
+ expr = "(objectClass=*)"
+
+ # Start new search
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, sort=sort,
+ page_size=first_page_size)
+
+ unedited_results, _ = self.paged_search(expr, sort=sort,
+ page_size=len(self.users)+1)
+
+ # Modify user in the middle of the remaining sort order
+ unwalked_users = [cn for cn in unedited_results if cn not in results]
+ middle_index = len(unwalked_users)//2
+ middle_cn = unwalked_users[middle_index]
+
+ # Find user object
+ users_with_middle_cn = [u for u in self.users if u['cn'] == middle_cn]
+ self.assertEqual(len(users_with_middle_cn), 1)
+ middle_user = users_with_middle_cn[0]
+
+ # Rename object
+ edit_cn = "z_" + middle_cn
+ new_dn = middle_user['dn'].replace(middle_cn, edit_cn)
+ self.ldb.rename(middle_user['dn'], new_dn)
+
+ results, _ = self.paged_search(expr, cookie=cookie, sort=sort,
+ page_size=len(self.users)+1)
+ expected_results = unwalked_users[:]
+ expected_results[middle_index] = edit_cn
+ self.assertEqual(results, expected_results)
+
+ # On Windows, when server_sort ctrl is NOT provided in the initial search,
+ # modifying a record during the search will cause the modified record to
+ # be returned in its new place in a CN ordering.
+ # When server_sort IS provided, the record will be returned its old place
+ # in the control-specified ordering.
+ # Samba implements the latter behaviour. This test confirms Samba's
+ # implementation and will fail on Windows.
+ def test_paged_modify_during_search_unsorted(self):
+ self.test_paged_modify_during_search(sort=False)
+
+ def test_paged_modify_object_scope(self):
+ expr = "(objectClass=*)"
+
+ ou2 = "OU=vlvtestou2,%s" % (self.tree_dn)
+ self.ldb.add({"dn": ou2, "objectclass": "organizationalUnit"})
+
+ # Do a separate, full search to get all results
+ unedited_results, _ = self.paged_search(expr,
+ page_size=len(self.users)+1)
+
+ # Rename before starting a search
+ first_cn = self.users[0]['cn']
+ new_dn = "CN=%s,%s" % (first_cn, ou2)
+ self.ldb.rename(self.users[0]['dn'], new_dn)
+
+ # Start new search under the original OU
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, page_size=first_page_size)
+ self.assertEqual(results, unedited_results[1:1+first_page_size])
+
+ # Get one of the users that is yet to be returned
+ unwalked_users = [cn for cn in unedited_results if cn not in results]
+ middle_index = len(unwalked_users)//2
+ middle_cn = unwalked_users[middle_index]
+
+ # Find user object
+ users_with_middle_cn = [u for u in self.users if u['cn'] == middle_cn]
+ self.assertEqual(len(users_with_middle_cn), 1)
+ middle_user = users_with_middle_cn[0]
+
+ # Rename
+ new_dn = "CN=%s,%s" % (middle_cn, ou2)
+ self.ldb.rename(middle_user['dn'], new_dn)
+
+ results, _ = self.paged_search(expr, cookie=cookie,
+ page_size=len(self.users)+1)
+
+ expected_results = unwalked_users[:]
+
+ # We should really expect that the object renamed into a different
+ # OU should vanish from the results, but turns out Windows does return
+ # the object in this case. Our module matches the Windows behaviour.
+
+ # If behaviour changes, this line inverts the test's expectations to
+ # what you might expect.
+ # del expected_results[middle_index]
+
+ # But still expect the user we removed before the search to be gone
+ del expected_results[0]
+
+ self.assertEqual(results, expected_results)
+
+ def test_paged_modify_one_during_search(self):
+ prefix = "change_during_search_"
+ num_users = 5
+ users = [self.create_user(i, num_users, prefix=prefix)
+ for i in range(num_users)]
+ expr = "(&(objectClass=user)(facsimileTelephoneNumber=%s*))" % (prefix)
+
+ # Get the first page, then change the searched attribute and
+ # try for the second page.
+ results, cookie = self.paged_search(expr, page_size=1)
+ self.assertEqual(len(results), 1)
+ unwalked_users = [u for u in users if u['cn'] != results[0]]
+ self.assertEqual(len(unwalked_users), num_users-1)
+
+ mod_dn = unwalked_users[0]['dn']
+ self.ldb.modify_ldif("dn: %s\n"
+ "changetype: modify\n"
+ "replace: facsimileTelephoneNumber\n"
+ "facsimileTelephoneNumber: 123" % mod_dn)
+
+ results, _ = self.paged_search(expr, cookie=cookie,
+ page_size=len(self.users))
+ expected_cns = {u['cn'] for u in unwalked_users if u['dn'] != mod_dn}
+ self.assertEqual(set(results), expected_cns)
+
+ def test_paged_modify_all_during_search(self):
+ prefix = "change_during_search_"
+ num_users = 5
+ users = [self.create_user(i, num_users, prefix=prefix)
+ for i in range(num_users)]
+ expr = "(&(objectClass=user)(facsimileTelephoneNumber=%s*))" % (prefix)
+
+ # Get the first page, then change the searched attribute and
+ # try for the second page.
+ results, cookie = self.paged_search(expr, page_size=1)
+ unwalked_users = [u for u in users if u['cn'] != results[0]]
+
+ for u in users:
+ self.ldb.modify_ldif("dn: %s\n"
+ "changetype: modify\n"
+ "replace: facsimileTelephoneNumber\n"
+ "facsimileTelephoneNumber: 123" % u['dn'])
+
+ results, _ = self.paged_search(expr, cookie=cookie,
+ page_size=len(self.users))
+ self.assertEqual(results, [])
+
+ def assertPagedSearchRaises(self, err_num, expr, cookie, attrs=None,
+ extra_ctrls=None):
+ try:
+ results, _ = self.paged_search(expr, cookie=cookie,
+ page_size=2,
+ extra_ctrls=extra_ctrls,
+ attrs=attrs)
+ except ldb.LdbError as e:
+ self.assertEqual(e.args[0], err_num)
+ return
+
+ self.fail("No error raised by invalid search")
+
+ def test_paged_changed_expr(self):
+ # Initiate search then use a different expr in subsequent req
+ expr = "(objectClass=*)"
+ results, cookie = self.paged_search(expr, page_size=3)
+ expr = "cn>=a"
+ expected_error_num = 12
+ self.assertPagedSearchRaises(expected_error_num, expr, cookie)
+
+ def test_paged_changed_controls(self):
+ expr = "(objectClass=*)"
+ sort_ctrl = "server_sort:1:0:cn"
+ del_ctrl = "show_deleted:1"
+ expected_error_num = 12
+ ps = 3
+
+ # Initiate search with a sort control then remove in subsequent req
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[sort_ctrl])
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[])
+
+ # Initiate search with no sort control then add one in subsequent req
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[])
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[sort_ctrl])
+
+ # Initiate search with show-deleted control then
+ # remove it in subsequent req
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[del_ctrl])
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[])
+
+ # Initiate normal search then add show-deleted control
+ # in subsequent req
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[])
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[del_ctrl])
+
+ # Changing order of controls shouldn't break the search
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[del_ctrl, sort_ctrl])
+ try:
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[sort_ctrl,
+ del_ctrl])
+ except ldb.LdbError as e:
+ self.fail(e)
+
+ def test_paged_cant_change_controls_data(self):
+ # Some defaults for the rest of the tests
+ expr = "(objectClass=*)"
+ sort_ctrl = "server_sort:1:0:cn"
+ expected_error_num = 12
+
+ # Initiate search with sort control then change it in subsequent req
+ results, cookie = self.paged_search(expr, page_size=3,
+ extra_ctrls=[sort_ctrl])
+ changed_sort_ctrl = "server_sort:1:0:roomNumber"
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[changed_sort_ctrl])
+
+ # Initiate search with a control with crit=1, then use crit=0
+ results, cookie = self.paged_search(expr, page_size=3,
+ extra_ctrls=[sort_ctrl])
+ changed_sort_ctrl = "server_sort:0:0:cn"
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[changed_sort_ctrl])
+
+ def test_paged_search_referrals(self):
+ expr = "(objectClass=*)"
+ paged_ctrl = "paged_results:1:5"
+ res = self.ldb.search(self.base_dn,
+ expression=expr,
+ attrs=['cn'],
+ scope=ldb.SCOPE_SUBTREE,
+ controls=[paged_ctrl])
+
+ # Do a paged search walk over the whole database and save a list
+ # of all the referrals returned by each search.
+ referral_lists = []
+
+ while True:
+ referral_lists.append(res.referals)
+
+ ctrls = [str(c) for c in res.controls if
+ str(c).startswith("paged_results")]
+ self.assertEqual(len(ctrls), 1)
+ spl = ctrls[0].rsplit(':')
+ if len(spl) != 3:
+ break
+
+ cookie = spl[-1]
+ res = self.ldb.search(self.base_dn,
+ expression=expr,
+ attrs=['cn'],
+ scope=ldb.SCOPE_SUBTREE,
+ controls=[paged_ctrl + ":" + cookie])
+
+ ref_list = referral_lists[0]
+
+ # Sanity check to make sure the search actually did something
+ self.assertGreater(len(referral_lists), 2)
+
+ # Check the first referral set contains stuff
+ self.assertGreater(len(ref_list), 0)
+
+ # Check the others don't
+ self.assertTrue(all([len(l) == 0 for l in referral_lists[1:]]))
+
+ # Check the entries in the first referral list look like referrals
+ self.assertTrue(all([s.startswith('ldap://') for s in ref_list]))
+
+ def test_paged_change_attrs(self):
+ expr = "(objectClass=*)"
+ attrs = ['cn']
+ expected_error_num = 12
+
+ results, cookie = self.paged_search(expr, page_size=3, attrs=attrs)
+ results, cookie = self.paged_search(expr, cookie=cookie, page_size=3,
+ attrs=attrs)
+
+ changed_attrs = attrs + ['roomNumber']
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, attrs=changed_attrs,
+ extra_ctrls=[])
+
+ def test_vlv_paged(self):
+ """Testing behaviour with VLV and paged_results set.
+
+ A strange combination, certainly
+
+ Thankfully combining both of these gives
+ unavailable-critical-extension against Windows 1709
+
+ """
+ sort_control = "server_sort:1:0:cn"
+
+ try:
+ msgs = self.ldb.search(base=self.base_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["vlv:1:20:20:11:0",
+ sort_control,
+ "paged_results:1:1024"])
+ self.fail("should have failed with LDAP_UNAVAILABLE_CRITICAL_EXTENSION")
+ except ldb.LdbError as e:
+ (enum, estr) = e.args
+ self.assertEqual(enum, ldb.ERR_UNSUPPORTED_CRITICAL_EXTENSION)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+TestProgram(module=__name__, opts=subunitopts)