From 0bb67a3a7e79a687e7809ab41f056c36629bc19f Mon Sep 17 00:00:00 2001 From: Rob van der Linde Date: Thu, 12 Oct 2023 17:08:34 +1300 Subject: [PATCH] python: silos: add support for allowed to authenticate from silo shortcut this avoids the need to write SDDL, the user just needs to give the silo name Signed-off-by: Rob van der Linde Reviewed-by: Douglas Bagnall Reviewed-by: Andrew Bartlett Autobuild-User(master): Andrew Bartlett Autobuild-Date(master): Fri Oct 27 00:30:05 UTC 2023 on atb-devel-224 --- python/samba/netcmd/domain/auth/policy.py | 61 ++++++++- .../samba/netcmd/domain/models/auth_silo.py | 6 + .../tests/samba_tool/domain_auth_policy.py | 128 ++++++++++++++++++ 3 files changed, 194 insertions(+), 1 deletion(-) diff --git a/python/samba/netcmd/domain/auth/policy.py b/python/samba/netcmd/domain/auth/policy.py index 00f5dc87aed..d0ca96b677a 100644 --- a/python/samba/netcmd/domain/auth/policy.py +++ b/python/samba/netcmd/domain/auth/policy.py @@ -22,13 +22,24 @@ import samba.getopt as options from samba.netcmd import Command, CommandError, Option, SuperCommand -from samba.netcmd.domain.models import AuthenticationPolicy +from samba.netcmd.domain.models import AuthenticationPolicy, AuthenticationSilo from samba.netcmd.domain.models.auth_policy import MIN_TGT_LIFETIME,\ MAX_TGT_LIFETIME, StrongNTLMPolicy from samba.netcmd.domain.models.exceptions import ModelError from samba.netcmd.validators import Range +def check_similar_args(option, args): + """Helper method for checking similar mutually exclusive args. + + Example: --user-allowed-to-authenticate-from and + --user-allowed-to-authenticate-from-silo + """ + num = sum(arg is not None for arg in args) + if num > 1: + raise CommandError(f"{option} argument repeated {num} times.") + + class UserOptions(options.OptionGroup): """User options used by policy create and policy modify commands.""" @@ -49,6 +60,10 @@ class UserOptions(options.OptionGroup): help="Conditions user is allowed to authenticate from.", type=str, dest="allowed_to_authenticate_from", action="callback", callback=self.set_option) + self.add_option("--user-allowed-to-authenticate-from-silo", + help="User is allowed to authenticate from silo.", + type=str, dest="allowed_to_authenticate_from_silo", + action="callback", callback=self.set_option) self.add_option("--user-allowed-to-authenticate-to", help="Conditions user is allowed to authenticate to.", type=str, dest="allowed_to_authenticate_to", @@ -75,6 +90,10 @@ class ServiceOptions(options.OptionGroup): help="Conditions service is allowed to authenticate from.", type=str, dest="allowed_to_authenticate_from", action="callback", callback=self.set_option) + self.add_option("--service-allowed-to-authenticate-from-silo", + help="Service is allowed to authenticate from silo.", + type=str, dest="allowed_to_authenticate_from_silo", + action="callback", callback=self.set_option) self.add_option("--service-allowed-to-authenticate-to", help="Conditions service is allowed to authenticate to.", type=str, dest="allowed_to_authenticate_to", @@ -217,8 +236,28 @@ class cmd_domain_auth_policy_create(Command): if audit and enforce: raise CommandError("--audit and --enforce cannot be used together.") + # Check for repeated, similar arguments. + check_similar_args("--user-allowed-to-authenticate-from", + [useropts.allowed_to_authenticate_from, + useropts.allowed_to_authenticate_from_silo]) + check_similar_args("--service-allowed-to-authenticate-from", + [serviceopts.allowed_to_authenticate_from, + serviceopts.allowed_to_authenticate_from_silo]) + ldb = self.ldb_connect(hostopts, sambaopts, credopts) + # Generate SDDL for authenticating users from a silo + if useropts.allowed_to_authenticate_from_silo: + silo = AuthenticationSilo.get( + ldb, cn=useropts.allowed_to_authenticate_from_silo) + useropts.allowed_to_authenticate_from = silo.get_authentication_sddl() + + # Generate SDDL for authenticating service accounts from a silo + if serviceopts.allowed_to_authenticate_from_silo: + silo = AuthenticationSilo.get( + ldb, cn=serviceopts.allowed_to_authenticate_from_silo) + serviceopts.allowed_to_authenticate_from = silo.get_authentication_sddl() + try: policy = AuthenticationPolicy.get(ldb, cn=name) except ModelError as e: @@ -313,8 +352,28 @@ class cmd_domain_auth_policy_modify(Command): if audit and enforce: raise CommandError("--audit and --enforce cannot be used together.") + # Check for repeated, similar arguments. + check_similar_args("--user-allowed-to-authenticate-from", + [useropts.allowed_to_authenticate_from, + useropts.allowed_to_authenticate_from_silo]) + check_similar_args("--service-allowed-to-authenticate-from", + [serviceopts.allowed_to_authenticate_from, + serviceopts.allowed_to_authenticate_from_silo]) + ldb = self.ldb_connect(hostopts, sambaopts, credopts) + # Generate SDDL for authenticating users from a silo + if useropts.allowed_to_authenticate_from_silo: + silo = AuthenticationSilo.get( + ldb, cn=useropts.allowed_to_authenticate_from_silo) + useropts.allowed_to_authenticate_from = silo.get_authentication_sddl() + + # Generate SDDL for authenticating service accounts from a silo + if serviceopts.allowed_to_authenticate_from_silo: + silo = AuthenticationSilo.get( + ldb, cn=serviceopts.allowed_to_authenticate_from_silo) + serviceopts.allowed_to_authenticate_from = silo.get_authentication_sddl() + try: policy = AuthenticationPolicy.get(ldb, cn=name) except ModelError as e: diff --git a/python/samba/netcmd/domain/models/auth_silo.py b/python/samba/netcmd/domain/models/auth_silo.py index 6e624449d31..28d94e64fa3 100644 --- a/python/samba/netcmd/domain/models/auth_silo.py +++ b/python/samba/netcmd/domain/models/auth_silo.py @@ -22,6 +22,8 @@ from ldb import FLAG_MOD_ADD, FLAG_MOD_DELETE, LdbError, Message, MessageElement +from samba.sd_utils import escaped_claim_id + from .exceptions import AddMemberError, RemoveMemberError from .fields import DnField, BooleanField, StringField from .model import Model @@ -96,3 +98,7 @@ class AuthenticationSilo(Model): # If the modify operation was successful refresh members field. self.refresh(ldb, fields=["members"]) + + def get_authentication_sddl(self): + return ("O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/" + f"AuthenticationSilo/{escaped_claim_id(self.name)}))") diff --git a/python/samba/tests/samba_tool/domain_auth_policy.py b/python/samba/tests/samba_tool/domain_auth_policy.py index 2815afe59ac..9720f788e65 100644 --- a/python/samba/tests/samba_tool/domain_auth_policy.py +++ b/python/samba/tests/samba_tool/domain_auth_policy.py @@ -153,6 +153,28 @@ class AuthPolicyCmdTestCase(BaseAuthCmdTest): self.assertIn("--user-tgt-lifetime-mins must be between 45 and 2147483647", err) + def test_create__user_allowed_to_authenticate_from_silo(self): + """Tests the --user-allowed-to-authenticate-from-silo shortcut.""" + name = self.unique_name() + + self.addCleanup(self.delete_authentication_policy, name=name, force=True) + result, out, err = self.runcmd("domain", "auth", "policy", "create", + "--name", name, + "--user-allowed-to-authenticate-from-silo", + "Developers") + self.assertIsNone(result, msg=err) + + # Check policy fields. + policy = self.get_authentication_policy(name) + self.assertEqual(str(policy["cn"]), name) + + # Check generated SDDL. + desc = policy["msDS-UserAllowedToAuthenticateFrom"][0] + sddl = ndr_unpack(security.descriptor, desc).as_sddl() + self.assertEqual( + sddl, + "O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo/Developers))") + def test_create__service_tgt_lifetime_mins(self): """Test create a new authentication policy with --service-tgt-lifetime-mins. @@ -187,6 +209,28 @@ class AuthPolicyCmdTestCase(BaseAuthCmdTest): self.assertIn("--service-tgt-lifetime-mins must be between 45 and 2147483647", err) + def test_create__service_allowed_to_authenticate_from_silo(self): + """Tests the --service-allowed-to-authenticate-from-silo shortcut.""" + name = self.unique_name() + + self.addCleanup(self.delete_authentication_policy, name=name, force=True) + result, out, err = self.runcmd("domain", "auth", "policy", "create", + "--name", name, + "--service-allowed-to-authenticate-from-silo", + "Managers") + self.assertIsNone(result, msg=err) + + # Check policy fields. + policy = self.get_authentication_policy(name) + self.assertEqual(str(policy["cn"]), name) + desc = policy["msDS-ServiceAllowedToAuthenticateFrom"][0] + + # Check generated SDDL. + sddl = ndr_unpack(security.descriptor, desc).as_sddl() + self.assertEqual( + sddl, + "O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo/Managers))") + def test_create__computer_tgt_lifetime_mins(self): """Test create a new authentication policy with --computer-tgt-lifetime-mins. @@ -316,6 +360,44 @@ class AuthPolicyCmdTestCase(BaseAuthCmdTest): self.assertEqual(result, -1) self.assertIn("--protect and --unprotect cannot be used together.", err) + def test_create__user_allowed_to_authenticate_from_repeated(self): + """Test repeating similar arguments doesn't make sense to use together. + + --user-allowed-to-authenticate-from + --user-allowed-to-authenticate-from-silo + """ + sddl = "O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo/Developers))" + name = self.unique_name() + + result, out, err = self.runcmd("domain", "auth", "policy", "create", + "--name", name, + "--user-allowed-to-authenticate-from", + sddl, + "--user-allowed-to-authenticate-from-silo", + "Managers") + + self.assertEqual(result, -1) + self.assertIn("--user-allowed-to-authenticate-from argument repeated 2 times.", err) + + def test_create__service_allowed_to_authenticate_from_repeated(self): + """Test repeating similar arguments doesn't make sense to use together. + + --service-allowed-to-authenticate-from + --service-allowed-to-authenticate-from-silo + """ + sddl = "O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo/Managers))" + name = self.unique_name() + + result, out, err = self.runcmd("domain", "auth", "policy", "create", + "--name", name, + "--service-allowed-to-authenticate-from", + sddl, + "--service-allowed-to-authenticate-from-silo", + "QA") + + self.assertEqual(result, -1) + self.assertIn("--service-allowed-to-authenticate-from argument repeated 2 times.", err) + def test_create__fails(self): """Test creating an authentication policy, but it fails.""" name = self.unique_name() @@ -504,6 +586,29 @@ class AuthPolicyCmdTestCase(BaseAuthCmdTest): sddl = ndr_unpack(security.descriptor, desc).as_sddl() self.assertEqual(sddl, expected) + def test_modify__user_allowed_to_authenticate_from_silo(self): + """Test the --user-allowed-to-authenticate-from-silo shortcut.""" + name = self.unique_name() + + # Create a policy to modify for this test. + self.addCleanup(self.delete_authentication_policy, name=name, force=True) + self.runcmd("domain", "auth", "policy", "create", "--name", name) + + # Modify user allowed to authenticate from silo field + result, out, err = self.runcmd("domain", "auth", "policy", "modify", + "--name", name, + "--user-allowed-to-authenticate-from-silo", + "QA") + self.assertIsNone(result, msg=err) + + # Check generated SDDL. + policy = self.get_authentication_policy(name) + desc = policy["msDS-UserAllowedToAuthenticateFrom"][0] + sddl = ndr_unpack(security.descriptor, desc).as_sddl() + self.assertEqual( + sddl, + "O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo/QA))") + def test_modify__user_allowed_to_authenticate_to(self): """Modify authentication policy user allowed to authenticate to.""" name = self.unique_name() @@ -550,6 +655,29 @@ class AuthPolicyCmdTestCase(BaseAuthCmdTest): sddl = ndr_unpack(security.descriptor, desc).as_sddl() self.assertEqual(sddl, expected) + def test_modify__service_allowed_to_authenticate_from_silo(self): + """Test the --service-allowed-to-authenticate-from-silo shortcut.""" + name = self.unique_name() + + # Create a policy to modify for this test. + self.addCleanup(self.delete_authentication_policy, name=name, force=True) + self.runcmd("domain", "auth", "policy", "create", "--name", name) + + # Modify user allowed to authenticate from silo field + result, out, err = self.runcmd("domain", "auth", "policy", "modify", + "--name", name, + "--service-allowed-to-authenticate-from-silo", + "Developers") + self.assertIsNone(result, msg=err) + + # Check generated SDDL. + policy = self.get_authentication_policy(name) + desc = policy["msDS-ServiceAllowedToAuthenticateFrom"][0] + sddl = ndr_unpack(security.descriptor, desc).as_sddl() + self.assertEqual( + sddl, + "O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/AuthenticationSilo/Developers))") + def test_modify__service_allowed_to_authenticate_to(self): """Modify authentication policy service allowed to authenticate to.""" name = self.unique_name() -- 2.34.1