1 # Unix SMB/CIFS implementation.
3 # Tests for samba-tool domain claim management
5 # Copyright (C) Catalyst.Net Ltd. 2023
7 # Written by Rob van der Linde <rob@catalyst.net.nz>
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 from ldb
import SCOPE_ONELEVEL
27 from samba
.sd_utils
import SDUtils
29 from .base
import SambaToolCmdTest
31 # List of claim value types we should expect to see.
34 "Multi-valued Choice",
38 "Single-valued Choice",
43 HOST
= "ldap://{DC_SERVER}".format(**os
.environ
)
44 CREDS
= "-U{DC_USERNAME}%{DC_PASSWORD}".format(**os
.environ
)
47 class BaseClaimCmdTest(SambaToolCmdTest
):
48 """Base class for claim types and claim value types tests."""
52 cls
.samdb
= cls
.getSamDB("-H", HOST
, CREDS
)
56 def setUpTestData(cls
):
57 cls
.create_claim_type("accountExpires", name
="expires",
59 cls
.create_claim_type("department", name
="dept", classes
=["user"],
61 cls
.create_claim_type("carLicense", name
="plate", classes
=["user"],
64 def get_services_dn(self
):
65 """Returns Services DN."""
66 services_dn
= self
.samdb
.get_config_basedn()
67 services_dn
.add_child("CN=Services")
70 def get_claim_types_dn(self
):
71 """Returns the Claim Types DN."""
72 claim_types_dn
= self
.get_services_dn()
73 claim_types_dn
.add_child("CN=Claim Types,CN=Claims Configuration")
78 """Override _run, so we don't always have to pass host and creds."""
80 args
.extend(["-H", HOST
, CREDS
])
81 return super()._run
(*args
)
87 def create_claim_type(cls
, attribute
, name
=None, description
=None,
88 classes
=None, disable
=False, protect
=False):
89 """Create a claim type using the samba-tool command."""
91 # if name is specified it will override the attribute name
92 display_name
= name
or attribute
94 # base command for create claim-type
95 cmd
= ["domain", "claim", "claim-type",
96 "create", "--attribute", attribute
]
98 # list of classes (applies_to)
99 if classes
is not None:
100 cmd
.extend([f
"--class={name}" for name
in classes
])
102 # optional attributes
104 cmd
.append(f
"--name={name}")
105 if description
is not None:
106 cmd
.append(f
"--description={description}")
108 cmd
.append("--disable")
110 cmd
.append("--protect")
112 result
, out
, err
= cls
.runcmd(*cmd
)
113 assert result
is None
114 assert out
.startswith("Created claim type")
115 cls
.addClassCleanup(cls
.delete_claim_type
, name
=display_name
, force
=True)
119 def delete_claim_type(cls
, name
, force
=False):
120 """Delete claim type by display name."""
121 cmd
= ["domain", "claim", "claim-type", "delete", "--name", name
]
123 # Force-delete protected claim type.
125 cmd
.append("--force")
127 result
, out
, err
= cls
.runcmd(*cmd
)
128 assert result
is None
129 assert "Deleted claim type" in out
131 def get_claim_type(self
, name
):
132 """Get claim type by display name."""
133 claim_types_dn
= self
.get_claim_types_dn()
135 result
= self
.samdb
.search(base
=claim_types_dn
,
136 scope
=SCOPE_ONELEVEL
,
137 expression
=f
"(displayName={name})")
143 class ClaimTypeCmdTestCase(BaseClaimCmdTest
):
144 """Tests for the claim-type command."""
147 """Test listing claim types in list format."""
148 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type", "list")
149 self
.assertIsNone(result
, msg
=err
)
151 expected_claim_types
= ["expires", "dept", "plate"]
153 for claim_type
in expected_claim_types
:
154 self
.assertIn(claim_type
, out
)
156 def test_list__json(self
):
157 """Test listing claim types in JSON format."""
158 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
160 self
.assertIsNone(result
, msg
=err
)
162 # we should get valid json
163 json_result
= json
.loads(out
)
164 claim_types
= list(json_result
.keys())
166 expected_claim_types
= ["expires", "dept", "plate"]
168 for claim_type
in expected_claim_types
:
169 self
.assertIn(claim_type
, claim_types
)
172 """Test viewing a single claim type."""
173 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
174 "view", "--name", "expires")
175 self
.assertIsNone(result
, msg
=err
)
177 # we should get valid json
178 claim_type
= json
.loads(out
)
180 # check a few fields only
181 self
.assertEqual(claim_type
["displayName"], "expires")
182 self
.assertEqual(claim_type
["description"], "Account-Expires")
184 def test_view__name_missing(self
):
185 """Test view claim type without --name is handled."""
186 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type", "view")
187 self
.assertEqual(result
, -1)
188 self
.assertIn("Argument --name is required.", err
)
190 def test_view__notfound(self
):
191 """Test viewing claim type that doesn't exist is handled."""
192 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
193 "view", "--name", "doesNotExist")
194 self
.assertEqual(result
, -1)
195 self
.assertIn("Claim type doesNotExist not found.", err
)
197 def test_create(self
):
198 """Test creating several known attributes as claim types.
200 The point is to test it against the various datatypes that could
201 be found, but not include every known attribute.
203 # We just need to test a few different data types for attributes,
204 # there is no need to test every known attribute.
210 "msDS-PrimaryComputer",
214 # Each known attribute must be in the schema.
215 for attribute
in claim_types
:
216 # Use a different name, so we don't clash with existing attributes.
217 name
= "test_create_" + attribute
219 self
.addCleanup(self
.delete_claim_type
, name
=name
, force
=True)
221 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
223 "--attribute", attribute
,
226 self
.assertIsNone(result
, msg
=err
)
228 # It should have used the attribute name as displayName.
229 claim_type
= self
.get_claim_type(name
)
230 self
.assertEqual(str(claim_type
["displayName"]), name
)
231 self
.assertEqual(str(claim_type
["Enabled"]), "TRUE")
232 self
.assertEqual(str(claim_type
["objectClass"][-1]), "msDS-ClaimType")
233 self
.assertEqual(str(claim_type
["msDS-ClaimSourceType"]), "AD")
235 def test_create__boolean(self
):
236 """Test adding a known boolean attribute and check its type."""
237 self
.addCleanup(self
.delete_claim_type
, name
="boolAttr", force
=True)
239 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
240 "create", "--attribute=msNPAllowDialin",
241 "--name=boolAttr", "--class=user")
243 self
.assertIsNone(result
, msg
=err
)
244 claim_type
= self
.get_claim_type("boolAttr")
245 self
.assertEqual(str(claim_type
["displayName"]), "boolAttr")
246 self
.assertEqual(str(claim_type
["msDS-ClaimValueType"]), "6")
248 def test_create__number(self
):
249 """Test adding a known numeric attribute and check its type."""
250 self
.addCleanup(self
.delete_claim_type
, name
="intAttr", force
=True)
252 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
253 "create", "--attribute=adminCount",
254 "--name=intAttr", "--class=user")
256 self
.assertIsNone(result
, msg
=err
)
257 claim_type
= self
.get_claim_type("intAttr")
258 self
.assertEqual(str(claim_type
["displayName"]), "intAttr")
259 self
.assertEqual(str(claim_type
["msDS-ClaimValueType"]), "1")
261 def test_create__text(self
):
262 """Test adding a known text attribute and check its type."""
263 self
.addCleanup(self
.delete_claim_type
, name
="textAttr", force
=True)
265 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
266 "create", "--attribute=givenName",
267 "--name=textAttr", "--class=user")
269 self
.assertIsNone(result
, msg
=err
)
270 claim_type
= self
.get_claim_type("textAttr")
271 self
.assertEqual(str(claim_type
["displayName"]), "textAttr")
272 self
.assertEqual(str(claim_type
["msDS-ClaimValueType"]), "3")
274 def test_create__disabled(self
):
275 """Test adding a disabled attribute."""
276 self
.addCleanup(self
.delete_claim_type
, name
="disabledAttr", force
=True)
278 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
279 "create", "--attribute=msTSHomeDrive",
280 "--name=disabledAttr", "--class=user",
283 self
.assertIsNone(result
, msg
=err
)
284 claim_type
= self
.get_claim_type("disabledAttr")
285 self
.assertEqual(str(claim_type
["displayName"]), "disabledAttr")
286 self
.assertEqual(str(claim_type
["Enabled"]), "FALSE")
288 def test_create__protected(self
):
289 """Test adding a protected attribute."""
290 self
.addCleanup(self
.delete_claim_type
, name
="protectedAttr", force
=True)
292 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
293 "create", "--attribute=mobile",
294 "--name=protectedAttr", "--class=user",
297 self
.assertIsNone(result
, msg
=err
)
298 claim_type
= self
.get_claim_type("protectedAttr")
299 self
.assertEqual(str(claim_type
["displayName"]), "protectedAttr")
301 # Check if the claim type is protected from accidental deletion.
302 utils
= SDUtils(self
.samdb
)
303 desc
= utils
.get_sd_as_sddl(claim_type
["dn"])
304 self
.assertIn("(D;;DTSD;;;WD)", desc
)
306 def test_create__classes(self
):
307 """Test adding an attribute applied to different classes."""
308 schema_dn
= self
.samdb
.get_schema_basedn()
309 user_dn
= f
"CN=User,{schema_dn}"
310 computer_dn
= f
"CN=Computer,{schema_dn}"
313 self
.addCleanup(self
.delete_claim_type
, name
="streetName", force
=True)
314 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
315 "create", "--attribute=street",
316 "--name=streetName", "--class=user")
317 self
.assertIsNone(result
, msg
=err
)
318 claim_type
= self
.get_claim_type("streetName")
319 applies_to
= [str(dn
) for dn
in claim_type
["msDS-ClaimTypeAppliesToClass"]]
320 self
.assertEqual(str(claim_type
["displayName"]), "streetName")
321 self
.assertEqual(len(applies_to
), 1)
322 self
.assertIn(user_dn
, applies_to
)
323 self
.assertNotIn(computer_dn
, applies_to
)
326 self
.addCleanup(self
.delete_claim_type
, name
="ext", force
=True)
327 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
328 "create", "--attribute=extensionName",
329 "--name=ext", "--class=computer")
330 self
.assertIsNone(result
, msg
=err
)
331 claim_type
= self
.get_claim_type("ext")
332 applies_to
= [str(dn
) for dn
in claim_type
["msDS-ClaimTypeAppliesToClass"]]
333 self
.assertEqual(str(claim_type
["displayName"]), "ext")
334 self
.assertEqual(len(applies_to
), 1)
335 self
.assertNotIn(user_dn
, applies_to
)
336 self
.assertIn(computer_dn
, applies_to
)
338 # --class=user --class=computer
339 self
.addCleanup(self
.delete_claim_type
,
340 name
="primaryComputer", force
=True)
341 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
342 "create", "--attribute=msDS-PrimaryComputer",
343 "--name=primaryComputer", "--class=user",
345 self
.assertIsNone(result
, msg
=err
)
346 claim_type
= self
.get_claim_type("primaryComputer")
347 applies_to
= [str(dn
) for dn
in claim_type
["msDS-ClaimTypeAppliesToClass"]]
348 self
.assertEqual(str(claim_type
["displayName"]), "primaryComputer")
349 self
.assertEqual(len(applies_to
), 2)
350 self
.assertIn(user_dn
, applies_to
)
351 self
.assertIn(computer_dn
, applies_to
)
353 # No classes should raise CommandError.
354 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
355 "create", "--attribute=wWWHomePage",
357 self
.assertEqual(result
, -1)
358 self
.assertIn("Argument --class is required.", err
)
360 def test__delete(self
):
361 """Test deleting a claim type that is not protected."""
362 # Create non-protected claim type.
363 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
364 "create", "--attribute=msDS-SiteName",
365 "--name=siteName", "--class=computer")
366 self
.assertIsNone(result
, msg
=err
)
367 claim_type
= self
.get_claim_type("siteName")
368 self
.assertIsNotNone(claim_type
)
371 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
372 "delete", "--name=siteName")
373 self
.assertIsNone(result
, msg
=err
)
375 # Claim type shouldn't exist anymore.
376 claim_type
= self
.get_claim_type("siteName")
377 self
.assertIsNone(claim_type
)
379 def test_delete__protected(self
):
380 """Test deleting a protected claim type, with and without --force."""
381 # Create protected claim type.
382 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
383 "create", "--attribute=postalCode",
384 "--name=postcode", "--class=user",
386 self
.assertIsNone(result
, msg
=err
)
387 claim_type
= self
.get_claim_type("postcode")
388 self
.assertIsNotNone(claim_type
)
391 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
392 "delete", "--name=postcode")
393 self
.assertEqual(result
, -1)
395 # Claim type should still exist.
396 claim_type
= self
.get_claim_type("postcode")
397 self
.assertIsNotNone(claim_type
)
399 # Try a force delete instead.
400 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
401 "delete", "--name=postcode", "--force")
402 self
.assertIsNone(result
, msg
=err
)
404 # Claim type shouldn't exist anymore.
405 claim_type
= self
.get_claim_type("siteName")
406 self
.assertIsNone(claim_type
)
408 def test_delete__notfound(self
):
409 """Test deleting a claim type that doesn't exist."""
410 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
411 "delete", "--name", "doesNotExist")
412 self
.assertEqual(result
, -1)
413 self
.assertIn("Claim type doesNotExist not found.", err
)
415 def test_modify__description(self
):
416 """Test modifying a claim type description."""
417 self
.addCleanup(self
.delete_claim_type
, name
="company", force
=True)
418 self
.create_claim_type("company", classes
=["user"])
420 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
421 "modify", "--name", "company",
422 "--description=NewDescription")
423 self
.assertIsNone(result
, msg
=err
)
425 # Verify fields were changed.
426 claim_type
= self
.get_claim_type("company")
427 self
.assertEqual(str(claim_type
["description"]), "NewDescription")
429 def test_modify__classes(self
):
430 """Test modify claim type classes."""
431 schema_dn
= self
.samdb
.get_schema_basedn()
432 user_dn
= f
"CN=User,{schema_dn}"
433 computer_dn
= f
"CN=Computer,{schema_dn}"
435 self
.addCleanup(self
.delete_claim_type
, name
="seeAlso", force
=True)
436 self
.create_claim_type("seeAlso", classes
=["user"])
438 # First try removing all classes which shouldn't be allowed.
439 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
440 "modify", "--name", "seeAlso",
442 self
.assertEqual(result
, -1)
443 self
.assertIn("Class name is required.", err
)
445 # Try changing it to just --class=computer first.
446 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
447 "modify", "--name", "seeAlso",
449 self
.assertIsNone(result
, msg
=err
)
450 claim_type
= self
.get_claim_type("seeAlso")
451 applies_to
= [str(dn
) for dn
in claim_type
["msDS-ClaimTypeAppliesToClass"]]
452 self
.assertNotIn(user_dn
, applies_to
)
453 self
.assertIn(computer_dn
, applies_to
)
455 # Now try changing it to --class=user again.
456 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
457 "modify", "--name", "seeAlso",
459 self
.assertIsNone(result
, msg
=err
)
460 claim_type
= self
.get_claim_type("seeAlso")
461 applies_to
= [str(dn
) for dn
in claim_type
["msDS-ClaimTypeAppliesToClass"]]
462 self
.assertIn(user_dn
, applies_to
)
463 self
.assertNotIn(computer_dn
, applies_to
)
466 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
467 "modify", "--name", "seeAlso",
468 "--class=user", "--class=computer")
469 self
.assertIsNone(result
, msg
=err
)
470 claim_type
= self
.get_claim_type("seeAlso")
471 applies_to
= [str(dn
) for dn
in claim_type
["msDS-ClaimTypeAppliesToClass"]]
472 self
.assertIn(user_dn
, applies_to
)
473 self
.assertIn(computer_dn
, applies_to
)
475 def test_modify__enable_disable(self
):
476 """Test modify disabling and enabling a claim type."""
477 self
.addCleanup(self
.delete_claim_type
, name
="catalogs", force
=True)
478 self
.create_claim_type("catalogs", classes
=["user"])
480 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
481 "modify", "--name", "catalogs",
483 self
.assertIsNone(result
, msg
=err
)
485 # Check that claim type was disabled.
486 claim_type
= self
.get_claim_type("catalogs")
487 self
.assertEqual(str(claim_type
["Enabled"]), "FALSE")
489 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
490 "modify", "--name", "catalogs",
492 self
.assertIsNone(result
, msg
=err
)
494 # Check that claim type was enabled.
495 claim_type
= self
.get_claim_type("catalogs")
496 self
.assertEqual(str(claim_type
["Enabled"]), "TRUE")
498 def test_modify__protect_unprotect(self
):
499 """Test modify un-protecting and protecting a claim type."""
500 self
.addCleanup(self
.delete_claim_type
, name
="pager", force
=True)
501 self
.create_claim_type("pager", classes
=["user"])
503 utils
= SDUtils(self
.samdb
)
504 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
505 "modify", "--name", "pager",
507 self
.assertIsNone(result
, msg
=err
)
509 # Check that claim type was protected.
510 claim_type
= self
.get_claim_type("pager")
511 desc
= utils
.get_sd_as_sddl(claim_type
["dn"])
512 self
.assertIn("(D;;DTSD;;;WD)", desc
)
514 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
515 "modify", "--name", "pager",
517 self
.assertIsNone(result
, msg
=err
)
519 # Check that claim type was unprotected.
520 claim_type
= self
.get_claim_type("pager")
521 desc
= utils
.get_sd_as_sddl(claim_type
["dn"])
522 self
.assertNotIn("(D;;DTSD;;;WD)", desc
)
524 def test_modify__enable_disable_together(self
):
525 """Test modify claim type doesn't allow both --enable and --disable."""
526 self
.addCleanup(self
.delete_claim_type
,
527 name
="businessCategory", force
=True)
528 self
.create_claim_type("businessCategory", classes
=["user"])
530 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
531 "modify", "--name", "businessCategory",
532 "--enable", "--disable")
533 self
.assertEqual(result
, -1)
534 self
.assertIn("--enable and --disable cannot be used together.", err
)
536 def test_modify__protect_unprotect_together(self
):
537 """Test modify claim type using both --protect and --unprotect."""
538 self
.addCleanup(self
.delete_claim_type
,
539 name
="businessCategory", force
=True)
540 self
.create_claim_type("businessCategory", classes
=["user"])
542 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
543 "modify", "--name", "businessCategory",
544 "--protect", "--unprotect")
545 self
.assertEqual(result
, -1)
546 self
.assertIn("--protect and --unprotect cannot be used together.", err
)
548 def test_modify__notfound(self
):
549 """Test modify a claim type that doesn't exist."""
550 result
, out
, err
= self
.runcmd("domain", "claim", "claim-type",
551 "modify", "--name", "doesNotExist",
552 "--description=NewDescription")
553 self
.assertEqual(result
, -1)
554 self
.assertIn("Claim type doesNotExist not found.", err
)
557 class ValueTypeCmdTestCase(BaseClaimCmdTest
):
558 """Tests for the value-type command."""
561 """Test listing claim value types in list format."""
562 result
, out
, err
= self
.runcmd("domain", "claim", "value-type", "list")
563 self
.assertIsNone(result
, msg
=err
)
565 # base list of value types is there
566 for value_type
in VALUE_TYPES
:
567 self
.assertIn(value_type
, out
)
569 def test_list__json(self
):
570 """Test listing claim value types in JSON format."""
571 result
, out
, err
= self
.runcmd("domain", "claim", "value-type",
573 self
.assertIsNone(result
, msg
=err
)
575 # we should get valid json
576 json_result
= json
.loads(out
)
577 value_types
= list(json_result
.keys())
579 # base list of value types is there
580 for value_type
in VALUE_TYPES
:
581 self
.assertIn(value_type
, value_types
)
584 """Test viewing a single claim value type."""
585 result
, out
, err
= self
.runcmd("domain", "claim", "value-type",
586 "view", "--name", "Text")
587 self
.assertIsNone(result
, msg
=err
)
589 # we should get valid json
590 value_type
= json
.loads(out
)
592 # check a few fields only
593 self
.assertEqual(value_type
["name"], "MS-DS-Text")
594 self
.assertEqual(value_type
["displayName"], "Text")
595 self
.assertEqual(value_type
["msDS-ClaimValueType"], 3)
597 def test_view__name_missing(self
):
598 """Test viewing a claim value type with missing --name is handled."""
599 result
, out
, err
= self
.runcmd("domain", "claim", "value-type", "view")
600 self
.assertEqual(result
, -1)
601 self
.assertIn("Argument --name is required.", err
)
603 def test_view__notfound(self
):
604 """Test viewing a claim value type that doesn't exist is handled."""
605 result
, out
, err
= self
.runcmd("domain", "claim", "value-type",
606 "view", "--name", "doesNotExist")
607 self
.assertEqual(result
, -1)
608 self
.assertIn("Value type doesNotExist not found.", err
)