--- /dev/null
+# 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 <http://www.gnu.org/licenses/>.
+
+
+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 <server> [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}")
+
+ 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),
+ ),
+
+ ("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()