python: tests: blackbox test for GMSA
authorRob van der Linde <rob@catalyst.net.nz>
Thu, 7 Dec 2023 02:53:01 +0000 (15:53 +1300)
committerDouglas Bagnall <dbagnall@samba.org>
Thu, 21 Dec 2023 02:05:38 +0000 (02:05 +0000)
Signed-off-by: Rob van der Linde <rob@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
python/samba/tests/samba_tool/user_getpassword_gmsa.py [new file with mode: 0644]
selftest/knownfail.d/user_getpassword_gmsa [new file with mode: 0644]
source4/selftest/tests.py

diff --git a/python/samba/tests/samba_tool/user_getpassword_gmsa.py b/python/samba/tests/samba_tool/user_getpassword_gmsa.py
new file mode 100644 (file)
index 0000000..9844456
--- /dev/null
@@ -0,0 +1,171 @@
+# Unix SMB/CIFS implementation.
+#
+# Blackbox tests for reading Group Managed Service Account passwords
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# 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 sys
+import os
+
+sys.path.insert(0, "bin/python")
+os.environ["PYTHONUNBUFFERED"] = "1"
+
+from ldb import SCOPE_BASE
+
+from samba.credentials import Credentials, MUST_USE_KERBEROS
+from samba.dcerpc import security, samr
+from samba.dsdb import UF_WORKSTATION_TRUST_ACCOUNT
+from samba.netcmd.domain.models import User
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.tests import connect_samdb, connect_samdb_env, delete_force
+
+from samba.tests import BlackboxTestCase
+
+DC_SERVER = os.environ["SERVER"]
+SERVER = os.environ["SERVER"]
+SERVER_USERNAME = os.environ["USERNAME"]
+SERVER_PASSWORD = os.environ["PASSWORD"]
+
+HOST = f"ldap://{SERVER}"
+CREDS = f"-U{SERVER_USERNAME}%{SERVER_PASSWORD}"
+
+
+class GMSAPasswordTest(BlackboxTestCase):
+    """Blackbox tests for GMSA getpassword and connecting as that user."""
+
+    @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()
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.username = "GMSA_Test_User$"
+        cls.base_dn = f"CN=Managed Service Accounts,{cls.samdb.domain_dn()}"
+        cls.user_dn = f"CN={cls.username},{cls.base_dn}"
+
+        msg = cls.samdb.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+        connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+        domain_sid = security.dom_sid(cls.samdb.get_domain_sid())
+        allow_sddl = f"O:SYD:(A;;RP;;;{connecting_user_sid})"
+        allow_sd = ndr_pack(security.descriptor.from_sddl(allow_sddl, domain_sid))
+
+        details = {
+            "dn": str(cls.user_dn),
+            "objectClass": "msDS-GroupManagedServiceAccount",
+            "msDS-ManagedPasswordInterval": "1",
+            "msDS-GroupMSAMembership": allow_sd,
+            "sAMAccountName": cls.username,
+            "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT),
+        }
+
+        cls.samdb.add(details)
+        cls.addClassCleanup(delete_force, cls.samdb, cls.user_dn)
+
+        cls.user = User.get(cls.samdb, username=cls.username)
+
+    def getpassword(self, attrs):
+        cmd = f"user getpassword --attributes={attrs} {self.username}"
+
+        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_getpassword(self):
+        self.getpassword("virtualClearTextUTF16,unicodePwd")
+        self.getpassword("virtualClearTextUTF16")
+        self.getpassword("unicodePwd")
+
+    def test_utf16_password(self):
+        user_msg = self.getpassword("virtualClearTextUTF16")
+        password = user_msg["virtualClearTextUTF16"][0]
+
+        creds = self.insta_creds(template=self.env_creds)
+        creds.set_username(self.username)
+        creds.set_utf16_password(password)
+        db = connect_samdb(HOST, credentials=creds, lp=self.lp)
+
+        msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+        connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+        self.assertEqual(self.user.object_sid, connecting_user_sid)
+
+    def test_utf8_password(self):
+        user_msg = self.getpassword("virtualClearTextUTF8")
+        password = str(user_msg["virtualClearTextUTF8"][0])
+
+        creds = self.insta_creds(template=self.env_creds)
+        # Because the password has been converted to utf-8 via UTF16_MUNGED
+        # the nthash is no longer valid. We need to use AES kerberos ciphers
+        # for this to work.
+        creds.set_kerberos_state(MUST_USE_KERBEROS)
+        creds.set_username(self.username)
+        creds.set_password(password)
+        db = connect_samdb(HOST, credentials=creds, lp=self.lp)
+
+        msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+        connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+        self.assertEqual(self.user.object_sid, connecting_user_sid)
+
+    def test_unicode_pwd(self):
+        user_msg = self.getpassword("unicodePwd")
+
+        creds = self.insta_creds(template=self.env_creds)
+        creds.set_username(self.username)
+        nt_pass = samr.Password()
+        nt_pass.hash = list(user_msg["unicodePwd"][0])
+        creds.set_nt_hash(nt_pass)
+        db = connect_samdb(HOST, credentials=creds, lp=self.lp)
+
+        msg = db.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
+        connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
+
+        self.assertEqual(self.user.object_sid, connecting_user_sid)
+
+    @classmethod
+    def _make_cmdline(cls, line):
+        """Override to pass line as samba-tool subcommand instead.
+
+        Automatically fills in HOST and CREDS as well.
+        """
+        if isinstance(line, list):
+            cmd = ["samba-tool"] + line + ["-H", SERVER, CREDS]
+        else:
+            cmd = f"samba-tool {line} -H {HOST} {CREDS}"
+
+        return super()._make_cmdline(cmd)
+
+
+if __name__ == "__main__":
+    import unittest
+    unittest.main()
diff --git a/selftest/knownfail.d/user_getpassword_gmsa b/selftest/knownfail.d/user_getpassword_gmsa
new file mode 100644 (file)
index 0000000..4a4fec9
--- /dev/null
@@ -0,0 +1 @@
+^samba.tests.samba_tool.user_getpassword_gmsa.samba.tests.samba_tool.user_getpassword_gmsa.GMSAPasswordTest
index a518bcf79e2577d5315db01c7ae456b9c32a0e27..934aec5e1fb093e4f7970a8fbff193bab1ae2cb5 100755 (executable)
@@ -1124,6 +1124,9 @@ planpythontestsuite("none", "samba.tests.samba_tool.visualize")
 for env in all_fl_envs:
     planpythontestsuite(env + ":local", "samba.tests.samba_tool.fsmo")
 
+# test getpassword for group managed service accounts
+planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.user_getpassword_gmsa")
+
 # test samba-tool user, group, contact and computer edit command
 for env in all_fl_envs:
     env += ":local"