ctdb-scripts: Do not de-duplicate the interfaces list
[samba.git] / python / samba / tests / ldap_spn.py
1 # Unix SMB/CIFS implementation.
2 #
3 # Copyright 2021 (C) Catalyst IT Ltd
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18
19 import sys
20 import os
21 import pprint
22 import re
23 from samba.samdb import SamDB
24 from samba.auth import system_session
25 import ldb
26 from samba.sd_utils import SDUtils
27 from samba.credentials import DONT_USE_KERBEROS, Credentials
28 from samba.gensec import FEATURE_SEAL
29 from samba.tests.subunitrun import SubunitOptions, TestProgram
30 from samba.tests import TestCase, ldb_err
31 from samba.tests import DynamicTestCase
32 import samba.getopt as options
33 import optparse
34 from samba.colour import c_RED, c_GREEN, c_DARK_YELLOW
35 from samba.dsdb import (
36     UF_SERVER_TRUST_ACCOUNT,
37     UF_TRUSTED_FOR_DELEGATION,
38 )
39
40
41 SPN_GUID = 'f3a64788-5306-11d1-a9c5-0000f80367c1'
42
43 RELEVANT_ATTRS = {'dNSHostName',
44                   'servicePrincipalName',
45                   'sAMAccountName',
46                   'dn'}
47
48 ok = True
49 bad = False
50 report = 'report'
51
52 operr = ldb.ERR_OPERATIONS_ERROR
53 denied = ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS
54 constraint = ldb.ERR_CONSTRAINT_VIOLATION
55 exists = ldb.ERR_ENTRY_ALREADY_EXISTS
56
57 add = ldb.FLAG_MOD_ADD
58 replace = ldb.FLAG_MOD_REPLACE
59 delete = ldb.FLAG_MOD_DELETE
60
61 try:
62     breakpoint
63 except NameError:
64     # for python <= 3.6
65     def breakpoint():
66         import pdb
67         pdb.set_trace()
68
69
70 def init():
71     # This needs to happen before the class definition, and we put it
72     # in a function to keep the namespace clean.
73     global LP, CREDS, SERVER, REALM, COLOUR_TEXT, subunitopts, FILTER
74
75     parser = optparse.OptionParser(
76         "python3 ldap_spn.py <server> [options]")
77     sambaopts = options.SambaOptions(parser)
78     parser.add_option_group(sambaopts)
79
80     # use command line creds if available
81     credopts = options.CredentialsOptions(parser)
82     parser.add_option_group(credopts)
83     subunitopts = SubunitOptions(parser)
84     parser.add_option_group(subunitopts)
85
86     parser.add_option('--colour', action="store_true",
87                       help="use colour text",
88                       default=sys.stdout.isatty())
89
90     parser.add_option('--filter', help="only run tests matching this regex")
91
92     opts, args = parser.parse_args()
93     if len(args) != 1:
94         parser.print_usage()
95         sys.exit(1)
96
97     LP = sambaopts.get_loadparm()
98     CREDS = credopts.get_credentials(LP)
99     SERVER = args[0]
100     REALM = CREDS.get_realm()
101     COLOUR_TEXT = opts.colour
102     FILTER = opts.filter
103
104
105 init()
106
107
108 def colour_text(x, state=None):
109     if not COLOUR_TEXT:
110         return x
111     if state == 'error':
112         return c_RED(x)
113     if state == 'pass':
114         return c_GREEN(x)
115
116     return c_DARK_YELLOW(x)
117
118
119 def get_samdb(creds=None):
120     if creds is None:
121         creds = CREDS
122         session = system_session()
123     else:
124         session = None
125
126     return SamDB(url=f"ldap://{SERVER}",
127                  lp=LP,
128                  session_info=session,
129                  credentials=creds)
130
131
132 def add_unpriv_user(samdb, ou, username,
133                     writeable_objects=None,
134                     password="samba123@"):
135     creds = Credentials()
136     creds.set_username(username)
137     creds.set_password(password)
138     creds.set_domain(CREDS.get_domain())
139     creds.set_realm(CREDS.get_realm())
140     creds.set_workstation(CREDS.get_workstation())
141     creds.set_gensec_features(CREDS.get_gensec_features() | FEATURE_SEAL)
142     creds.set_kerberos_state(DONT_USE_KERBEROS)
143     dnstr = f"CN={username},{ou}"
144
145     # like, WTF, samdb.newuser(), this is what you make us do.
146     short_ou = ou.split(',', 1)[0]
147
148     samdb.newuser(username, password, userou=short_ou)
149
150     if writeable_objects:
151         sd_utils = SDUtils(samdb)
152         sid = sd_utils.get_object_sid(dnstr)
153         for obj in writeable_objects:
154             mod = f"(OA;CI;WP;{ SPN_GUID };;{ sid })"
155             sd_utils.dacl_add_ace(obj, mod)
156
157     unpriv_samdb = get_samdb(creds=creds)
158     return unpriv_samdb
159
160
161 class LdapSpnTestBase(TestCase):
162     _disabled = False
163
164     @classmethod
165     def setUpDynamicTestCases(cls):
166         if getattr(cls, '_disabled', False):
167             return
168         for doc, *rows in cls.cases:
169             if FILTER:
170                 if not re.search(FILTER, doc):
171                     continue
172             name = re.sub(r'\W+', '_', doc)
173             cls.generate_dynamic_test("test_spn", name, rows, doc)
174
175     def setup_objects(self, rows):
176         objects = set(r[0] for r in rows)
177         for name in objects:
178             if ':' in name:
179                 objtype, name = name.split(':', 1)
180             else:
181                 objtype = 'dc'
182             getattr(self, f'add_{objtype}')(name)
183
184     def setup_users(self, rows):
185         # When you are adding an SPN that aliases (or would be aliased
186         # by) another SPN on another object, you need to have write
187         # permission on that other object too.
188         #
189         # To test this negatively and positively, we need to have
190         # users with various combinations of write permission, which
191         # means fiddling with SDs on the objects.
192         #
193         # The syntax is:
194         #   ''    :  user with no special permissions
195         #   '*'   :  admin user
196         #   'A'   :  user can write to A only
197         #   'A,C' :  user can write to A and C
198         #   'C,A' :  same, but makes another user
199         self.userdbs = {
200             '*': self.samdb
201         }
202
203         permissions = set(r[2] for r in rows)
204         for p in permissions:
205             if p == '*':
206                 continue
207             if p == '':
208                 user = 'nobody'
209                 writeable_objects = None
210             else:
211                 user = 'writes_' + p.replace(",", '_')
212                 writeable_objects = [self.objects[x][0] for x in p.split(',')]
213
214             self.userdbs[p] = add_unpriv_user(self.samdb, self.ou, user,
215                                               writeable_objects)
216
217     def _test_spn_with_args(self, rows, doc):
218         cdoc = colour_text(doc)
219         edoc = colour_text(doc, 'error')
220         pdoc = colour_text(doc, 'pass')
221
222         if COLOUR_TEXT:
223             sys.stderr.flush()
224             print('\n', c_DARK_YELLOW('#' * 10), f'starting «{cdoc}»\n')
225             sys.stdout.flush()
226
227         self.samdb = get_samdb()
228         self.base_dn = self.samdb.get_default_basedn()
229         self.short_id = self.id().rsplit('.', 1)[1][:63]
230         self.objects = {}
231         self.ou = f"OU={ self.short_id },{ self.base_dn }"
232         self.addCleanup(self.samdb.delete, self.ou, ["tree_delete:1"])
233         self.samdb.create_ou(self.ou)
234
235         self.setup_objects(rows)
236         self.setup_users(rows)
237
238         for i, row in enumerate(rows):
239             if len(row) == 5:
240                 obj, data, rights, expected, op = row
241             else:
242                 obj, data, rights, expected = row
243                 op = ldb.FLAG_MOD_REPLACE
244
245             # We use this DB with possibly restricted rights for this row
246             samdb = self.userdbs[rights]
247
248             if ':' in obj:
249                 objtype, obj = obj.split(':', 1)
250             else:
251                 objtype = 'dc'
252
253             dn, dnsname = self.objects[obj]
254             m = {"dn": dn}
255
256             if isinstance(data, dict):
257                 m.update(data)
258             else:
259                 m['servicePrincipalName'] = data
260
261             # for python's sake (and our sanity) we try to ensure we
262             # have consistent canonical case in our attributes
263             keys = set(m.keys())
264             if not keys.issubset(RELEVANT_ATTRS):
265                 raise ValueError(f"unexpected attr {keys - RELEVANT_ATTRS}. "
266                                  "Casefold typo?")
267
268             for k in ('dNSHostName', 'servicePrincipalName'):
269                 if isinstance(m.get(k), str):
270                     m[k] = m[k].format(dnsname=f"x.{REALM}")
271                 elif isinstance(m.get(k), list):
272                     m[k] = [x.format(dnsname=f"x.{REALM}") for x in m[k]]
273
274             msg = ldb.Message.from_dict(samdb, m, op)
275
276             if expected is bad:
277                 try:
278                     samdb.modify(msg)
279                 except ldb.LdbError as e:
280                     print(f"row {i+1} of '{pdoc}' failed as expected with "
281                           f"{ldb_err(e)}\n")
282                     continue
283                 self.fail(f"row {i+1}: "
284                           f"{rights} {pprint.pformat(m)} on {objtype} {obj} "
285                           f"should fail ({edoc})")
286
287             elif expected is ok:
288                 try:
289                     samdb.modify(msg)
290                 except ldb.LdbError as e:
291                     self.fail(f"row {i+1} of {edoc} failed with {ldb_err(e)}:\n"
292                               f"{rights} {pprint.pformat(m)} on {objtype} {obj}")
293
294             elif expected is report:
295                 try:
296                     self.samdb.modify(msg)
297                     print(f"row {i+1} "
298                           f"of '{cdoc}' {colour_text('SUCCEEDED', 'pass')}:\n"
299                           f"{pprint.pformat(m)} on {obj}")
300                 except ldb.LdbError as e:
301                     print(f"row {i+1} "
302                           f"of '{cdoc}' {colour_text('FAILED', 'error')} "
303                           f"with {ldb_err(e)}:\n{pprint.pformat(m)} on {obj}")
304
305             elif expected is breakpoint:
306                 try:
307                     breakpoint()
308                     samdb.modify(msg)
309                 except ldb.LdbError as e:
310                     print(f"row {i+1} of '{pdoc}' FAILED with {ldb_err(e)}\n")
311
312             else:  # an ldb error number
313                 try:
314                     samdb.modify(msg)
315                 except ldb.LdbError as e:
316                     if e.args[0] == expected:
317                         continue
318                     self.fail(f"row {i+1} of '{edoc}' "
319                               f"should have failed with {ldb_err(expected)}:\n"
320                               f"not {ldb_err(e)}:\n"
321                               f"{rights} {pprint.pformat(m)} on {objtype} {obj}")
322                 self.fail(f"row {i+1} of '{edoc}' "
323                           f"should have failed with {ldb_err(expected)}:\n"
324                           f"{rights} {pprint.pformat(m)} on {objtype} {obj}")
325
326     def add_dc(self, name):
327         dn = f"CN={name},OU=Domain Controllers,{self.base_dn}"
328         dnsname = f"{name}.{REALM}".lower()
329         self.samdb.add({
330             "dn": dn,
331             "objectclass": "computer",
332             "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT |
333                                       UF_TRUSTED_FOR_DELEGATION),
334             "dnsHostName": dnsname,
335             "carLicense": self.id()
336         })
337         self.addCleanup(self.remove_object, name)
338         self.objects[name] = (dn, dnsname)
339
340     def add_user(self, name):
341         dn = f"CN={name},{self.ou}"
342         self.samdb.add({
343             "dn": dn,
344             "name": name,
345             "samAccountName": name,
346             "objectclass": "user",
347             "carLicense": self.id()
348         })
349         self.addCleanup(self.remove_object, name)
350         self.objects[name] = (dn, None)
351
352     def remove_object(self, name):
353         dn, dnsname = self.objects.pop(name)
354         self.samdb.delete(dn)
355
356
357 @DynamicTestCase
358 class LdapSpnTest(LdapSpnTestBase):
359     """Make sure we can't add clashing servicePrincipalNames.
360
361     This would be possible using sPNMappings aliases — for example, if
362     the mapping maps host/ to cifs/, we should not be able to add
363     different addresses for each.
364     """
365
366     # default sPNMappings: host=alerter, appmgmt, cisvc, clipsrv,
367     # browser, dhcp, dnscache, replicator, eventlog, eventsystem,
368     # policyagent, oakley, dmserver, dns, mcsvc, fax, msiserver, ias,
369     # messenger, netlogon, netman, netdde, netddedsm, nmagent,
370     # plugplay, protectedstorage, rasman, rpclocator, rpc, rpcss,
371     # remoteaccess, rsvp, samss, scardsvr, scesrv, seclogon, scm,
372     # dcom, cifs, spooler, snmp, schedule, tapisrv, trksvr, trkwks,
373     # ups, time, wins, www, http, w3svc, iisadmin, msdtc
374     #
375     # I think in practice this is rarely if ever changed or added to.
376
377     cases = [
378         ("add one as admin",
379          ('A', 'host/{dnsname}', '*', ok),
380         ),
381         ("add one as rightful user",
382          ('A', 'host/{dnsname}', 'A', ok),
383         ),
384         ("attempt to add one as nobody",
385          ('A', 'host/{dnsname}', '', denied),
386         ),
387
388         ("add and replace as admin",
389          ('A', 'host/{dnsname}', '*', ok),
390          ('A', 'host/x.{dnsname}', '*', ok),
391         ),
392         ("replace as rightful user",
393          ('A', 'host/{dnsname}', 'A', ok),
394          ('A', 'host/x.{dnsname}', 'A', ok),
395         ),
396         ("attempt to replace one as nobody",
397          ('A', 'host/{dnsname}', '*', ok),
398          ('A', 'host/x.{dnsname}', '', denied),
399         ),
400
401         ("add second as admin",
402          ('A', 'host/{dnsname}', '*', ok),
403          ('A', 'host/x.{dnsname}', '*', ok, add),
404         ),
405         ("add second as rightful user",
406          ('A', 'host/{dnsname}', 'A', ok),
407          ('A', 'host/x.{dnsname}', 'A', ok, add),
408         ),
409         ("attempt to add second as nobody",
410          ('A', 'host/{dnsname}', '*', ok),
411          ('A', 'host/x.{dnsname}', '', denied, add),
412         ),
413
414         ("add the same one twice, simple duplicate error",
415          ('A', 'host/{dnsname}', '*', ok),
416          ('A', 'host/{dnsname}', '*', bad, add),
417         ),
418         ("simple duplicate attributes, as non-admin",
419          ('A', 'host/{dnsname}', '*', ok),
420          ('A', 'host/{dnsname}', 'A', bad, add),
421         ),
422
423         ("add the same one twice, identical duplicate",
424          ('A', 'host/{dnsname}', '*', ok),
425          ('A', 'host/{dnsname}', '*', bad, add),
426         ),
427
428         ("add a conflict, host first, as nobody",
429          ('A', 'host/z.{dnsname}', '*', ok),
430          ('B', 'cifs/z.{dnsname}', '', denied),
431         ),
432
433         ("add a conflict, service first, as nobody",
434          ('A', 'cifs/{dnsname}', '*', ok),
435          ('B', 'host/{dnsname}', '', denied),
436         ),
437
438
439         ("three way conflict, host first, as admin",
440          ('A', 'host/z.{dnsname}', '*', ok),
441          ('B', 'cifs/z.{dnsname}', '*', ok),
442          ('C', 'www/z.{dnsname}', '*', ok),
443         ),
444         ("three way conflict, host first, with sufficient rights",
445          ('A', 'host/z.{dnsname}', 'A', ok),
446          ('B', 'cifs/z.{dnsname}', 'B,A', ok),
447          ('C', 'www/z.{dnsname}', 'C,A', ok),
448         ),
449         ("three way conflict, host first, adding duplicate",
450          ('A', 'host/z.{dnsname}', 'A', ok),
451          ('B', 'cifs/z.{dnsname}', 'B,A', ok),
452          ('C', 'cifs/z.{dnsname}', 'C,A', bad),
453         ),
454         ("three way conflict, host first, adding duplicate, full rights",
455          ('A', 'host/z.{dnsname}', 'A', ok),
456          ('B', 'cifs/z.{dnsname}', 'B,A', ok),
457          ('C', 'cifs/z.{dnsname}', 'C,B,A', bad),
458         ),
459
460         ("three way conflict, host first, with other write rights",
461          ('A', 'host/z.{dnsname}', '*', ok),
462          ('B', 'cifs/z.{dnsname}', 'A,B', ok),
463          ('C', 'cifs/z.{dnsname}', 'A,B', bad),
464
465         ),
466         ("three way conflict, host first, as nobody",
467          ('A', 'host/z.{dnsname}', '*', ok),
468          ('B', 'cifs/z.{dnsname}', '*', ok),
469          ('C', 'www/z.{dnsname}', '', denied),
470         ),
471
472         ("three way conflict, services first, as admin",
473          ('A', 'cifs/{dnsname}', '*', ok),
474          ('B', 'www/{dnsname}', '*', ok),
475          ('C', 'host/{dnsname}', '*', constraint),
476         ),
477         ("three way conflict, services first, with service write rights",
478          ('A', 'cifs/{dnsname}', '*', ok),
479          ('B', 'www/{dnsname}', '*', ok),
480          ('C', 'host/{dnsname}', 'A,B', bad),
481         ),
482
483         ("three way conflict, service first, as nobody",
484          ('A', 'cifs/{dnsname}', '*', ok),
485          ('B', 'www/{dnsname}', '*', ok),
486          ('C', 'host/{dnsname}', '', denied),
487         ),
488         ("replace host before specific",
489          ('A', 'host/{dnsname}', '*', ok),
490          ('A', 'cifs/{dnsname}', '*', ok),
491         ),
492         ("replace host after specific, as nobody",
493          ('A', 'cifs/{dnsname}', '*', ok),
494          ('A', 'host/{dnsname}', '', denied),
495         ),
496
497         ("non-conflict host before specific",
498          ('A', 'host/{dnsname}', '*', ok),
499          ('A', 'cifs/{dnsname}', '*', ok, add),
500         ),
501         ("non-conflict host after specific",
502          ('A', 'cifs/{dnsname}', '*', ok),
503          ('A', 'host/{dnsname}', '*', ok, add),
504         ),
505         ("non-conflict host before specific, non-admin",
506          ('A', 'host/{dnsname}', 'A', ok),
507          ('A', 'cifs/{dnsname}', 'A', ok, add),
508         ),
509         ("non-conflict host after specific, as nobody",
510          ('A', 'cifs/{dnsname}', '*', ok),
511          ('A', 'host/{dnsname}', '', denied, add),
512         ),
513
514         ("add a conflict, host first on user, as admin",
515          ('user:C', 'host/{dnsname}', '*', ok),
516          ('B', 'cifs/{dnsname}', '*', ok),
517         ),
518         ("add a conflict, host first on user, host rights",
519          ('user:C', 'host/{dnsname}', '*', ok),
520          ('B', 'cifs/{dnsname}', 'C', denied),
521         ),
522         ("add a conflict, host first on user, both rights",
523          ('user:C', 'host/{dnsname}', '*', ok),
524          ('B', 'cifs/{dnsname}', 'B,C', ok),
525         ),
526         ("add a conflict, host first both on user",
527          ('user:C', 'host/{dnsname}', '*', ok),
528          ('user:D', 'www/{dnsname}', '*', ok),
529         ),
530         ("add a conflict, host first both on user, host rights",
531          ('user:C', 'host/{dnsname}', '*', ok),
532          ('user:D', 'www/{dnsname}', 'C', denied),
533          ),
534         ("add a conflict, host first both on user, both rights",
535          ('user:C', 'host/{dnsname}', '*', ok),
536          ('user:D', 'www/{dnsname}', 'C,D', ok),
537         ),
538         ("add a conflict, host first both on user, as nobody",
539          ('user:C', 'host/{dnsname}', '*', ok),
540          ('user:D', 'www/{dnsname}', '', denied),
541         ),
542         ("add a conflict, host first, with both write rights",
543          ('A', 'host/z.{dnsname}', '*', ok),
544          ('B', 'cifs/z.{dnsname}', 'A,B', ok),
545         ),
546
547         ("add a conflict, host first, second on user, as admin",
548          ('A', 'host/{dnsname}', '*', ok),
549          ('user:D', 'cifs/{dnsname}', '*', ok),
550         ),
551         ("add a conflict, host first, second on user, with rights",
552          ('A', 'host/{dnsname}', '*', ok),
553          ('user:D', 'cifs/{dnsname}', 'A,D', ok),
554         ),
555
556         ("nonsense SPNs, part 1, as admin",
557          ('A', 'a-b-c/{dnsname}', '*', ok),
558          ('A', 'rrrrrrrrrrrrr /{dnsname}', '*', ok),
559         ),
560         ("nonsense SPNs, part 1, as user",
561          ('A', 'a-b-c/{dnsname}', 'A', ok),
562          ('A', 'rrrrrrrrrrrrr /{dnsname}', 'A', ok),
563         ),
564         ("nonsense SPNs, part 1, as nobody",
565          ('A', 'a-b-c/{dnsname}', '', denied),
566          ('A', 'rrrrrrrrrrrrr /{dnsname}', '', denied),
567         ),
568
569         ("add a conflict, using port",
570          ('A', 'dns/{dnsname}', '*', ok),
571          ('B', 'dns/{dnsname}:53', '*', ok),
572         ),
573         ("add a conflict, using port, port first",
574          ('user:C', 'dns/{dnsname}:53', '*', ok),
575          ('user:D', 'dns/{dnsname}', '*', ok),
576         ),
577         ("three part spns",
578          ('A', {'dNSHostName': '{dnsname}'}, '*', ok),
579          ('A', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', ok),
580          ('B', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', constraint),
581          ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok),
582          ('B', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', ok),
583          ('B', 'cifs/y.{dnsname}/DomainDNSZones.{dnsname}', '*', constraint),
584         ),
585         ("three part nonsense spns",
586          ('A', {'dNSHostName': 'bean'}, '*', ok),
587          ('A', 'cifs/bean/DomainDNSZones.bean', '*', ok),
588          ('B', 'cifs/bean/DomainDNSZones.bean', '*', constraint),
589          ('A', {'dNSHostName': 'y.bean'}, '*', ok),
590          ('B', 'cifs/bean/DomainDNSZones.bean', '*', ok),
591          ('B', 'cifs/y.bean/DomainDNSZones.bean', '*', constraint),
592          ('C', 'host/bean/bean', '*', ok),
593         ),
594
595         ("one part spns (no slashes)",
596          ('A', '{dnsname}', '*', constraint),
597          ('B', 'cifs', '*', constraint),
598          ('B', 'cifs/', '*', ok),
599          ('B', ' ', '*', constraint),
600          ('user:C', 'host', '*', constraint),
601         ),
602
603         ("dodgy spns",
604          # These tests pass on Windows. An SPN must have one or two
605          # slashes, with at least one character before the first one,
606          # UNLESS the first slash is followed by a good enough service
607          # name (e.g. "/host/x.y" rather than "sdfsd/x.y").
608          ('A', '\\/{dnsname}', '*', ok),
609          ('B', 'cifs/\\\\{dnsname}', '*', ok),
610          ('B', r'cifs/\\\{dnsname}', '*', ok),
611          ('B', r'cifs/\\\{dnsname}/', '*', ok),
612          ('A', r'cīfs/\\\{dnsname}/', '*', constraint),  # 'ī' maps to 'i'
613          # on the next two, full-width solidus (U+FF0F) does not work
614          # as '/'.
615          ('A', 'cifs/sfic', '*', constraint, add),
616          ('A', r'cifs/\\\{dnsname}', '*', constraint, add),
617          ('B', '\n', '*', constraint),
618          ('B', '\n/\n', '*', ok),
619          ('B', '\n/\n/\n', '*', ok),
620          ('B', '\n/\n/\n/\n', '*', constraint),
621          ('B', ' /* and so on */ ', '*', ok, add),
622          ('B', r'¯\_(ツ)_/¯', '*', ok, add),      # ¯\_(ツ)_/¯
623          # つ is hiragana for katakana ツ, so the next one fails for
624          # something analogous to casefold reasons.
625          ('A', r'¯\_(つ)_/¯', '*', constraint),
626          ('A', r'¯\_(㋡)_/¯', '*', constraint),   # circled ツ
627          ('B', '//', '*', constraint),           # all can't be empty,
628          ('B', ' //', '*', ok),                  # service can be space
629          ('B', '/host/{dnsname}', '*', ok),      # or empty if others aren't
630          ('B', '/host/x.y.z', '*', ok),
631          ('B', '/ /x.y.z', '*', ok),
632          ('B', ' / / ', '*', ok),
633          ('user:C', b'host/', '*', ok),
634          ('user:C', ' /host', '*', ok),          # service is ' ' (space)
635          ('B', ' /host', '*', constraint),       # already on C
636          ('B', ' /HōST', '*', constraint),       # ō equiv to O
637          ('B', ' /ħØşt', '*', constraint),       # maps to ' /host'
638          ('B', ' /H0ST', '*', ok),               # 0 is zero
639          ('B', ' /НoST', '*', ok),               # Cyrillic Н (~N)
640          ('B', '  /host', '*', ok),              # two space
641          ('B', '\u00a0/host', '*', ok),          # non-breaking space
642          ('B', ' 2/HōST/⌷[ ][]¨(', '*', ok),
643          ('B', ' (//)', '*', ok, add),
644          ('B', ' ///', '*', constraint),
645          ('B', r' /\//', '*', constraint),        # escape doesn't help
646          ('B', ' /\\//', '*', constraint),       # double escape doesn't help
647          ('B', r'\//', '*', ok),
648          ('A', r'\\/\\/', '*', ok),
649          ('B', '|//|', '*', ok, add),
650          ('B', r'\/\/\\', '*', ok, add),
651
652          ('A', ':', '*', constraint),
653          ('A', ':/:', '*', ok),
654          ('A', ':/:80', '*', ok),   # port number syntax is not special
655          ('A', ':/:( ツ', '*', ok),
656          ('A', ':/:/:', '*', ok),
657          ('B', b'cifs/\x11\xaa\xbb\xcc\\example.com', '*', ok),
658          ('A', b':/\xcc\xcc\xcc\xcc', '*', ok),
659          ('A', b':/b\x00/b/b/b', '*', ok),  # string handlng truncates at \x00
660          ('A', b'a@b/a@b/a@b', '*', ok),
661          ('A', b'a/a@b/a@b', '*', ok),
662         ),
663         ("empty part spns (consecutive slashes)",
664          ('A', 'cifs//{dnsname}', '*', ok),
665          ('B', 'cifs//{dnsname}', '*', bad),  # should clash with line 1
666          ('B', 'cifs/zzzy.{dnsname}/', '*', ok),
667          ('B', '/host/zzzy.{dnsname}', '*', ok),
668         ),
669         ("too many spn parts",
670          ('A', 'cifs/{dnsname}/{dnsname}/{dnsname}', '*', bad),
671          ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok),
672          ('B', 'cifs/{dnsname}/{dnsname}/', '*', bad),
673          ('B', 'cifs/y.{dnsname}/{dnsname}/toop', '*', bad),
674          ('B', 'host/{dnsname}/a/b/c', '*', bad),
675         ),
676         ("add a conflict, host first, as admin",
677          ('A', 'host/z.{dnsname}', '*', ok),
678          ('B', 'cifs/z.{dnsname}', '*', ok),
679         ),
680         ("add a conflict, host first, with host write rights",
681          ('A', 'host/z.{dnsname}', '*', ok),
682          ('B', 'cifs/z.{dnsname}', 'A', denied),
683         ),
684         ("add a conflict, service first, with service write rights",
685          ('A', 'cifs/{dnsname}', '*', ok),
686          ('B', 'host/{dnsname}', 'A', denied),
687         ),
688         ("adding dNSHostName after cifs with no old dNSHostName",
689          ('A', 'cifs/{dnsname}', '*', ok),
690          ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok),
691          ('B', 'cifs/{dnsname}', '*', constraint),
692          ('B', 'cifs/y.{dnsname}', '*', ok),
693          ('B', 'host/y.{dnsname}', '*', ok),
694         ),
695         ("changing dNSHostName after cifs",
696          ('A', {'dNSHostName': '{dnsname}'}, '*', ok),
697          ('A', 'cifs/{dnsname}', '*', ok),
698          ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok),
699          ('B', 'cifs/{dnsname}', '*', ok),
700          ('B', 'cifs/y.{dnsname}', '*', bad),
701          ('B', 'host/y.{dnsname}', '*', bad),
702         ),
703     ]
704
705
706 @DynamicTestCase
707 class LdapSpnSambaOnlyTest(LdapSpnTestBase):
708     # We don't run these ones outside of selftest, where we are
709     # probably testing against Windows and these are known failures.
710     _disabled = 'SAMBA_SELFTEST' not in os.environ
711     cases = [
712         ("add a conflict, host first, with service write rights",
713          ('A', 'host/z.{dnsname}', '*', ok),
714          ('B', 'cifs/z.{dnsname}', 'B', denied),
715         ),
716         ("add a conflict, service first, with host write rights",
717          ('A', 'cifs/{dnsname}', '*', ok),
718          ('B', 'host/{dnsname}', 'B', constraint),
719         ),
720         ("add a conflict, service first, as admin",
721          ('A', 'cifs/{dnsname}', '*', ok),
722          ('B', 'host/{dnsname}', '*', constraint),
723         ),
724         ("add a conflict, service first, with both write rights",
725          ('A', 'cifs/{dnsname}', '*', ok),
726          ('B', 'host/{dnsname}', 'A,B', constraint),
727         ),
728         ("add a conflict, host first both on user, service rights",
729          ('user:C', 'host/{dnsname}', '*', ok),
730          ('user:D', 'www/{dnsname}', 'D', denied),
731         ),
732         ("add a conflict, along with a re-added SPN",
733          ('A', 'cifs/{dnsname}', '*', ok),
734          ('B', 'cifs/heeble.example.net', 'B', ok),
735          ('B', ['cifs/heeble.example.net', 'host/{dnsname}'], 'B', constraint),
736         ),
737
738         ("changing dNSHostName after host",
739          ('A', {'dNSHostName': '{dnsname}'}, '*', ok),
740          ('A', 'host/{dnsname}', '*', ok),
741          ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok),
742          ('B', 'cifs/{dnsname}', 'B', ok),     # no clash with A
743          ('B', 'cifs/y.{dnsname}', 'B', bad),  # should clash with A
744          ('B', 'host/y.{dnsname}', '*', bad),
745         ),
746
747         ("mystery dnsname clash, host first",
748          ('user:C', 'host/heeble.example.net', '*', ok),
749          ('user:D', 'www/heeble.example.net', '*', ok),
750         ),
751         ("mystery dnsname clash, www first",
752          ('user:D', 'www/heeble.example.net', '*', ok),
753          ('user:C', 'host/heeble.example.net', '*', constraint),
754         ),
755         ("replace as admin",
756          ('A', 'cifs/{dnsname}', '*', ok),
757          ('A', 'host/{dnsname}', '*', ok),
758          ('A', 'cifs/{dnsname}', '*', ok),
759         ),
760         ("replace as non-admin with rights",
761          ('A', 'cifs/{dnsname}', '*', ok),
762          ('A', 'host/{dnsname}', 'A', ok),
763          ('A', 'cifs/{dnsname}', 'A', ok),
764         ),
765         ("replace vial delete as non-admin with rights",
766          ('A', 'cifs/{dnsname}', '*', ok),
767          ('A', 'host/{dnsname}', 'A', ok),
768          ('A', 'host/{dnsname}', 'A', ok, delete),
769          ('A', 'cifs/{dnsname}', 'A', ok, add),
770         ),
771         ("replace as non-admin without rights",
772          ('B', 'cifs/b', '*', ok),
773          ('A', 'cifs/{dnsname}', '*', ok),
774          ('A', 'host/{dnsname}', 'B', denied),
775          ('A', 'cifs/{dnsname}', 'B', denied),
776         ),
777         ("replace as nobody",
778          ('B', 'cifs/b', '*', ok),
779          ('A', 'cifs/{dnsname}', '*', ok),
780          ('A', 'host/{dnsname}', '', denied),
781          ('A', 'cifs/{dnsname}', '', denied),
782         ),
783         ("accumulate and delete as admin",
784          ('A', 'cifs/{dnsname}', '*', ok),
785          ('A', 'host/{dnsname}', '*', ok, add),
786          ('A', 'www/{dnsname}', '*', ok, add),
787          ('A', 'www/...', '*', ok, add),
788          ('A', 'host/...', '*', ok, add),
789          ('A', 'www/{dnsname}', '*', ok, delete),
790          ('A', 'host/{dnsname}', '*', ok, delete),
791          ('A', 'host/{dnsname}', '*', ok, add),
792          ('A', 'www/{dnsname}', '*', ok, add),
793          ('A', 'host/...', '*', ok, delete),
794         ),
795         ("accumulate and delete with user rights",
796          ('A', 'cifs/{dnsname}', '*', ok),
797          ('A', 'host/{dnsname}', 'A', ok, add),
798          ('A', 'www/{dnsname}', 'A', ok, add),
799          ('A', 'www/...', 'A', ok, add),
800          ('A', 'host/...', 'A', ok, add),
801          ('A', 'www/{dnsname}', 'A', ok, delete),
802          ('A', 'host/{dnsname}', 'A', ok, delete),
803          ('A', 'host/{dnsname}', 'A', ok, add),
804          ('A', 'www/{dnsname}', 'A', ok, add),
805          ('A', 'host/...', 'A', ok, delete),
806         ),
807         ("three way conflict, host first, with partial write rights",
808          ('A', 'host/z.{dnsname}', 'A', ok),
809          ('B', 'cifs/z.{dnsname}', 'B', denied),
810          ('C', 'www/z.{dnsname}', 'C', denied),
811         ),
812         ("three way conflict, host first, with partial write rights 2",
813          ('A', 'host/z.{dnsname}', 'A', ok),
814          ('B', 'cifs/z.{dnsname}', 'B', bad),
815          ('C', 'www/z.{dnsname}', 'C,A', ok),
816         ),
817
818         ("three way conflict sandwich, sufficient rights",
819          ('B', 'host/{dnsname}', 'B', ok),
820          ('A', 'cifs/{dnsname}', 'A,B', ok),
821          # the replaces don't fail even though they appear to affect A
822          # and B, because they are effectively no-ops, leaving
823          # everything as it was before.
824          ('A', 'cifs/{dnsname}', 'A', ok),
825          ('B', 'host/{dnsname}', 'B', ok),
826          ('C', 'www/{dnsname}', 'A,B,C', ok),
827          ('C', 'www/{dnsname}', 'B,C', ok),
828          # because B already has host/, C doesn't matter
829          ('B', 'host/{dnsname}', 'A,B', ok),
830          # removing host (via replace) frees others, needs B only
831          ('B', 'ldap/{dnsname}', 'B', ok),
832          ('C', 'www/{dnsname}', 'C', ok),
833          ('A', 'cifs/{dnsname}', 'A', ok),
834
835          # re-adding host is now impossible while A and C have {dnsname} spns
836          ('B', 'host/{dnsname}', '*', bad),
837          ('B', 'host/{dnsname}', 'A,B,C', bad),
838          # so let's remove those... (not needing B rights)
839          ('C', 'www/{dnsname}', 'C', ok, delete),
840          ('A', 'cifs/{dnsname}', 'A', ok, delete),
841          # and now we can add host/ again
842          ('B', 'host/{dnsname}', 'B', ok),
843          ('C', 'www/{dnsname}', 'B,C', ok, add),
844          ('A', 'cifs/{dnsname}', 'A,B', ok),
845         ),
846         ("three way conflict, service first, with all write rights",
847          ('A', 'cifs/{dnsname}', '*', ok),
848          ('B', 'www/{dnsname}', 'A,B,C', ok),
849          ('C', 'host/{dnsname}', 'A,B,C', bad),
850         ),
851         ("three way conflict, service first, just sufficient rights",
852          ('A', 'cifs/{dnsname}', 'A', ok),
853          ('B', 'www/{dnsname}', 'B', ok),
854          ('C', 'host/{dnsname}', 'A,B,C', bad),
855         ),
856
857         ("three way conflict, service first, with host write rights",
858          ('A', 'cifs/{dnsname}', '*', ok),
859          ('B', 'www/{dnsname}', '*', ok),
860          ('C', 'host/{dnsname}', 'C', bad),
861         ),
862         ("three way conflict, service first, with both write rights",
863          ('A', 'cifs/{dnsname}', '*', ok),
864          ('A', 'cifs/{dnsname}', '*', ok, delete),
865          ('A', 'www/{dnsname}', 'A,B,C', ok),
866          ('B', 'host/{dnsname}', 'A,B', bad),
867          ('A', 'www/{dnsname}', 'A', ok, delete),
868          ('B', 'host/{dnsname}', 'A,B', ok),
869          ('C', 'cifs/{dnsname}', 'C', bad),
870          ('C', 'cifs/{dnsname}', 'B,C', ok),
871         ),
872         ("three way conflict, services first, with partial rights",
873          ('A', 'cifs/{dnsname}', 'A,C', ok),
874          ('B', 'www/{dnsname}', '*', ok),
875          ('C', 'host/{dnsname}', 'A,C', bad),
876         ),
877     ]
878
879
880 @DynamicTestCase
881 class LdapSpnAmbitiousTest(LdapSpnTestBase):
882     _disabled = True
883     cases = [
884         ("add a conflict with port, host first both on user",
885          ('user:C', 'host/{dnsname}', '*', ok),
886          ('user:D', 'www/{dnsname}:80', '*', bad),
887         ),
888         # see https://bugzilla.samba.org/show_bug.cgi?id=8929
889         ("add the same one twice, case-insensitive duplicate",
890          ('A', 'host/{dnsname}', '*', ok),
891          ('A', 'Host/{dnsname}', '*', bad, add),
892         ),
893         ("special SPN",
894          # should fail because we don't have all the DSA infrastructure
895          ('A', ("E3514235-4B06-11D1-AB04-00C04FC2DCD2/"
896                 "75b84f00-a81b-4a19-8ef2-8e483cccff11/"
897                 "{dnsname}"), '*', constraint)
898          ),
899         ("single part SPNs matching sAMAccountName",
900          # setting them both together is allegedly a MacOS behaviour,
901          # but all we get from Windows is a mysterious NO_SUCH_OBJECT.
902          ('user:A', {'sAMAccountName': 'A',
903                      'servicePrincipalName': 'A'}, '*', ldb.ERR_NO_SUCH_OBJECT),
904          ('user:B', {'sAMAccountName': 'B'}, '*', ok),
905          ('user:B', {'servicePrincipalName': 'B'}, '*', constraint),
906          ('user:C', {'servicePrincipalName': 'C'}, '*', constraint),
907          ('user:C', {'sAMAccountName': 'C'}, '*', ok),
908         ),
909         ("three part spns with dnsHostName",
910          ('A', {'dNSHostName': '{dnsname}'}, '*', ok),
911          ('A', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', ok),
912          ('A', {'dNSHostName': 'y.{dnsname}'}, '*', ok),
913          ('B', 'cifs/{dnsname}/DomainDNSZones.{dnsname}', '*', ok),
914          ('B', 'cifs/y.{dnsname}/DomainDNSZones.{dnsname}', '*', constraint),
915          ('C', 'host/{y.dnsname}/{y.dnsname}', '*', constraint),
916          ('A', 'host/y.{dnsname}/{dnsname}', '*', constraint),
917         ),
918     ]
919
920
921 def main():
922     TestProgram(module=__name__, opts=subunitopts)
923
924 main()