# Unix SMB/CIFS implementation. # # Copyright 2021 (C) Catalyst IT 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 . import sys import os import pprint import re from samba.samdb import SamDB from samba.auth import system_session import ldb from samba.sd_utils import SDUtils from samba.credentials import DONT_USE_KERBEROS, Credentials from samba.gensec import FEATURE_SEAL from samba.tests.subunitrun import SubunitOptions, TestProgram from samba.tests import TestCase, ldb_err from samba.tests import DynamicTestCase import samba.getopt as options import optparse from samba.colour import c_RED, c_GREEN, c_DARK_YELLOW from samba.dsdb import ( UF_SERVER_TRUST_ACCOUNT, UF_TRUSTED_FOR_DELEGATION, ) SPN_GUID = 'f3a64788-5306-11d1-a9c5-0000f80367c1' RELEVANT_ATTRS = {'dNSHostName', 'servicePrincipalName', 'sAMAccountName', 'dn'} ok = True bad = False report = 'report' operr = ldb.ERR_OPERATIONS_ERROR denied = ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS constraint = ldb.ERR_CONSTRAINT_VIOLATION exists = ldb.ERR_ENTRY_ALREADY_EXISTS add = ldb.FLAG_MOD_ADD replace = ldb.FLAG_MOD_REPLACE delete = ldb.FLAG_MOD_DELETE try: breakpoint except NameError: # for python <= 3.6 def breakpoint(): import pdb pdb.set_trace() def init(): # This needs to happen before the class definition, and we put it # in a function to keep the namespace clean. global LP, CREDS, SERVER, REALM, COLOUR_TEXT, subunitopts, FILTER parser = optparse.OptionParser( "python3 ldap_spn.py [options]") sambaopts = options.SambaOptions(parser) parser.add_option_group(sambaopts) # 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('--colour', action="store_true", help="use colour text", default=sys.stdout.isatty()) parser.add_option('--filter', help="only run tests matching this regex") opts, args = parser.parse_args() if len(args) != 1: parser.print_usage() sys.exit(1) LP = sambaopts.get_loadparm() CREDS = credopts.get_credentials(LP) SERVER = args[0] REALM = CREDS.get_realm() COLOUR_TEXT = opts.colour FILTER = opts.filter init() def colour_text(x, state=None): if not COLOUR_TEXT: return x if state == 'error': return c_RED(x) if state == 'pass': return c_GREEN(x) return c_DARK_YELLOW(x) def get_samdb(creds=None): if creds is None: creds = CREDS session = system_session() else: session = None return SamDB(url=f"ldap://{SERVER}", lp=LP, session_info=session, credentials=creds) def add_unpriv_user(samdb, ou, username, writeable_objects=None, password="samba123@"): creds = Credentials() creds.set_username(username) creds.set_password(password) creds.set_domain(CREDS.get_domain()) creds.set_realm(CREDS.get_realm()) creds.set_workstation(CREDS.get_workstation()) creds.set_gensec_features(CREDS.get_gensec_features() | FEATURE_SEAL) creds.set_kerberos_state(DONT_USE_KERBEROS) dnstr = f"CN={username},{ou}" # like, WTF, samdb.newuser(), this is what you make us do. short_ou = ou.split(',', 1)[0] samdb.newuser(username, password, userou=short_ou) if writeable_objects: sd_utils = SDUtils(samdb) sid = sd_utils.get_object_sid(dnstr) for obj in writeable_objects: mod = f"(OA;CI;WP;{ SPN_GUID };;{ sid })" sd_utils.dacl_add_ace(obj, mod) unpriv_samdb = get_samdb(creds=creds) return unpriv_samdb class LdapSpnTestBase(TestCase): _disabled = False @classmethod def setUpDynamicTestCases(cls): if getattr(cls, '_disabled', False): return for doc, *rows in cls.cases: if FILTER: if not re.search(FILTER, doc): continue name = re.sub(r'\W+', '_', doc) cls.generate_dynamic_test("test_spn", name, rows, doc) def setup_objects(self, rows): objects = set(r[0] for r in rows) for name in objects: if ':' in name: objtype, name = name.split(':', 1) else: objtype = 'dc' getattr(self, f'add_{objtype}')(name) def setup_users(self, rows): # When you are adding an SPN that aliases (or would be aliased # by) another SPN on another object, you need to have write # permission on that other object too. # # To test this negatively and positively, we need to have # users with various combinations of write permission, which # means fiddling with SDs on the objects. # # The syntax is: # '' : user with no special permissions # '*' : admin user # 'A' : user can write to A only # 'A,C' : user can write to A and C # 'C,A' : same, but makes another user self.userdbs = { '*': self.samdb } permissions = set(r[2] for r in rows) for p in permissions: if p == '*': continue if p == '': user = 'nobody' writeable_objects = None else: user = 'writes_' + p.replace(",", '_') writeable_objects = [self.objects[x][0] for x in p.split(',')] self.userdbs[p] = add_unpriv_user(self.samdb, self.ou, user, writeable_objects) def _test_spn_with_args(self, rows, doc): cdoc = colour_text(doc) edoc = colour_text(doc, 'error') pdoc = colour_text(doc, 'pass') if COLOUR_TEXT: sys.stderr.flush() print('\n', c_DARK_YELLOW('#' * 10), f'starting «{cdoc}»\n') sys.stdout.flush() self.samdb = get_samdb() self.base_dn = self.samdb.get_default_basedn() self.short_id = self.id().rsplit('.', 1)[1][:63] self.objects = {} self.ou = f"OU={ self.short_id },{ self.base_dn }" self.addCleanup(self.samdb.delete, self.ou, ["tree_delete:1"]) self.samdb.create_ou(self.ou) self.setup_objects(rows) self.setup_users(rows) for i, row in enumerate(rows): if len(row) == 5: obj, data, rights, expected, op = row else: obj, data, rights, expected = row op = ldb.FLAG_MOD_REPLACE # We use this DB with possibly restricted rights for this row samdb = self.userdbs[rights] if ':' in obj: objtype, obj = obj.split(':', 1) else: objtype = 'dc' dn, dnsname = self.objects[obj] m = {"dn": dn} if isinstance(data, dict): m.update(data) else: m['servicePrincipalName'] = data # for python's sake (and our sanity) we try to ensure we # have consistent canonical case in our attributes keys = set(m.keys()) if not keys.issubset(RELEVANT_ATTRS): raise ValueError(f"unexpected attr {keys - RELEVANT_ATTRS}. " "Casefold typo?") for k in ('dNSHostName', 'servicePrincipalName'): if isinstance(m.get(k), str): m[k] = m[k].format(dnsname=f"x.{REALM}") elif isinstance(m.get(k), list): m[k] = [x.format(dnsname=f"x.{REALM}") for x in m[k]] msg = ldb.Message.from_dict(samdb, m, op) if expected is bad: try: samdb.modify(msg) except ldb.LdbError as e: print(f"row {i+1} of '{pdoc}' failed as expected with " f"{ldb_err(e)}\n") continue self.fail(f"row {i+1}: " f"{rights} {pprint.pformat(m)} on {objtype} {obj} " f"should fail ({edoc})") elif expected is ok: try: samdb.modify(msg) except ldb.LdbError as e: self.fail(f"row {i+1} of {edoc} failed with {ldb_err(e)}:\n" f"{rights} {pprint.pformat(m)} on {objtype} {obj}") elif expected is report: try: self.samdb.modify(msg) print(f"row {i+1} " f"of '{cdoc}' {colour_text('SUCCEEDED', 'pass')}:\n" f"{pprint.pformat(m)} on {obj}") except ldb.LdbError as e: print(f"row {i+1} " f"of '{cdoc}' {colour_text('FAILED', 'error')} " f"with {ldb_err(e)}:\n{pprint.pformat(m)} on {obj}") elif expected is breakpoint: try: breakpoint() samdb.modify(msg) except ldb.LdbError as e: print(f"row {i+1} of '{pdoc}' FAILED with {ldb_err(e)}\n") else: # an ldb error number try: samdb.modify(msg) except ldb.LdbError as e: if e.args[0] == expected: continue self.fail(f"row {i+1} of '{edoc}' " f"should have failed with {ldb_err(expected)}:\n" f"not {ldb_err(e)}:\n" f"{rights} {pprint.pformat(m)} on {objtype} {obj}") self.fail(f"row {i+1} of '{edoc}' " f"should have failed with {ldb_err(expected)}:\n" f"{rights} {pprint.pformat(m)} on {objtype} {obj}") def add_dc(self, name): dn = f"CN={name},OU=Domain Controllers,{self.base_dn}" dnsname = f"{name}.{REALM}".lower() self.samdb.add({ "dn": dn, "objectclass": "computer", "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT | UF_TRUSTED_FOR_DELEGATION), "dnsHostName": dnsname, "carLicense": self.id() }) self.addCleanup(self.remove_object, name) self.objects[name] = (dn, dnsname) def add_user(self, name): dn = f"CN={name},{self.ou}" self.samdb.add({ "dn": dn, "name": name, "samAccountName": name, "objectclass": "user", "carLicense": self.id() }) self.addCleanup(self.remove_object, name) self.objects[name] = (dn, None) def remove_object(self, name): dn, dnsname = self.objects.pop(name) self.samdb.delete(dn) @DynamicTestCase class LdapSpnTest(LdapSpnTestBase): """Make sure we can't add clashing servicePrincipalNames. This would be possible using sPNMappings aliases — for example, if the mapping maps host/ to cifs/, we should not be able to add different addresses for each. """ # default sPNMappings: host=alerter, appmgmt, cisvc, clipsrv, # browser, dhcp, dnscache, replicator, eventlog, eventsystem, # policyagent, oakley, dmserver, dns, mcsvc, fax, msiserver, ias, # messenger, netlogon, netman, netdde, netddedsm, nmagent, # plugplay, protectedstorage, rasman, rpclocator, rpc, rpcss, # remoteaccess, rsvp, samss, scardsvr, scesrv, seclogon, scm, # dcom, cifs, spooler, snmp, schedule, tapisrv, trksvr, trkwks, # ups, time, wins, www, http, w3svc, iisadmin, msdtc # # I think in practice this is rarely if ever changed or added to. cases = [ ("add one as admin", ('A', 'host/{dnsname}', '*', ok), ), ("add one as rightful user", ('A', 'host/{dnsname}', 'A', ok), ), ("attempt to add one as nobody", ('A', 'host/{dnsname}', '', denied), ), ("add and replace as admin", ('A', 'host/{dnsname}', '*', ok), ('A', 'host/x.{dnsname}', '*', ok), ), ("replace as rightful user", ('A', 'host/{dnsname}', 'A', ok), ('A', 'host/x.{dnsname}', 'A', ok), ), ("attempt to replace one as nobody", ('A', 'host/{dnsname}', '*', ok), ('A', 'host/x.{dnsname}', '', denied), ), ("add second as admin", ('A', 'host/{dnsname}', '*', ok), ('A', 'host/x.{dnsname}', '*', ok, add), ), ("add second as rightful user", ('A', 'host/{dnsname}', 'A', ok), ('A', 'host/x.{dnsname}', 'A', ok, add), ), ("attempt to add second as nobody", ('A', 'host/{dnsname}', '*', ok), ('A', 'host/x.{dnsname}', '', denied, add), ), ("add the same one twice, simple duplicate error", ('A', 'host/{dnsname}', '*', ok), ('A', 'host/{dnsname}', '*', bad, add), ), ("simple duplicate attributes, as non-admin", ('A', 'host/{dnsname}', '*', ok), ('A', 'host/{dnsname}', 'A', bad, add), ), ("add the same one twice, identical duplicate", ('A', 'host/{dnsname}', '*', ok), ('A', 'host/{dnsname}', '*', bad, add), ), ("add a conflict, host first, as nobody", ('A', 'host/z.{dnsname}', '*', ok), ('B', 'cifs/z.{dnsname}', '', denied), ), ("add a conflict, service first, as nobody", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'host/{dnsname}', '', denied), ), ("three way conflict, host first, as admin", ('A', 'host/z.{dnsname}', '*', ok), ('B', 'cifs/z.{dnsname}', '*', ok), ('C', 'www/z.{dnsname}', '*', ok), ), ("three way conflict, host first, with sufficient rights", ('A', 'host/z.{dnsname}', 'A', ok), ('B', 'cifs/z.{dnsname}', 'B,A', ok), ('C', 'www/z.{dnsname}', 'C,A', ok), ), ("three way conflict, host first, adding duplicate", ('A', 'host/z.{dnsname}', 'A', ok), ('B', 'cifs/z.{dnsname}', 'B,A', ok), ('C', 'cifs/z.{dnsname}', 'C,A', bad), ), ("three way conflict, host first, adding duplicate, full rights", ('A', 'host/z.{dnsname}', 'A', ok), ('B', 'cifs/z.{dnsname}', 'B,A', ok), ('C', 'cifs/z.{dnsname}', 'C,B,A', bad), ), ("three way conflict, host first, with other write rights", ('A', 'host/z.{dnsname}', '*', ok), ('B', 'cifs/z.{dnsname}', 'A,B', ok), ('C', 'cifs/z.{dnsname}', 'A,B', bad), ), ("three way conflict, host first, as nobody", ('A', 'host/z.{dnsname}', '*', ok), ('B', 'cifs/z.{dnsname}', '*', ok), ('C', 'www/z.{dnsname}', '', denied), ), ("three way conflict, services first, as admin", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'www/{dnsname}', '*', ok), ('C', 'host/{dnsname}', '*', constraint), ), ("three way conflict, services first, with service write rights", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'www/{dnsname}', '*', ok), ('C', 'host/{dnsname}', 'A,B', bad), ), ("three way conflict, service first, as nobody", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'www/{dnsname}', '*', ok), ('C', 'host/{dnsname}', '', denied), ), ("replace host before specific", ('A', 'host/{dnsname}', '*', ok), ('A', 'cifs/{dnsname}', '*', ok), ), ("replace host after specific, as nobody", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', '', denied), ), ("non-conflict host before specific", ('A', 'host/{dnsname}', '*', ok), ('A', 'cifs/{dnsname}', '*', ok, add), ), ("non-conflict host after specific", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', '*', ok, add), ), ("non-conflict host before specific, non-admin", ('A', 'host/{dnsname}', 'A', ok), ('A', 'cifs/{dnsname}', 'A', ok, add), ), ("non-conflict host after specific, as nobody", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', '', denied, add), ), ("add a conflict, host first on user, as admin", ('user:C', 'host/{dnsname}', '*', ok), ('B', 'cifs/{dnsname}', '*', ok), ), ("add a conflict, host first on user, host rights", ('user:C', 'host/{dnsname}', '*', ok), ('B', 'cifs/{dnsname}', 'C', denied), ), ("add a conflict, host first on user, both rights", ('user:C', 'host/{dnsname}', '*', ok), ('B', 'cifs/{dnsname}', 'B,C', ok), ), ("add a conflict, host first both on user", ('user:C', 'host/{dnsname}', '*', ok), ('user:D', 'www/{dnsname}', '*', ok), ), ("add a conflict, host first both on user, host rights", ('user:C', 'host/{dnsname}', '*', ok), ('user:D', 'www/{dnsname}', 'C', denied), ), ("add a conflict, host first both on user, both rights", ('user:C', 'host/{dnsname}', '*', ok), ('user:D', 'www/{dnsname}', 'C,D', ok), ), ("add a conflict, host first both on user, as nobody", ('user:C', 'host/{dnsname}', '*', ok), ('user:D', 'www/{dnsname}', '', denied), ), ("add a conflict, host first, with both write rights", ('A', 'host/z.{dnsname}', '*', ok), ('B', 'cifs/z.{dnsname}', 'A,B', ok), ), ("add a conflict, host first, second on user, as admin", ('A', 'host/{dnsname}', '*', ok), ('user:D', 'cifs/{dnsname}', '*', ok), ), ("add a conflict, host first, second on user, with rights", ('A', 'host/{dnsname}', '*', ok), ('user:D', 'cifs/{dnsname}', 'A,D', ok), ), ("nonsense SPNs, part 1, as admin", ('A', 'a-b-c/{dnsname}', '*', ok), ('A', 'rrrrrrrrrrrrr /{dnsname}', '*', ok), ), ("nonsense SPNs, part 1, as user", ('A', 'a-b-c/{dnsname}', 'A', ok), ('A', 'rrrrrrrrrrrrr /{dnsname}', 'A', ok), ), ("nonsense SPNs, part 1, as nobody", ('A', 'a-b-c/{dnsname}', '', denied), ('A', 'rrrrrrrrrrrrr /{dnsname}', '', denied), ), ("add a conflict, using port", ('A', 'dns/{dnsname}', '*', ok), ('B', 'dns/{dnsname}:53', '*', ok), ), ("add a conflict, using port, port first", ('user:C', 'dns/{dnsname}:53', '*', ok), ('user:D', 'dns/{dnsname}', '*', ok), ), ("three part spns", ('A', {'dNSHostName': '{dnsname}'}, '*', ok), ('A', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', ok), ('B', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', constraint), ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok), ('B', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', ok), ('B', 'cifs/y.{dnsname}/DomainDNSZones.{dnsname}', '*', constraint), ), ("three part nonsense spns", ('A', {'dNSHostName': 'bean'}, '*', ok), ('A', 'cifs/bean/DomainDNSZones.bean', '*', ok), ('B', 'cifs/bean/DomainDNSZones.bean', '*', constraint), ('A', {'dNSHostName': 'y.bean'}, '*', ok), ('B', 'cifs/bean/DomainDNSZones.bean', '*', ok), ('B', 'cifs/y.bean/DomainDNSZones.bean', '*', constraint), ('C', 'host/bean/bean', '*', ok), ), ("one part spns (no slashes)", ('A', '{dnsname}', '*', constraint), ('B', 'cifs', '*', constraint), ('B', 'cifs/', '*', ok), ('B', ' ', '*', constraint), ('user:C', 'host', '*', constraint), ), ("dodgy spns", # These tests pass on Windows. An SPN must have one or two # slashes, with at least one character before the first one, # UNLESS the first slash is followed by a good enough service # name (e.g. "/host/x.y" rather than "sdfsd/x.y"). ('A', '\\/{dnsname}', '*', ok), ('B', 'cifs/\\\\{dnsname}', '*', ok), ('B', r'cifs/\\\{dnsname}', '*', ok), ('B', r'cifs/\\\{dnsname}/', '*', ok), ('A', r'cīfs/\\\{dnsname}/', '*', constraint), # 'ī' maps to 'i' # on the next two, full-width solidus (U+FF0F) does not work # as '/'. ('A', 'cifs/sfic', '*', constraint, add), ('A', r'cifs/\\\{dnsname}', '*', constraint, add), ('B', '\n', '*', constraint), ('B', '\n/\n', '*', ok), ('B', '\n/\n/\n', '*', ok), ('B', '\n/\n/\n/\n', '*', constraint), ('B', ' /* and so on */ ', '*', ok, add), ('B', r'¯\_(ツ)_/¯', '*', ok, add), # ¯\_(ツ)_/¯ # つ is hiragana for katakana ツ, so the next one fails for # something analogous to casefold reasons. ('A', r'¯\_(つ)_/¯', '*', constraint), ('A', r'¯\_(㋡)_/¯', '*', constraint), # circled ツ ('B', '//', '*', constraint), # all can't be empty, ('B', ' //', '*', ok), # service can be space ('B', '/host/{dnsname}', '*', ok), # or empty if others aren't ('B', '/host/x.y.z', '*', ok), ('B', '/ /x.y.z', '*', ok), ('B', ' / / ', '*', ok), ('user:C', b'host/', '*', ok), ('user:C', ' /host', '*', ok), # service is ' ' (space) ('B', ' /host', '*', constraint), # already on C ('B', ' /HōST', '*', constraint), # ō equiv to O ('B', ' /ħØşt', '*', constraint), # maps to ' /host' ('B', ' /H0ST', '*', ok), # 0 is zero ('B', ' /НoST', '*', ok), # Cyrillic Н (~N) ('B', ' /host', '*', ok), # two space ('B', '\u00a0/host', '*', ok), # non-breaking space ('B', ' 2/HōST/⌷[ ][]¨(', '*', ok), ('B', ' (//)', '*', ok, add), ('B', ' ///', '*', constraint), ('B', r' /\//', '*', constraint), # escape doesn't help ('B', ' /\\//', '*', constraint), # double escape doesn't help ('B', r'\//', '*', ok), ('A', r'\\/\\/', '*', ok), ('B', '|//|', '*', ok, add), ('B', r'\/\/\\', '*', ok, add), ('A', ':', '*', constraint), ('A', ':/:', '*', ok), ('A', ':/:80', '*', ok), # port number syntax is not special ('A', ':/:( ツ', '*', ok), ('A', ':/:/:', '*', ok), ('B', b'cifs/\x11\xaa\xbb\xcc\\example.com', '*', ok), ('A', b':/\xcc\xcc\xcc\xcc', '*', ok), ('A', b':/b\x00/b/b/b', '*', ok), # string handlng truncates at \x00 ('A', b'a@b/a@b/a@b', '*', ok), ('A', b'a/a@b/a@b', '*', ok), ), ("empty part spns (consecutive slashes)", ('A', 'cifs//{dnsname}', '*', ok), ('B', 'cifs//{dnsname}', '*', bad), # should clash with line 1 ('B', 'cifs/zzzy.{dnsname}/', '*', ok), ('B', '/host/zzzy.{dnsname}', '*', ok), ), ("too many spn parts", ('A', 'cifs/{dnsname}/{dnsname}/{dnsname}', '*', bad), ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok), ('B', 'cifs/{dnsname}/{dnsname}/', '*', bad), ('B', 'cifs/y.{dnsname}/{dnsname}/toop', '*', bad), ('B', 'host/{dnsname}/a/b/c', '*', bad), ), ("add a conflict, host first, as admin", ('A', 'host/z.{dnsname}', '*', ok), ('B', 'cifs/z.{dnsname}', '*', ok), ), ("add a conflict, host first, with host write rights", ('A', 'host/z.{dnsname}', '*', ok), ('B', 'cifs/z.{dnsname}', 'A', denied), ), ("add a conflict, service first, with service write rights", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'host/{dnsname}', 'A', denied), ), ("adding dNSHostName after cifs with no old dNSHostName", ('A', 'cifs/{dnsname}', '*', ok), ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok), ('B', 'cifs/{dnsname}', '*', constraint), ('B', 'cifs/y.{dnsname}', '*', ok), ('B', 'host/y.{dnsname}', '*', ok), ), ("changing dNSHostName after cifs", ('A', {'dNSHostName': '{dnsname}'}, '*', ok), ('A', 'cifs/{dnsname}', '*', ok), ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok), ('B', 'cifs/{dnsname}', '*', ok), ('B', 'cifs/y.{dnsname}', '*', bad), ('B', 'host/y.{dnsname}', '*', bad), ), ] @DynamicTestCase class LdapSpnSambaOnlyTest(LdapSpnTestBase): # We don't run these ones outside of selftest, where we are # probably testing against Windows and these are known failures. _disabled = 'SAMBA_SELFTEST' not in os.environ cases = [ ("add a conflict, host first, with service write rights", ('A', 'host/z.{dnsname}', '*', ok), ('B', 'cifs/z.{dnsname}', 'B', denied), ), ("add a conflict, service first, with host write rights", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'host/{dnsname}', 'B', constraint), ), ("add a conflict, service first, as admin", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'host/{dnsname}', '*', constraint), ), ("add a conflict, service first, with both write rights", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'host/{dnsname}', 'A,B', constraint), ), ("add a conflict, host first both on user, service rights", ('user:C', 'host/{dnsname}', '*', ok), ('user:D', 'www/{dnsname}', 'D', denied), ), ("add a conflict, along with a re-added SPN", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'cifs/heeble.example.net', 'B', ok), ('B', ['cifs/heeble.example.net', 'host/{dnsname}'], 'B', constraint), ), ("changing dNSHostName after host", ('A', {'dNSHostName': '{dnsname}'}, '*', ok), ('A', 'host/{dnsname}', '*', ok), ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok), ('B', 'cifs/{dnsname}', 'B', ok), # no clash with A ('B', 'cifs/y.{dnsname}', 'B', bad), # should clash with A ('B', 'host/y.{dnsname}', '*', bad), ), ("mystery dnsname clash, host first", ('user:C', 'host/heeble.example.net', '*', ok), ('user:D', 'www/heeble.example.net', '*', ok), ), ("mystery dnsname clash, www first", ('user:D', 'www/heeble.example.net', '*', ok), ('user:C', 'host/heeble.example.net', '*', constraint), ), ("replace as admin", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', '*', ok), ('A', 'cifs/{dnsname}', '*', ok), ), ("replace as non-admin with rights", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', 'A', ok), ('A', 'cifs/{dnsname}', 'A', ok), ), ("replace vial delete as non-admin with rights", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', 'A', ok), ('A', 'host/{dnsname}', 'A', ok, delete), ('A', 'cifs/{dnsname}', 'A', ok, add), ), ("replace as non-admin without rights", ('B', 'cifs/b', '*', ok), ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', 'B', denied), ('A', 'cifs/{dnsname}', 'B', denied), ), ("replace as nobody", ('B', 'cifs/b', '*', ok), ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', '', denied), ('A', 'cifs/{dnsname}', '', denied), ), ("accumulate and delete as admin", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', '*', ok, add), ('A', 'www/{dnsname}', '*', ok, add), ('A', 'www/...', '*', ok, add), ('A', 'host/...', '*', ok, add), ('A', 'www/{dnsname}', '*', ok, delete), ('A', 'host/{dnsname}', '*', ok, delete), ('A', 'host/{dnsname}', '*', ok, add), ('A', 'www/{dnsname}', '*', ok, add), ('A', 'host/...', '*', ok, delete), ), ("accumulate and delete with user rights", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'host/{dnsname}', 'A', ok, add), ('A', 'www/{dnsname}', 'A', ok, add), ('A', 'www/...', 'A', ok, add), ('A', 'host/...', 'A', ok, add), ('A', 'www/{dnsname}', 'A', ok, delete), ('A', 'host/{dnsname}', 'A', ok, delete), ('A', 'host/{dnsname}', 'A', ok, add), ('A', 'www/{dnsname}', 'A', ok, add), ('A', 'host/...', 'A', ok, delete), ), ("three way conflict, host first, with partial write rights", ('A', 'host/z.{dnsname}', 'A', ok), ('B', 'cifs/z.{dnsname}', 'B', denied), ('C', 'www/z.{dnsname}', 'C', denied), ), ("three way conflict, host first, with partial write rights 2", ('A', 'host/z.{dnsname}', 'A', ok), ('B', 'cifs/z.{dnsname}', 'B', bad), ('C', 'www/z.{dnsname}', 'C,A', ok), ), ("three way conflict sandwich, sufficient rights", ('B', 'host/{dnsname}', 'B', ok), ('A', 'cifs/{dnsname}', 'A,B', ok), # the replaces don't fail even though they appear to affect A # and B, because they are effectively no-ops, leaving # everything as it was before. ('A', 'cifs/{dnsname}', 'A', ok), ('B', 'host/{dnsname}', 'B', ok), ('C', 'www/{dnsname}', 'A,B,C', ok), ('C', 'www/{dnsname}', 'B,C', ok), # because B already has host/, C doesn't matter ('B', 'host/{dnsname}', 'A,B', ok), # removing host (via replace) frees others, needs B only ('B', 'ldap/{dnsname}', 'B', ok), ('C', 'www/{dnsname}', 'C', ok), ('A', 'cifs/{dnsname}', 'A', ok), # re-adding host is now impossible while A and C have {dnsname} spns ('B', 'host/{dnsname}', '*', bad), ('B', 'host/{dnsname}', 'A,B,C', bad), # so let's remove those... (not needing B rights) ('C', 'www/{dnsname}', 'C', ok, delete), ('A', 'cifs/{dnsname}', 'A', ok, delete), # and now we can add host/ again ('B', 'host/{dnsname}', 'B', ok), ('C', 'www/{dnsname}', 'B,C', ok, add), ('A', 'cifs/{dnsname}', 'A,B', ok), ), ("three way conflict, service first, with all write rights", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'www/{dnsname}', 'A,B,C', ok), ('C', 'host/{dnsname}', 'A,B,C', bad), ), ("three way conflict, service first, just sufficient rights", ('A', 'cifs/{dnsname}', 'A', ok), ('B', 'www/{dnsname}', 'B', ok), ('C', 'host/{dnsname}', 'A,B,C', bad), ), ("three way conflict, service first, with host write rights", ('A', 'cifs/{dnsname}', '*', ok), ('B', 'www/{dnsname}', '*', ok), ('C', 'host/{dnsname}', 'C', bad), ), ("three way conflict, service first, with both write rights", ('A', 'cifs/{dnsname}', '*', ok), ('A', 'cifs/{dnsname}', '*', ok, delete), ('A', 'www/{dnsname}', 'A,B,C', ok), ('B', 'host/{dnsname}', 'A,B', bad), ('A', 'www/{dnsname}', 'A', ok, delete), ('B', 'host/{dnsname}', 'A,B', ok), ('C', 'cifs/{dnsname}', 'C', bad), ('C', 'cifs/{dnsname}', 'B,C', ok), ), ("three way conflict, services first, with partial rights", ('A', 'cifs/{dnsname}', 'A,C', ok), ('B', 'www/{dnsname}', '*', ok), ('C', 'host/{dnsname}', 'A,C', bad), ), ] @DynamicTestCase class LdapSpnAmbitiousTest(LdapSpnTestBase): _disabled = True cases = [ ("add a conflict with port, host first both on user", ('user:C', 'host/{dnsname}', '*', ok), ('user:D', 'www/{dnsname}:80', '*', bad), ), # see https://bugzilla.samba.org/show_bug.cgi?id=8929 ("add the same one twice, case-insensitive duplicate", ('A', 'host/{dnsname}', '*', ok), ('A', 'Host/{dnsname}', '*', bad, add), ), ("special SPN", # should fail because we don't have all the DSA infrastructure ('A', ("E3514235-4B06-11D1-AB04-00C04FC2DCD2/" "75b84f00-a81b-4a19-8ef2-8e483cccff11/" "{dnsname}"), '*', constraint) ), ("single part SPNs matching sAMAccountName", # setting them both together is allegedly a MacOS behaviour, # but all we get from Windows is a mysterious NO_SUCH_OBJECT. ('user:A', {'sAMAccountName': 'A', 'servicePrincipalName': 'A'}, '*', ldb.ERR_NO_SUCH_OBJECT), ('user:B', {'sAMAccountName': 'B'}, '*', ok), ('user:B', {'servicePrincipalName': 'B'}, '*', constraint), ('user:C', {'servicePrincipalName': 'C'}, '*', constraint), ('user:C', {'sAMAccountName': 'C'}, '*', ok), ), ("three part spns with dnsHostName", ('A', {'dNSHostName': '{dnsname}'}, '*', ok), ('A', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', ok), ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok), ('B', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', ok), ('B', 'cifs/y.{dnsname}/DomainDNSZones.{dnsname}', '*', constraint), ('C', 'host/{y.dnsname}/{y.dnsname}', '*', constraint), ('A', 'host/y.{dnsname}/{dnsname}', '*', constraint), ), ] def main(): TestProgram(module=__name__, opts=subunitopts) main()