python: silos: add support for allowed to authenticate from silo shortcut
authorRob van der Linde <rob@catalyst.net.nz>
Thu, 12 Oct 2023 04:08:34 +0000 (17:08 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Fri, 27 Oct 2023 00:30:05 +0000 (00:30 +0000)
this avoids the need to write SDDL, the user just needs to give the silo name

Signed-off-by: Rob van der Linde <rob@catalyst.net.nz>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Autobuild-User(master): Andrew Bartlett <abartlet@samba.org>
Autobuild-Date(master): Fri Oct 27 00:30:05 UTC 2023 on atb-devel-224

python/samba/netcmd/domain/auth/policy.py
python/samba/netcmd/domain/models/auth_silo.py
python/samba/tests/samba_tool/domain_auth_policy.py

index 00f5dc87aed280d80369a211873e9fe2f26814ca..d0ca96b677a4526c611c03212ddd9eaa432c492a 100644 (file)
 
 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:
index 6e624449d315005669ddb3b5d3fa4bff44e6c793..28d94e64fa3571b6a7d71219b26a7b1909c6e580 100644 (file)
@@ -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)}))")
index 2815afe59accc53d4ea710533866626e826085b2..9720f788e65d923ffeb116dfaf8e53b24087c3a7 100644 (file)
@@ -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()