1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# This tests the restrictions on userAccountControl that apply even if write access is permitted
4#
5# Copyright Samuel Cabrero 2014 <samuelcabrero@kernevil.me>
6# Copyright Andrew Bartlett 2014 <abartlet@samba.org>
7#
8# Licenced under the GPLv3
9#
10
11from __future__ import print_function
12import optparse
13import sys
14import unittest
15import samba
16import samba.getopt as options
17import samba.tests
18import ldb
19import base64
20
21sys.path.insert(0, "bin/python")
22from samba.tests.subunitrun import TestProgram, SubunitOptions
23
24from samba.subunit.run import SubunitTestRunner
25from samba.auth import system_session
26from samba.samdb import SamDB
27from samba.dcerpc import samr, security, lsa
28from samba.credentials import Credentials
29from samba.ndr import ndr_unpack, ndr_pack
30from samba.tests import delete_force
31from samba import gensec, sd_utils
32from samba.credentials import DONT_USE_KERBEROS
33from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
34from ldb import Message, MessageElement, Dn
35from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
36from samba.dsdb import UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED, \
37    UF_LOCKOUT, UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,\
38    UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400, UF_INTERDOMAIN_TRUST_ACCOUNT, \
39    UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000, \
40    UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED, \
41    UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY, UF_DONT_REQUIRE_PREAUTH, \
42    UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_NO_AUTH_DATA_REQUIRED, \
43    UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS
44
45
46parser = optparse.OptionParser("user_account_control.py [options] <host>")
47sambaopts = options.SambaOptions(parser)
48parser.add_option_group(sambaopts)
49parser.add_option_group(options.VersionOptions(parser))
50
51# use command line creds if available
52credopts = options.CredentialsOptions(parser)
53parser.add_option_group(credopts)
54opts, args = parser.parse_args()
55
56if len(args) < 1:
57    parser.print_usage()
58    sys.exit(1)
59host = args[0]
60
61if "://" not in host:
62    ldaphost = "ldap://%s" % host
63else:
64    ldaphost = host
65    start = host.rindex("://")
66    host = host.lstrip(start + 3)
67
68lp = sambaopts.get_loadparm()
69creds = credopts.get_credentials(lp)
70creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
71
72bits = [UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED,
73        UF_LOCKOUT, UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE,
74        UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
75        UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400,
76        UF_INTERDOMAIN_TRUST_ACCOUNT,
77        UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000,
78        UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED,
79        UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY,
80        UF_DONT_REQUIRE_PREAUTH,
81        UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
82        UF_NO_AUTH_DATA_REQUIRED,
83        UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS,
84        int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)]
85
86account_types = set([UF_NORMAL_ACCOUNT, UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT])
87
88
89class UserAccountControlTests(samba.tests.TestCase):
90    def add_computer_ldap(self, computername, others=None, samdb=None):
91        if samdb is None:
92            samdb = self.samdb
93        dn = "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn)
94        domainname = ldb.Dn(self.samdb, self.samdb.domain_dn()).canonical_str().replace("/", "")
95        samaccountname = "%s$" % computername
96        dnshostname = "%s.%s" % (computername, domainname)
97        msg_dict = {
98            "dn": dn,
99            "objectclass": "computer"}
100        if others is not None:
101            msg_dict = dict(list(msg_dict.items()) + list(others.items()))
102
103        msg = ldb.Message.from_dict(self.samdb, msg_dict)
104        msg["sAMAccountName"] = samaccountname
105
106        print("Adding computer account %s" % computername)
107        samdb.add(msg)
108
109    def get_creds(self, target_username, target_password):
110        creds_tmp = Credentials()
111        creds_tmp.set_username(target_username)
112        creds_tmp.set_password(target_password)
113        creds_tmp.set_domain(creds.get_domain())
114        creds_tmp.set_realm(creds.get_realm())
115        creds_tmp.set_workstation(creds.get_workstation())
116        creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
117                                      | gensec.FEATURE_SEAL)
118        creds_tmp.set_kerberos_state(DONT_USE_KERBEROS)  # kinit is too expensive to use in a tight loop
119        return creds_tmp
120
121    def setUp(self):
122        super(UserAccountControlTests, self).setUp()
123        self.admin_creds = creds
124        self.admin_samdb = SamDB(url=ldaphost,
125                                 session_info=system_session(),
126                                 credentials=self.admin_creds, lp=lp)
127        self.domain_sid = security.dom_sid(self.admin_samdb.get_domain_sid())
128        self.base_dn = self.admin_samdb.domain_dn()
129
130        self.unpriv_user = "testuser1"
131        self.unpriv_user_pw = "samba123@"
132        self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw)
133
134        delete_force(self.admin_samdb, "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn))
135        delete_force(self.admin_samdb, "OU=test_computer_ou1,%s" % (self.base_dn))
136        delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn))
137
138        self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw)
139        res = self.admin_samdb.search("CN=%s,CN=Users,%s" % (self.unpriv_user, self.admin_samdb.domain_dn()),
140                                      scope=SCOPE_BASE,
141                                      attrs=["objectSid"])
142        self.assertEqual(1, len(res))
143
144        self.unpriv_user_sid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0])
145        self.unpriv_user_dn = res[0].dn
146
147        self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
148
149        self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, self.unpriv_creds)
150        self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
151        self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
152
153        self.sd_utils = sd_utils.SDUtils(self.admin_samdb)
154
155        self.admin_samdb.create_ou("OU=test_computer_ou1," + self.base_dn)
156        self.unpriv_user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
157        mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid)
158
159        old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
160
161        self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
162
163        self.add_computer_ldap("testcomputer-t")
164
165        self.sd_utils.modify_sd_on_dn("OU=test_computer_ou1," + self.base_dn, old_sd)
166
167        self.computernames = ["testcomputer-0"]
168
169        # Get the SD of the template account, then force it to match
170        # what we expect for SeMachineAccountPrivilege accounts, so we
171        # can confirm we created the accounts correctly
172        self.sd_reference_cc = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn))
173
174        self.sd_reference_modify = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn))
175        for ace in self.sd_reference_modify.dacl.aces:
176            if ace.type == security.SEC_ACE_TYPE_ACCESS_ALLOWED and ace.trustee == self.unpriv_user_sid:
177                ace.access_mask = ace.access_mask | security.SEC_ADS_SELF_WRITE | security.SEC_ADS_WRITE_PROP
178
179        # Now reconnect without domain admin rights
180        self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
181
182    def tearDown(self):
183        super(UserAccountControlTests, self).tearDown()
184        for computername in self.computernames:
185            delete_force(self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn))
186        delete_force(self.admin_samdb, "CN=testcomputer-t,OU=test_computer_ou1,%s" % (self.base_dn))
187        delete_force(self.admin_samdb, "OU=test_computer_ou1,%s" % (self.base_dn))
188        delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn))
189
190    def test_add_computer_sd_cc(self):
191        user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
192        mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
193
194        old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
195
196        self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
197
198        computername = self.computernames[0]
199        sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)),
200                                ldb.FLAG_MOD_ADD,
201                                "nTSecurityDescriptor")
202        self.add_computer_ldap(computername,
203                               others={"nTSecurityDescriptor": sd})
204
205        res = self.admin_samdb.search("%s" % self.base_dn,
206                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
207                                      scope=SCOPE_SUBTREE,
208                                      attrs=["ntSecurityDescriptor"])
209
210        desc = res[0]["nTSecurityDescriptor"][0]
211        desc = ndr_unpack(security.descriptor, desc, allow_remaining=True)
212
213        sddl = desc.as_sddl(self.domain_sid)
214        self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl)
215
216        m = ldb.Message()
217        m.dn = res[0].dn
218        m["description"] = ldb.MessageElement(
219            ("A description"), ldb.FLAG_MOD_REPLACE,
220            "description")
221        self.samdb.modify(m)
222
223        m = ldb.Message()
224        m.dn = res[0].dn
225        m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT),
226                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
227        try:
228            self.samdb.modify(m)
229            self.fail("Unexpectedly able to set userAccountControl to be a DC on %s" % m.dn)
230        except LdbError as e5:
231            (enum, estr) = e5.args
232            self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
233
234        m = ldb.Message()
235        m.dn = res[0].dn
236        m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
237                                                         samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT),
238                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
239        try:
240            self.samdb.modify(m)
241            self.fail("Unexpectedly able to set userAccountControl to be an RODC on %s" % m.dn)
242        except LdbError as e6:
243            (enum, estr) = e6.args
244            self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
245
246        m = ldb.Message()
247        m.dn = res[0].dn
248        m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
249                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
250        try:
251            self.samdb.modify(m)
252            self.fail("Unexpectedly able to set userAccountControl to be an Workstation on %s" % m.dn)
253        except LdbError as e7:
254            (enum, estr) = e7.args
255            self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
256
257        m = ldb.Message()
258        m.dn = res[0].dn
259        m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT),
260                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
261        self.samdb.modify(m)
262
263        m = ldb.Message()
264        m.dn = res[0].dn
265        m["primaryGroupID"] = ldb.MessageElement(str(security.DOMAIN_RID_ADMINS),
266                                                 ldb.FLAG_MOD_REPLACE, "primaryGroupID")
267        try:
268            self.samdb.modify(m)
269        except LdbError as e8:
270            (enum, estr) = e8.args
271            self.assertEqual(ldb.ERR_UNWILLING_TO_PERFORM, enum)
272            return
273        self.fail()
274
275    def test_mod_computer_cc(self):
276        user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
277        mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
278
279        old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
280
281        self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
282
283        computername = self.computernames[0]
284        self.add_computer_ldap(computername)
285
286        res = self.admin_samdb.search("%s" % self.base_dn,
287                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
288                                      scope=SCOPE_SUBTREE,
289                                      attrs=[])
290
291        m = ldb.Message()
292        m.dn = res[0].dn
293        m["description"] = ldb.MessageElement(
294            ("A description"), ldb.FLAG_MOD_REPLACE,
295            "description")
296        self.samdb.modify(m)
297
298        m = ldb.Message()
299        m.dn = res[0].dn
300        m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
301                                                         samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT),
302                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
303        try:
304            self.samdb.modify(m)
305            self.fail("Unexpectedly able to set userAccountControl on %s" % m.dn)
306        except LdbError as e9:
307            (enum, estr) = e9.args
308            self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
309
310        m = ldb.Message()
311        m.dn = res[0].dn
312        m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT),
313                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
314        try:
315            self.samdb.modify(m)
316            self.fail()
317        except LdbError as e10:
318            (enum, estr) = e10.args
319            self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
320
321        m = ldb.Message()
322        m.dn = res[0].dn
323        m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT),
324                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
325        self.samdb.modify(m)
326
327        m = ldb.Message()
328        m.dn = res[0].dn
329        m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
330                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
331        try:
332            self.samdb.modify(m)
333            self.fail("Unexpectedly able to set userAccountControl to be an Workstation on %s" % m.dn)
334        except LdbError as e11:
335            (enum, estr) = e11.args
336            self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
337
338    def test_admin_mod_uac(self):
339        computername = self.computernames[0]
340        self.add_computer_ldap(computername, samdb=self.admin_samdb)
341
342        res = self.admin_samdb.search("%s" % self.base_dn,
343                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
344                                      scope=SCOPE_SUBTREE,
345                                      attrs=["userAccountControl"])
346
347        self.assertEqual(int(res[0]["userAccountControl"][0]), (UF_NORMAL_ACCOUNT |
348                                                                UF_ACCOUNTDISABLE |
349                                                                UF_PASSWD_NOTREQD))
350
351        m = ldb.Message()
352        m.dn = res[0].dn
353        m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT |
354                                                         UF_PARTIAL_SECRETS_ACCOUNT |
355                                                         UF_TRUSTED_FOR_DELEGATION),
356                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
357        try:
358            self.admin_samdb.modify(m)
359            self.fail("Unexpectedly able to set userAccountControl to UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT|UF_TRUSTED_FOR_DELEGATION on %s" % m.dn)
360        except LdbError as e12:
361            (enum, estr) = e12.args
362            self.assertEqual(ldb.ERR_OTHER, enum)
363
364        m = ldb.Message()
365        m.dn = res[0].dn
366        m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT |
367                                                         UF_PARTIAL_SECRETS_ACCOUNT),
368                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
369        self.admin_samdb.modify(m)
370
371        res = self.admin_samdb.search("%s" % self.base_dn,
372                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
373                                      scope=SCOPE_SUBTREE,
374                                      attrs=["userAccountControl"])
375
376        self.assertEqual(int(res[0]["userAccountControl"][0]), (UF_WORKSTATION_TRUST_ACCOUNT |
377                                                                UF_PARTIAL_SECRETS_ACCOUNT))
378        m = ldb.Message()
379        m.dn = res[0].dn
380        m["userAccountControl"] = ldb.MessageElement(str(UF_ACCOUNTDISABLE),
381                                                     ldb.FLAG_MOD_REPLACE, "userAccountControl")
382        self.admin_samdb.modify(m)
383
384        res = self.admin_samdb.search("%s" % self.base_dn,
385                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
386                                      scope=SCOPE_SUBTREE,
387                                      attrs=["userAccountControl"])
388
389        self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE)
390
391    def test_uac_bits_set(self):
392        user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
393        mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
394
395        old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
396
397        self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
398
399        computername = self.computernames[0]
400        self.add_computer_ldap(computername)
401
402        res = self.admin_samdb.search("%s" % self.base_dn,
403                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
404                                      scope=SCOPE_SUBTREE,
405                                      attrs=[])
406
407        m = ldb.Message()
408        m.dn = res[0].dn
409        m["description"] = ldb.MessageElement(
410            ("A description"), ldb.FLAG_MOD_REPLACE,
411            "description")
412        self.samdb.modify(m)
413
414        # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test
415        priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
416                                       UF_DONT_EXPIRE_PASSWD])
417
418        # These bits really are privileged, or can't be changed from UF_NORMAL as a non-admin
419        priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
420                         UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
421                         UF_WORKSTATION_TRUST_ACCOUNT])
422
423        invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
424
425        for bit in bits:
426            m = ldb.Message()
427            m.dn = res[0].dn
428            m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD),
429                                                         ldb.FLAG_MOD_REPLACE, "userAccountControl")
430            try:
431                self.samdb.modify(m)
432                if (bit in priv_bits):
433                    self.fail("Unexpectedly able to set userAccountControl bit 0x%08X on %s" % (bit, m.dn))
434            except LdbError as e:
435                (enum, estr) = e.args
436                if bit in invalid_bits:
437                    self.assertEqual(enum, ldb.ERR_OTHER, "was not able to set 0x%08X on %s" % (bit, m.dn))
438                    # No point going on, try the next bit
439                    continue
440                elif (bit in priv_bits):
441                    self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
442                else:
443                    self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
444
445    def uac_bits_unrelated_modify_helper(self, account_type):
446        user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
447        mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
448
449        old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
450
451        self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
452
453        computername = self.computernames[0]
454        self.add_computer_ldap(computername, others={"userAccountControl": [str(account_type)]})
455
456        res = self.admin_samdb.search("%s" % self.base_dn,
457                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
458                                      scope=SCOPE_SUBTREE,
459                                      attrs=["userAccountControl"])
460        self.assertEqual(int(res[0]["userAccountControl"][0]), account_type)
461
462        m = ldb.Message()
463        m.dn = res[0].dn
464        m["description"] = ldb.MessageElement(
465            ("A description"), ldb.FLAG_MOD_REPLACE,
466            "description")
467        self.samdb.modify(m)
468
469        invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
470
471        # UF_LOCKOUT isn't actually ignored, it changes other
472        # attributes but does not stick here.  See MS-SAMR 2.2.1.13
473        # UF_FLAG Codes clarification that UF_SCRIPT and
474        # UF_PASSWD_CANT_CHANGE are simply ignored by both clients and
475        # servers.  Other bits are ignored as they are undefined, or
476        # are not set into the attribute (instead triggering other
477        # events).
478        ignored_bits = set([UF_SCRIPT, UF_00000004, UF_LOCKOUT, UF_PASSWD_CANT_CHANGE,
479                            UF_00000400, UF_00004000, UF_00008000, UF_PASSWORD_EXPIRED,
480                            int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)])
481        super_priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT])
482
483        priv_to_remove_bits = set([UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_WORKSTATION_TRUST_ACCOUNT])
484
485        for bit in bits:
486            # Reset this to the initial position, just to be sure
487            m = ldb.Message()
488            m.dn = res[0].dn
489            m["userAccountControl"] = ldb.MessageElement(str(account_type),
490                                                         ldb.FLAG_MOD_REPLACE, "userAccountControl")
491            self.admin_samdb.modify(m)
492
493            res = self.admin_samdb.search("%s" % self.base_dn,
494                                          expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
495                                          scope=SCOPE_SUBTREE,
496                                          attrs=["userAccountControl"])
497
498            self.assertEqual(int(res[0]["userAccountControl"][0]), account_type)
499
500            m = ldb.Message()
501            m.dn = res[0].dn
502            m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD),
503                                                         ldb.FLAG_MOD_REPLACE, "userAccountControl")
504            try:
505                self.admin_samdb.modify(m)
506                if bit in invalid_bits:
507                    self.fail("Should have been unable to set userAccountControl bit 0x%08X on %s" % (bit, m.dn))
508
509            except LdbError as e1:
510                (enum, estr) = e1.args
511                if bit in invalid_bits:
512                    self.assertEqual(enum, ldb.ERR_OTHER)
513                    # No point going on, try the next bit
514                    continue
515                elif bit in super_priv_bits:
516                    self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
517                    # No point going on, try the next bit
518                    continue
519                else:
520                    self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
521
522            res = self.admin_samdb.search("%s" % self.base_dn,
523                                          expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
524                                          scope=SCOPE_SUBTREE,
525                                          attrs=["userAccountControl"])
526
527            if bit in ignored_bits:
528                self.assertEqual(int(res[0]["userAccountControl"][0]),
529                                 UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD,
530                                 "Bit 0x%08x shouldn't stick" % bit)
531            else:
532                if bit in account_types:
533                    self.assertEqual(int(res[0]["userAccountControl"][0]),
534                                     bit | UF_PASSWD_NOTREQD,
535                                     "Bit 0x%08x didn't stick" % bit)
536                else:
537                    self.assertEqual(int(res[0]["userAccountControl"][0]),
538                                     bit | UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD,
539                                     "Bit 0x%08x didn't stick" % bit)
540
541            try:
542                m = ldb.Message()
543                m.dn = res[0].dn
544                m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE),
545                                                             ldb.FLAG_MOD_REPLACE, "userAccountControl")
546                self.samdb.modify(m)
547
548            except LdbError as e2:
549                (enum, estr) = e2.args
550                self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
551
552            res = self.admin_samdb.search("%s" % self.base_dn,
553                                          expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
554                                          scope=SCOPE_SUBTREE,
555                                          attrs=["userAccountControl"])
556
557            if bit in account_types:
558                self.assertEqual(int(res[0]["userAccountControl"][0]),
559                                 bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
560                                 "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
561                                 % (bit, int(res[0]["userAccountControl"][0]),
562                                    bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
563            elif bit in ignored_bits:
564                self.assertEqual(int(res[0]["userAccountControl"][0]),
565                                 UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
566                                 "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
567                                 % (bit, int(res[0]["userAccountControl"][0]),
568                                    UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
569
570            else:
571                self.assertEqual(int(res[0]["userAccountControl"][0]),
572                                 bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
573                                 "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
574                                 % (bit, int(res[0]["userAccountControl"][0]),
575                                    bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
576
577            try:
578                m = ldb.Message()
579                m.dn = res[0].dn
580                m["userAccountControl"] = ldb.MessageElement(str(UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE),
581                                                             ldb.FLAG_MOD_REPLACE, "userAccountControl")
582                self.samdb.modify(m)
583                if bit in priv_to_remove_bits:
584                    self.fail("Should have been unable to remove userAccountControl bit 0x%08X on %s" % (bit, m.dn))
585
586            except LdbError as e3:
587                (enum, estr) = e3.args
588                if bit in priv_to_remove_bits:
589                    self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
590                else:
591                    self.fail("Unexpectedly unable to remove userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
592
593            res = self.admin_samdb.search("%s" % self.base_dn,
594                                          expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
595                                          scope=SCOPE_SUBTREE,
596                                          attrs=["userAccountControl"])
597
598            if bit in priv_to_remove_bits:
599                if bit in account_types:
600                    self.assertEqual(int(res[0]["userAccountControl"][0]),
601                                     bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
602                                     "bit 0X%08x should not have been removed" % bit)
603                else:
604                    self.assertEqual(int(res[0]["userAccountControl"][0]),
605                                     bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
606                                     "bit 0X%08x should not have been removed" % bit)
607            else:
608                self.assertEqual(int(res[0]["userAccountControl"][0]),
609                                 UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
610                                 "bit 0X%08x should have been removed" % bit)
611
612    def test_uac_bits_unrelated_modify_normal(self):
613        self.uac_bits_unrelated_modify_helper(UF_NORMAL_ACCOUNT)
614
615    def test_uac_bits_unrelated_modify_workstation(self):
616        self.uac_bits_unrelated_modify_helper(UF_WORKSTATION_TRUST_ACCOUNT)
617
618    def test_uac_bits_add(self):
619        computername = self.computernames[0]
620
621        user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
622        mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
623
624        old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
625
626        self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
627
628        invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
629        # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test
630        priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
631                                       UF_DONT_EXPIRE_PASSWD])
632
633        # These bits really are privileged
634        priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
635                         UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION])
636
637        for bit in bits:
638            try:
639                self.add_computer_ldap(computername, others={"userAccountControl": [str(bit)]})
640                delete_force(self.admin_samdb, "CN=%s,OU=test_computer_ou1,%s" % (computername, self.base_dn))
641                if bit in priv_bits:
642                    self.fail("Unexpectdly able to set userAccountControl bit 0x%08X on %s" % (bit, computername))
643
644            except LdbError as e4:
645                (enum, estr) = e4.args
646                if bit in invalid_bits:
647                    self.assertEqual(enum, ldb.ERR_OTHER, "Invalid bit 0x%08X was able to be set on %s" % (bit, computername))
648                    # No point going on, try the next bit
649                    continue
650                elif bit in priv_bits:
651                    self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
652                    continue
653                else:
654                    self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, computername, estr))
655
656    def test_primarygroupID_cc_add(self):
657        computername = self.computernames[0]
658
659        user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
660        mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
661
662        old_sd = self.sd_utils.read_sd_on_dn("OU=test_computer_ou1," + self.base_dn)
663
664        self.sd_utils.dacl_add_ace("OU=test_computer_ou1," + self.base_dn, mod)
665        try:
666            # When creating a new object, you can not ever set the primaryGroupID
667            self.add_computer_ldap(computername, others={"primaryGroupID": [str(security.DOMAIN_RID_ADMINS)]})
668            self.fail("Unexpectedly able to set primaryGruopID to be an admin on %s" % computername)
669        except LdbError as e13:
670            (enum, estr) = e13.args
671            self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
672
673    def test_primarygroupID_priv_DC_modify(self):
674        computername = self.computernames[0]
675
676        self.add_computer_ldap(computername,
677                               others={"userAccountControl": [str(UF_SERVER_TRUST_ACCOUNT)]},
678                               samdb=self.admin_samdb)
679        res = self.admin_samdb.search("%s" % self.base_dn,
680                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
681                                      scope=SCOPE_SUBTREE,
682                                      attrs=[""])
683
684        m = ldb.Message()
685        m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
686                                                         security.DOMAIN_RID_USERS))
687        m["member"] = ldb.MessageElement(
688            [str(res[0].dn)], ldb.FLAG_MOD_ADD,
689            "member")
690        self.admin_samdb.modify(m)
691
692        m = ldb.Message()
693        m.dn = res[0].dn
694        m["primaryGroupID"] = ldb.MessageElement(
695            [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE,
696            "primaryGroupID")
697        try:
698            self.admin_samdb.modify(m)
699
700            # When creating a new object, you can not ever set the primaryGroupID
701            self.fail("Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername)
702        except LdbError as e14:
703            (enum, estr) = e14.args
704            self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
705
706    def test_primarygroupID_priv_member_modify(self):
707        computername = self.computernames[0]
708
709        self.add_computer_ldap(computername,
710                               others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)]},
711                               samdb=self.admin_samdb)
712        res = self.admin_samdb.search("%s" % self.base_dn,
713                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
714                                      scope=SCOPE_SUBTREE,
715                                      attrs=[""])
716
717        m = ldb.Message()
718        m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
719                                                         security.DOMAIN_RID_USERS))
720        m["member"] = ldb.MessageElement(
721            [str(res[0].dn)], ldb.FLAG_MOD_ADD,
722            "member")
723        self.admin_samdb.modify(m)
724
725        m = ldb.Message()
726        m.dn = res[0].dn
727        m["primaryGroupID"] = ldb.MessageElement(
728            [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE,
729            "primaryGroupID")
730        try:
731            self.admin_samdb.modify(m)
732
733            # When creating a new object, you can not ever set the primaryGroupID
734            self.fail("Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername)
735        except LdbError as e15:
736            (enum, estr) = e15.args
737            self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
738
739    def test_primarygroupID_priv_user_modify(self):
740        computername = self.computernames[0]
741
742        self.add_computer_ldap(computername,
743                               others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT)]},
744                               samdb=self.admin_samdb)
745        res = self.admin_samdb.search("%s" % self.base_dn,
746                                      expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
747                                      scope=SCOPE_SUBTREE,
748                                      attrs=[""])
749
750        m = ldb.Message()
751        m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
752                                                         security.DOMAIN_RID_ADMINS))
753        m["member"] = ldb.MessageElement(
754            [str(res[0].dn)], ldb.FLAG_MOD_ADD,
755            "member")
756        self.admin_samdb.modify(m)
757
758        m = ldb.Message()
759        m.dn = res[0].dn
760        m["primaryGroupID"] = ldb.MessageElement(
761            [str(security.DOMAIN_RID_ADMINS)], ldb.FLAG_MOD_REPLACE,
762            "primaryGroupID")
763        self.admin_samdb.modify(m)
764
765
766runner = SubunitTestRunner()
767rc = 0
768if not runner.run(unittest.makeSuite(UserAccountControlTests)).wasSuccessful():
769    rc = 1
770sys.exit(rc)
771