tests: gmsa blackbox tests
authorRob van der Linde <rob@catalyst.net.nz>
Thu, 29 Feb 2024 22:22:03 +0000 (11:22 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Wed, 20 Mar 2024 04:53:57 +0000 (04:53 +0000)
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): Wed Mar 20 04:53:57 UTC 2024 on atb-devel-224

python/samba/tests/blackbox/gmsa.py [new file with mode: 0644]
selftest/knownfail.d/gmsa
source4/selftest/tests.py

diff --git a/python/samba/tests/blackbox/gmsa.py b/python/samba/tests/blackbox/gmsa.py
new file mode 100644 (file)
index 0000000..830585d
--- /dev/null
@@ -0,0 +1,202 @@
+# Unix SMB/CIFS implementation.
+#
+# Blackbox tests for GMSA workflow.
+#
+# Copyright (C) Catalyst.Net Ltd. 2024
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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 json
+import os
+import sys
+from shlex import quote
+
+sys.path.insert(0, "bin/python")
+os.environ["PYTHONUNBUFFERED"] = "1"
+
+from samba.domain.models import Computer
+from samba.tests import BlackboxProcessError, BlackboxTestCase, connect_samdb
+
+DC_SERVER = os.environ["SERVER"]
+SERVER = os.environ["SERVER"]
+SERVER_USERNAME = os.environ["USERNAME"]
+SERVER_PASSWORD = os.environ["PASSWORD"]
+
+HOST = f"ldap://{SERVER}"
+ADMIN_CREDS = f"-U{SERVER_USERNAME}%{SERVER_PASSWORD}"
+
+
+class GMSABlackboxTest(BlackboxTestCase):
+    """Blackbox tests for GMSA management."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.lp = cls.get_loadparm()
+        cls.env_creds = cls.get_env_credentials(lp=cls.lp,
+                                                env_username="USERNAME",
+                                                env_password="PASSWORD",
+                                                env_domain="DOMAIN",
+                                                env_realm="REALM")
+        cls.samdb = connect_samdb(HOST, lp=cls.lp, credentials=cls.env_creds)
+        super().setUpClass()
+
+    def getpassword(self, account_name, attrs, creds=ADMIN_CREDS):
+        cmd = f"samba-tool user getpassword --attributes={quote(attrs)} {account_name} -H {HOST} {creds}"
+        ldif = self.check_output(cmd).decode()
+        res = self.samdb.parse_ldif(ldif)
+        _, user_message = next(res)
+
+        # check each attr is returned
+        for attr in attrs.split(","):
+            self.assertIn(attr, user_message)
+
+        return user_message
+
+    def test_gmsa_password_access(self):
+        """Test machine account read password access."""
+        machine_account = "Machine_Account$"
+        machine_password = "T3stPassword0nly"
+        machine_creds = f"-U{machine_account}%{machine_password}"
+        gmsa_account = "GMSA_Test_User$"
+
+        # Create a machine account and set the password.
+        self.check_run(f"samba-tool computer create {machine_account} -H {HOST} {ADMIN_CREDS}")
+        self.addCleanup(self.run_command, f"samba-tool computer delete {machine_account} -H {HOST} {ADMIN_CREDS}")
+        self.check_run(f"samba-tool user setpassword {machine_account} --newpassword={machine_password} -H {HOST} {ADMIN_CREDS}")
+
+        # Create a Group Managed Service Account with default SDDL.
+        self.check_run(f"samba-tool service-account create --name={gmsa_account} --dns-host-name=example.com --managed-password-interval=1 -H {HOST} {ADMIN_CREDS}")
+        self.addCleanup(self.run_command, f"samba-tool service-account delete --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+
+        # Grant password read access to the machine account.
+        self.check_run(f"samba-tool service-account group-msa-membership add --name={gmsa_account} --principal={machine_account} -H {HOST} {ADMIN_CREDS}")
+
+        self.getpassword(gmsa_account, "unicodePwd", creds=machine_creds)
+
+        # Remove password read access from the machine account and verify.
+        self.check_run(f"samba-tool service-account group-msa-membership remove --name={gmsa_account} --principal={machine_account} -H {HOST} {ADMIN_CREDS}")
+
+        try:
+            self.getpassword(gmsa_account, "unicodePwd", creds=machine_creds)
+        except BlackboxProcessError:
+            self.fail("Unexpected failure retrieving unicodePwd")
+
+    def test_gmsa_add_sid_only_viewer(self):
+        """Add unknown SID to password viewers and check group-msa-membership show output."""
+        gmsa_account = "GMSA_Test_User$"
+        unknown_sid = f"{self.samdb.domain_sid}-9999"
+
+        self.check_run(f"samba-tool service-account create --name={gmsa_account} --dns-host-name=example.com --managed-password-interval=1 -H {HOST} {ADMIN_CREDS}")
+        self.addCleanup(self.run_command, f"samba-tool service-account delete --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+
+        self.check_run(f"samba-tool service-account group-msa-membership add --name={gmsa_account} --principal={unknown_sid} -H {HOST} {ADMIN_CREDS}")
+
+        out = self.check_output(f"samba-tool service-account group-msa-membership show --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+        self.assertIn(f"<SID={unknown_sid}>", out.decode())
+
+    def test_custom_sddl_as_list(self):
+        """Test custom SDDL that can be represented by a simple list."""
+        machine_account = "Machine_Account$"
+        machine_password = "T3stPassword0nly"
+        gmsa_account = "GMSA_Test_User$"
+        unknown_sid = f"{self.samdb.domain_sid}-9999"
+
+        # Create a machine account and set the password.
+        self.check_run(f"samba-tool computer create {machine_account} -H {HOST} {ADMIN_CREDS}")
+        self.addCleanup(self.run_command, f"samba-tool computer delete {machine_account} -H {HOST} {ADMIN_CREDS}")
+        self.check_run(f"samba-tool user setpassword {machine_account} --newpassword={machine_password} -H {HOST} {ADMIN_CREDS}")
+
+        # Create GMSA with custom SDDL this time rather than the command default.
+        initial_sddl = f"O:SYD:(A;;RP;;;{self.samdb.connecting_user_sid})"
+        self.check_run(f'samba-tool service-account create --name={gmsa_account} --dns-host-name=example.com --group-msa-membership="{initial_sddl}" --managed-password-interval=1 -H {HOST} {ADMIN_CREDS}')
+        self.addCleanup(self.run_command, f"samba-tool service-account delete --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+
+        # Read the SDDL using service-account view JSON, it should be the same.
+        out = self.check_output(f"samba-tool service-account view --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+        gmsa = json.loads(out.decode())
+        self.assertEqual(gmsa["msDS-GroupMSAMembership"], initial_sddl)
+
+        # Add the machine account as a password viewer.
+        self.check_run(f"samba-tool service-account group-msa-membership add --name={gmsa_account} --principal={machine_account} -H {HOST} {ADMIN_CREDS}")
+
+        # Add the unknown SID as a viewer as well.
+        self.check_run(f"samba-tool service-account group-msa-membership add --name={gmsa_account} --principal={unknown_sid} -H {HOST} {ADMIN_CREDS}")
+
+        # Read the SDDL again and check if the machine account and unknown SID were added.
+        out = self.check_output(f"samba-tool service-account view --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+        gmsa = json.loads(out.decode())
+        machine_user = Computer.get(self.samdb, account_name=machine_account)
+        expected_sddl = (initial_sddl +
+                         f"(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{machine_user.object_sid})" +
+                         f"(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{unknown_sid})")
+        self.assertEqual(gmsa["msDS-GroupMSAMembership"], expected_sddl)
+
+        # Get the list as --json which is easier to parse in the test.
+        out = self.check_output(f"samba-tool service-account group-msa-membership show --name={gmsa_account} --json -H {HOST} {ADMIN_CREDS}")
+        response = json.loads(out.decode())
+        self.assertListEqual(response["trustees"], [
+            "CN=Administrator,CN=Users,DC=addom,DC=samba,DC=example,DC=com",
+            "CN=Machine_Account,CN=Computers,DC=addom,DC=samba,DC=example,DC=com",
+            f"<SID={unknown_sid}>",
+        ])
+
+    def test_custom_sddl_complex(self):
+        """Test custom SDDL that cannot be display as a simple list.
+
+        In this case the "samba-tool service-account view" command
+        can be used to retrieve the SDDL.
+        """
+        machine_account = "Machine_Account$"
+        machine_password = "T3stPassword0nly"
+        gmsa_account = "GMSA_Test_User$"
+
+        # Create a machine account and set the password.
+        self.check_run(f"samba-tool computer create {machine_account} -H {HOST} {ADMIN_CREDS}")
+        self.addCleanup(self.run_command, f"samba-tool computer delete {machine_account} -H {HOST} {ADMIN_CREDS}")
+        self.check_run(f"samba-tool user setpassword {machine_account} --newpassword={machine_password} -H {HOST} {ADMIN_CREDS}")
+
+        # Create GMSA with custom SDDL this time rather than the command default.
+        initial_sddl = f"O:SYD:(A;;RP;;;{self.samdb.connecting_user_sid})"
+        self.check_run(f'samba-tool service-account create --name={gmsa_account} --dns-host-name=example.com --group-msa-membership="{initial_sddl}" --managed-password-interval=1 -H {HOST} {ADMIN_CREDS}')
+        self.addCleanup(self.run_command, f"samba-tool service-account delete --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+
+        # At first retrieving as a list will work fine.
+        out = self.check_output(f"samba-tool service-account group-msa-membership show --name={gmsa_account} --json -H {HOST} {ADMIN_CREDS}")
+        response = json.loads(out.decode())
+        self.assertListEqual(
+            response["trustees"],
+            ["CN=Administrator,CN=Users,DC=addom,DC=samba,DC=example,DC=com"])
+
+        # Set custom SDDL this time using the service-account modify command.
+        machine_user = Computer.get(self.samdb, account_name=machine_account)
+        deny_ace = f"(D;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{machine_user.object_sid})"
+        sddl = initial_sddl + deny_ace
+        self.check_run(f'samba-tool service-account modify --name={gmsa_account} --group-msa-membership="{sddl}" -H {HOST} {ADMIN_CREDS}')
+
+        # Group MSA membership can no longer be represented as a simple list.
+        with self.assertRaisesRegexp(BlackboxProcessError, "Cannot be represented as a simple list"):
+            self.check_run(f"samba-tool service-account group-msa-membership show --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+
+        # Retrieving the SDDL still works fine.
+        out = self.check_output(f"samba-tool service-account view --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
+        gmsa = json.loads(out.decode())
+        self.assertEqual(gmsa["msDS-GroupMSAMembership"], sddl)
+
+
+if __name__ == "__main__":
+    import unittest
+    unittest.main()
index 4b1ab7e9d8b06b37bed7ec0ec0d5f7e9abef3365..1d819e55b637dd4b98ecd7254bf22179df8f9f41 100644 (file)
@@ -1 +1,2 @@
-^samba.tests.dckeytab.samba.tests.dckeytab.DCKeytabTests.test_export_keytab_gmsa
\ No newline at end of file
+^samba.tests.dckeytab.samba.tests.dckeytab.DCKeytabTests.test_export_keytab_gmsa
+^samba.tests.blackbox.gmsa.samba.tests.blackbox.gmsa.GMSABlackboxTest.test_gmsa_password_access
index 219ae8b1f83fe1880e5aefce5a74a2bb9118ddba..310226f625af668d89a934e3289bb53f4736767d 100755 (executable)
@@ -604,6 +604,8 @@ plantestsuite("samba4.blackbox.test_old_enctypes", "fl2003dc:local", [os.path.jo
 
 planpythontestsuite("ad_dc_default", "samba.tests.blackbox.claims")
 
+planpythontestsuite("ad_dc_default", "samba.tests.blackbox.gmsa")
+
 if have_heimdal_support:
     plantestsuite("samba4.blackbox.kpasswd",
                   "ad_dc:local",