From 85ca9e7cba4041138b0e3d5057afba5f8ac1575e Mon Sep 17 00:00:00 2001 From: Rob van der Linde Date: Fri, 23 Feb 2024 13:48:02 +1300 Subject: [PATCH] netcmd: tests: add tests for service-account commands Signed-off-by: Rob van der Linde Reviewed-by: Andrew Bartlett Reviewed-by: Douglas Bagnall --- .../samba/tests/samba_tool/service_account.py | 333 ++++++++++++++++++ source4/selftest/tests.py | 1 + 2 files changed, 334 insertions(+) create mode 100644 python/samba/tests/samba_tool/service_account.py diff --git a/python/samba/tests/samba_tool/service_account.py b/python/samba/tests/samba_tool/service_account.py new file mode 100644 index 00000000000..b74fb9d5bc2 --- /dev/null +++ b/python/samba/tests/samba_tool/service_account.py @@ -0,0 +1,333 @@ +# Unix SMB/CIFS implementation. +# +# Tests for samba-tool service-account commands. +# +# Copyright (C) Catalyst.Net Ltd. 2024 +# +# Written by Rob van der Linde +# +# 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 . +# + +import json +import os + +from samba.netcmd.domain.models import Group, GroupManagedServiceAccount, User +from samba.netcmd.domain.models.constants import GROUP_MSA_MEMBERSHIP_DEFAULT + +from .base import SambaToolCmdTest + +HOST = "ldap://{DC_SERVER}".format(**os.environ) +CREDS = "-U{DC_USERNAME}%{DC_PASSWORD}".format(**os.environ) + + +class ServiceAccountTests(SambaToolCmdTest): + + @classmethod + def setUpClass(cls): + cls.samdb = cls.getSamDB("-H", HOST, CREDS) + super().setUpClass() + + @classmethod + def setUpTestData(cls): + """Setup initial data without the samba-tool command.""" + cls.accounts = [ + GroupManagedServiceAccount.create(cls.samdb, name="foo", + dns_host_name="example.com"), + GroupManagedServiceAccount.create(cls.samdb, name="bar", + dns_host_name="example.com"), + GroupManagedServiceAccount.create(cls.samdb, name="baz", + dns_host_name="example.com"), + ] + + for account in cls.accounts: + cls.addClassCleanup(account.delete, cls.samdb) + + @classmethod + def _run(cls, *argv): + """Override _run, so we don't always have to pass HOST and CREDS.""" + args = list(argv) + args.extend(["-H", HOST, CREDS]) + return super()._run(*args) + + runcmd = _run + runsubcmd = _run + + @classmethod + def delete_service_account(cls, name): + """Delete a service account using samba-tool.""" + result, out, err = cls.runcmd("service-account", "delete", + "--name", name) + assert result is None + assert out.startswith("Deleted group managed service account") + + @classmethod + def create_service_account(cls, name, dns_host_name="example.com", + managed_password_interval=None): + """Create a service account using samba-tool. + + Adds a class cleanup to automatically delete the gmsa and the end + of the test case. + """ + # required arguments + cmd = ["service-account", "create", + "--name", name, + "--dns-host-name", dns_host_name] + + # defaults to 30 if left None + if managed_password_interval is not None: + cmd += ["--managed-password-interval", str(managed_password_interval)] + + # create gmsa and setup cleanup + result, out, err = cls.runcmd(*cmd) + assert result is None + assert out.startswith("Created group managed service account") + cls.addClassCleanup(cls.delete_service_account, name=name) + + def test_list(self): + """List group managed service accounts with samba-tool.""" + result, out, err = self.runcmd("service-account", "list") + self.assertIsNone(result, msg=err) + + self.assertIn("foo$", out) + self.assertIn("bar$", out) + self.assertIn("baz$", out) + + def test_list__json(self): + """List group managed service accounts in json format.""" + result, out, err = self.runcmd("service-account", "list", "--json") + self.assertIsNone(result, msg=err) + accounts = json.loads(out) + + self.assertIn("foo$", accounts) + self.assertIn("bar$", accounts) + self.assertIn("baz$", accounts) + + def test_create(self): + """Create a group managed service account using samba-tool.""" + # Create a Group Managed Service account using samba-tool. + name = self.unique_name() + self.create_service_account(name, + dns_host_name="test.com", + managed_password_interval=60) + + # Group Managed Service count exists. + # Since GroupManagedServiceAccount is also a Computer it ends in '$' + gmsa = GroupManagedServiceAccount.get(self.samdb, username=name + "$") + self.assertIsNotNone(gmsa) + self.assertEqual(gmsa.username, name + "$") + self.assertEqual(gmsa.dns_host_name, "test.com") + self.assertEqual(gmsa.managed_password_interval, 60) + + def test_view(self): + """View a group managed service account using samba-tool.""" + result, out, err = self.runcmd("service-account", "view", + "--name", "foo") + self.assertIsNone(result, msg=err) + + # Service account view always returns JSON. + response = json.loads(out) + self.assertEqual(response["cn"], "foo") + self.assertEqual(response["dNSHostName"], "example.com") + self.assertEqual(response["msDS-ManagedPasswordInterval"], 30) + + def test_delete(self): + """Delete a group managed service account using samba-tool.""" + # Create the gmsa without samba-tool. + name = self.unique_name() + GroupManagedServiceAccount.create(self.samdb, name=name, + dns_host_name="example.com"), + + # The group managed service account exists. + gmsa = GroupManagedServiceAccount.get(self.samdb, username=name + "$") + self.assertIsNotNone(gmsa) + + # Now delete the gmsa. + result, out, err = self.runcmd("service-account", "delete", + "--name", name) + self.assertIsNone(result, msg=err) + + # Service account is gone. + gmsa = GroupManagedServiceAccount.get(self.samdb, username=name + "$") + self.assertIsNone(gmsa, msg="Group Managed Service Account not deleted.") + + def test_modify(self): + """Modify a group managed service account and manually set SDDL.""" + name = self.unique_name() + gmsa = GroupManagedServiceAccount.create(self.samdb, name=name, + dns_host_name="example.com") + self.addCleanup(gmsa.delete, self.samdb) + + # Build some SDDL for adding a user manually. + bob = User.get(self.samdb, username="bob") + sddl = gmsa.group_msa_membership.as_sddl() + sddl += f"(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{bob.object_sid})" + + result, out, err = self.runcmd("service-account", "modify", + "--name", name, + "--dns-host-name", "new.example.com", + "--group-msa-membership", sddl) + self.assertIsNone(result, msg=err) + + # Check field changes and see if the new user is in there. + gmsa = GroupManagedServiceAccount.get(self.samdb, username=name + "$") + self.assertEqual(gmsa.dns_host_name, "new.example.com") + self.assertIn(bob.object_sid, gmsa.trustees) + + +class ServiceAccountGroupMSAMembershipTests(SambaToolCmdTest): + + @classmethod + def setUpClass(cls): + cls.samdb = cls.getSamDB("-H", HOST, CREDS) + super().setUpClass() + + @classmethod + def setUpTestData(cls): + """Setup initial data without the samba-tool command.""" + # Add a user other than the Administrator to the default SDDL. + jane = User.get(cls.samdb, username="jane") + sddl = f"{GROUP_MSA_MEMBERSHIP_DEFAULT}(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{jane.object_sid})" + cls.gmsa = GroupManagedServiceAccount.create(cls.samdb, name="gmsa", + dns_host_name="example.com", + group_msa_membership=sddl) + + cls.addClassCleanup(cls.gmsa.delete, cls.samdb) + + @classmethod + def _run(cls, *argv): + """Override _run, so we don't always have to pass HOST and CREDS.""" + args = list(argv) + args.extend(["-H", HOST, CREDS]) + return super()._run(*args) + + runcmd = _run + runsubcmd = _run + + def test_show(self): + """Show password viewers on a Group Managed Service Account.""" + result, out, err = self.runcmd("service-account", + "group-msa-membership", "show", + "--name", self.gmsa.username) + self.assertIsNone(result, msg=err) + + # Plain text output. + self.assertIn( + "Account-DN: CN=gmsa,CN=Managed Service Accounts,DC=addom,DC=samba,DC=example,DC=com", out) + self.assertIn( + "CN=Administrator,CN=Users,DC=addom,DC=samba,DC=example,DC=com", out) + self.assertIn( + "CN=jane,CN=Users,DC=addom,DC=samba,DC=example,DC=com", out) + + def test_show__json(self): + """Show password viewers on a Group Managed Service Account as JSON.""" + result, out, err = self.runcmd("service-account", + "group-msa-membership", "show", + "--name", self.gmsa.username, + "--json") + self.assertIsNone(result, msg=err) + + # JSON output. + response = json.loads(out) + self.assertEqual(response["dn"], str(self.gmsa.dn)) + self.assertListEqual(response["trustees"], [ + "CN=Administrator,CN=Users,DC=addom,DC=samba,DC=example,DC=com", + "CN=jane,CN=Users,DC=addom,DC=samba,DC=example,DC=com" + ]) + + def test_add__username(self): + """Add principal to a Group Managed Service Account by username.""" + alice = User.get(self.samdb, username="alice") + name = self.unique_name() + gmsa = GroupManagedServiceAccount.create(self.samdb, name=name, + dns_host_name="example.com") + self.addCleanup(gmsa.delete, self.samdb) + + # Add user 'alice' by username. + result, out, err = self.runcmd("service-account", + "group-msa-membership", "add", + "--name", gmsa.username, + "--principal", alice.username) + self.assertIsNone(result, msg=err) + + # See if user was added. + gmsa.refresh(self.samdb) + self.assertIn(alice.object_sid, gmsa.trustees) + + def test_add__dn(self): + """Add principal to a Group Managed Service Account by dn.""" + admins = Group.get(self.samdb, name="DnsAdmins") + name = self.unique_name() + gmsa = GroupManagedServiceAccount.create(self.samdb, name=name, + dns_host_name="example.com") + self.addCleanup(gmsa.delete, self.samdb) + + # Add group 'DnsAdmins' by dn. + result, out, err = self.runcmd("service-account", + "group-msa-membership", "add", + "--name", gmsa.username, + "--principal", str(admins.dn)) + self.assertIsNone(result, msg=err) + + # See if group was added. + gmsa.refresh(self.samdb) + self.assertIn(admins.object_sid, gmsa.trustees) + + def test_remove__username(self): + """Remove principal from a Group Managed Service Account by username.""" + # Create a GMSA with custom SDDL and add extra user. + bob = User.get(self.samdb, username="bob") + sddl = f"{GROUP_MSA_MEMBERSHIP_DEFAULT}(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{bob.object_sid})" + name = self.unique_name() + gmsa = GroupManagedServiceAccount.create(self.samdb, name=name, + dns_host_name="example.com", + group_msa_membership=sddl) + + # The user is in list to start with. + self.assertIn(bob.object_sid, gmsa.trustees) + + # Remove user 'bob' by username. + result, out, err = self.runcmd("service-account", + "group-msa-membership", "remove", + "--name", gmsa.username, + "--principal", bob.username) + self.assertIsNone(result, msg=err) + + # See if user was removed. + gmsa.refresh(self.samdb) + self.assertNotIn(bob.object_sid, gmsa.trustees) + + def test_remove__dn(self): + """Remove principal from a Group Managed Service Account by dn.""" + # Create a GMSA with custom SDDL and add extra group. + admins = Group.get(self.samdb, name="DnsAdmins") + sddl = f"{GROUP_MSA_MEMBERSHIP_DEFAULT}(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{admins.object_sid})" + name = self.unique_name() + gmsa = GroupManagedServiceAccount.create(self.samdb, name=name, + dns_host_name="example.com", + group_msa_membership=sddl) + + # The group is in list to start with. + self.assertIn(admins.object_sid, gmsa.trustees) + + # Remove group 'DnsAdmins' by dn. + result, out, err = self.runcmd("service-account", + "group-msa-membership", "remove", + "--name", gmsa.username, + "--principal", str(admins.dn)) + self.assertIsNone(result, msg=err) + + # See if group was removed. + gmsa.refresh(self.samdb) + self.assertNotIn(admins.object_sid, gmsa.trustees) diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index 222e60d9887..219ae8b1f83 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -1170,6 +1170,7 @@ planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.domain_auth_policy" planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.domain_auth_silo") planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.domain_kds_root_key") planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.domain_models") +planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.service_account") planpythontestsuite("schema_dc:local", "samba.tests.samba_tool.schema") planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl") planpythontestsuite("none", "samba.tests.samba_tool.provision_password_check")