1 # Unix SMB/CIFS implementation.
3 # Blackbox tests for GMSA workflow.
5 # Copyright (C) Catalyst.Net Ltd. 2024
7 # Written by Rob van der Linde <rob@catalyst.net.nz>
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 from shlex
import quote
28 sys
.path
.insert(0, "bin/python")
29 os
.environ
["PYTHONUNBUFFERED"] = "1"
31 from samba
.domain
.models
import Computer
32 from samba
.tests
import BlackboxProcessError
, BlackboxTestCase
, connect_samdb
34 DC_SERVER
= os
.environ
["SERVER"]
35 SERVER
= os
.environ
["SERVER"]
36 SERVER_USERNAME
= os
.environ
["USERNAME"]
37 SERVER_PASSWORD
= os
.environ
["PASSWORD"]
39 HOST
= f
"ldap://{SERVER}"
40 ADMIN_CREDS
= f
"-U{SERVER_USERNAME}%{SERVER_PASSWORD}"
43 class GMSABlackboxTest(BlackboxTestCase
):
44 """Blackbox tests for GMSA management."""
48 cls
.lp
= cls
.get_loadparm()
49 cls
.env_creds
= cls
.get_env_credentials(lp
=cls
.lp
,
50 env_username
="USERNAME",
51 env_password
="PASSWORD",
54 cls
.samdb
= connect_samdb(HOST
, lp
=cls
.lp
, credentials
=cls
.env_creds
)
57 def getpassword(self
, account_name
, attrs
, creds
=ADMIN_CREDS
):
58 cmd
= f
"samba-tool user getpassword --attributes={quote(attrs)} {account_name} -H {HOST} {creds}"
59 ldif
= self
.check_output(cmd
).decode()
60 res
= self
.samdb
.parse_ldif(ldif
)
61 _
, user_message
= next(res
)
63 # check each attr is returned
64 for attr
in attrs
.split(","):
65 if attr
not in user_message
:
70 def test_gmsa_password_access(self
):
71 """Test machine account read password access."""
72 machine_account
= "Machine_Account$"
73 machine_password
= "T3stPassword0nly"
74 machine_creds
= f
"-U{machine_account}%{machine_password}"
75 gmsa_account
= "GMSA_Test_User$"
77 # Create a machine account and set the password.
78 self
.check_run(f
"samba-tool computer create {machine_account} -H {HOST} {ADMIN_CREDS}")
79 self
.addCleanup(self
.run_command
, f
"samba-tool computer delete {machine_account} -H {HOST} {ADMIN_CREDS}")
80 self
.check_run(f
"samba-tool user setpassword {machine_account} --newpassword={machine_password} -H {HOST} {ADMIN_CREDS}")
82 # Create a Group Managed Service Account with default SDDL.
83 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}")
84 self
.addCleanup(self
.run_command
, f
"samba-tool service-account delete --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
86 # Grant password read access to the machine account.
87 self
.check_run(f
"samba-tool service-account group-msa-membership add --name={gmsa_account} --principal={machine_account} -H {HOST} {ADMIN_CREDS}")
90 self
.getpassword(gmsa_account
, "unicodePwd", creds
=machine_creds
)
92 self
.fail("Failed to get unicodePwd despite being in the gMSA membership")
94 # Remove password read access from the machine account and verify.
95 self
.check_run(f
"samba-tool service-account group-msa-membership remove --name={gmsa_account} --principal={machine_account} -H {HOST} {ADMIN_CREDS}")
98 self
.assertRaises(KeyError, self
.getpassword
, gmsa_account
, "unicodePwd", creds
=machine_creds
)
99 except BlackboxProcessError
:
100 self
.fail("Unexpected subcommand failure retrieving unicodePwd")
102 def test_gmsa_add_sid_only_viewer(self
):
103 """Add unknown SID to password viewers and check group-msa-membership show output."""
104 gmsa_account
= "GMSA_Test_User$"
105 unknown_sid
= f
"{self.samdb.domain_sid}-9999"
107 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}")
108 self
.addCleanup(self
.run_command
, f
"samba-tool service-account delete --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
110 self
.check_run(f
"samba-tool service-account group-msa-membership add --name={gmsa_account} --principal={unknown_sid} -H {HOST} {ADMIN_CREDS}")
112 out
= self
.check_output(f
"samba-tool service-account group-msa-membership show --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
113 self
.assertIn(f
"<SID={unknown_sid}>", out
.decode())
115 def test_custom_sddl_as_list(self
):
116 """Test custom SDDL that can be represented by a simple list."""
117 machine_account
= "Machine_Account$"
118 machine_password
= "T3stPassword0nly"
119 gmsa_account
= "GMSA_Test_User$"
120 unknown_sid
= f
"{self.samdb.domain_sid}-9999"
122 # Create a machine account and set the password.
123 self
.check_run(f
"samba-tool computer create {machine_account} -H {HOST} {ADMIN_CREDS}")
124 self
.addCleanup(self
.run_command
, f
"samba-tool computer delete {machine_account} -H {HOST} {ADMIN_CREDS}")
125 self
.check_run(f
"samba-tool user setpassword {machine_account} --newpassword={machine_password} -H {HOST} {ADMIN_CREDS}")
127 # Create GMSA with custom SDDL this time rather than the command default.
128 initial_sddl
= f
"O:SYD:(A;;RP;;;{self.samdb.connecting_user_sid})"
129 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}')
130 self
.addCleanup(self
.run_command
, f
"samba-tool service-account delete --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
132 # Read the SDDL using service-account view JSON, it should be the same.
133 out
= self
.check_output(f
"samba-tool service-account view --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
134 gmsa
= json
.loads(out
.decode())
135 self
.assertEqual(gmsa
["msDS-GroupMSAMembership"], initial_sddl
)
137 # Add the machine account as a password viewer.
138 self
.check_run(f
"samba-tool service-account group-msa-membership add --name={gmsa_account} --principal={machine_account} -H {HOST} {ADMIN_CREDS}")
140 # Add the unknown SID as a viewer as well.
141 self
.check_run(f
"samba-tool service-account group-msa-membership add --name={gmsa_account} --principal={unknown_sid} -H {HOST} {ADMIN_CREDS}")
143 # Read the SDDL again and check if the machine account and unknown SID were added.
144 out
= self
.check_output(f
"samba-tool service-account view --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
145 gmsa
= json
.loads(out
.decode())
146 machine_user
= Computer
.get(self
.samdb
, account_name
=machine_account
)
147 expected_sddl
= (initial_sddl
+
148 f
"(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{machine_user.object_sid})" +
149 f
"(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{unknown_sid})")
150 self
.assertEqual(gmsa
["msDS-GroupMSAMembership"], expected_sddl
)
152 # Get the list as --json which is easier to parse in the test.
153 out
= self
.check_output(f
"samba-tool service-account group-msa-membership show --name={gmsa_account} --json -H {HOST} {ADMIN_CREDS}")
154 response
= json
.loads(out
.decode())
155 self
.assertListEqual(response
["trustees"], [
156 "CN=Administrator,CN=Users,DC=addom,DC=samba,DC=example,DC=com",
157 "CN=Machine_Account,CN=Computers,DC=addom,DC=samba,DC=example,DC=com",
158 f
"<SID={unknown_sid}>",
161 def test_custom_sddl_complex(self
):
162 """Test custom SDDL that cannot be display as a simple list.
164 In this case the "samba-tool service-account view" command
165 can be used to retrieve the SDDL.
167 machine_account
= "Machine_Account$"
168 machine_password
= "T3stPassword0nly"
169 gmsa_account
= "GMSA_Test_User$"
171 # Create a machine account and set the password.
172 self
.check_run(f
"samba-tool computer create {machine_account} -H {HOST} {ADMIN_CREDS}")
173 self
.addCleanup(self
.run_command
, f
"samba-tool computer delete {machine_account} -H {HOST} {ADMIN_CREDS}")
174 self
.check_run(f
"samba-tool user setpassword {machine_account} --newpassword={machine_password} -H {HOST} {ADMIN_CREDS}")
176 # Create GMSA with custom SDDL this time rather than the command default.
177 initial_sddl
= f
"O:SYD:(A;;RP;;;{self.samdb.connecting_user_sid})"
178 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}')
179 self
.addCleanup(self
.run_command
, f
"samba-tool service-account delete --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
181 # At first retrieving as a list will work fine.
182 out
= self
.check_output(f
"samba-tool service-account group-msa-membership show --name={gmsa_account} --json -H {HOST} {ADMIN_CREDS}")
183 response
= json
.loads(out
.decode())
184 self
.assertListEqual(
185 response
["trustees"],
186 ["CN=Administrator,CN=Users,DC=addom,DC=samba,DC=example,DC=com"])
188 # Set custom SDDL this time using the service-account modify command.
189 machine_user
= Computer
.get(self
.samdb
, account_name
=machine_account
)
190 deny_ace
= f
"(D;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;{machine_user.object_sid})"
191 sddl
= initial_sddl
+ deny_ace
192 self
.check_run(f
'samba-tool service-account modify --name={gmsa_account} --group-msa-membership="{sddl}" -H {HOST} {ADMIN_CREDS}')
194 # Group MSA membership can no longer be represented as a simple list.
195 with self
.assertRaisesRegex(BlackboxProcessError
, "Cannot be represented as a simple list"):
196 self
.check_run(f
"samba-tool service-account group-msa-membership show --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
198 # Retrieving the SDDL still works fine.
199 out
= self
.check_output(f
"samba-tool service-account view --name={gmsa_account} -H {HOST} {ADMIN_CREDS}")
200 gmsa
= json
.loads(out
.decode())
201 self
.assertEqual(gmsa
["msDS-GroupMSAMembership"], sddl
)
204 if __name__
== "__main__":