1 # Tests for SamDb password change audit logging.
2 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 from __future__ import print_function
19 """Tests for the SamDb logging of password changes.
23 from samba.dcerpc.messaging import MSG_DSDB_LOG, DSDB_EVENT_NAME
24 from ldb import ERR_NO_SUCH_OBJECT
25 from samba.samdb import SamDB
26 from samba.auth import system_session
29 from samba.tests.audit_log_base import AuditLogTestBase
30 from samba.tests import delete_force
31 from samba.net import Net
33 from samba.dcerpc import security, lsa
35 USER_NAME = "auditlogtestuser"
36 USER_PASS = samba.generate_random_password(32, 32)
39 class AuditLogDsdbTests(AuditLogTestBase):
42 self.message_type = MSG_DSDB_LOG
43 self.event_type = DSDB_EVENT_NAME
44 super(AuditLogDsdbTests, self).setUp()
46 self.remoteAddress = os.environ["CLIENT_IP"]
47 self.server_ip = os.environ["SERVER_IP"]
49 host = "ldap://%s" % os.environ["SERVER"]
50 self.ldb = SamDB(url=host,
51 session_info=system_session(),
52 credentials=self.get_credentials(),
53 lp=self.get_loadparm())
54 self.server = os.environ["SERVER"]
56 # Gets back the basedn
57 self.base_dn = self.ldb.domain_dn()
59 # Get the old "dSHeuristics" if it was set
60 dsheuristics = self.ldb.get_dsheuristics()
62 # Set the "dSHeuristics" to activate the correct "userPassword"
64 self.ldb.set_dsheuristics("000000001")
66 # Reset the "dSHeuristics" as they were before
67 self.addCleanup(self.ldb.set_dsheuristics, dsheuristics)
69 # Get the old "minPwdAge"
70 minPwdAge = self.ldb.get_minPwdAge()
72 # Set it temporarily to "0"
73 self.ldb.set_minPwdAge("0")
74 self.base_dn = self.ldb.domain_dn()
76 # Reset the "minPwdAge" as it was before
77 self.addCleanup(self.ldb.set_minPwdAge, minPwdAge)
79 # (Re)adds the test user USER_NAME with password USER_PASS
80 delete_force(self.ldb, "cn=" + USER_NAME + ",cn=users," + self.base_dn)
82 "dn": "cn=" + USER_NAME + ",cn=users," + self.base_dn,
83 "objectclass": "user",
84 "sAMAccountName": USER_NAME,
85 "userPassword": USER_PASS
89 # Discard the messages from the setup code
91 def discardSetupMessages(self, dn):
92 self.waitForMessages(2, dn=dn)
93 self.discardMessages()
96 self.discardMessages()
97 super(AuditLogDsdbTests, self).tearDown()
99 def haveExpectedTxn(self, expected):
100 if self.context["txnMessage"] is not None:
101 txn = self.context["txnMessage"]["dsdbTransaction"]
102 if txn["transactionId"] == expected:
106 def waitForTransaction(self, expected, connection=None):
107 """Wait for a transaction message to arrive
108 The connection is passed through to keep the connection alive
109 until all the logging messages have been received.
112 self.connection = connection
114 start_time = time.time()
115 while not self.haveExpectedTxn(expected):
116 self.msg_ctx.loop_once(0.1)
117 if time.time() - start_time > 1:
118 self.connection = None
121 self.connection = None
122 return self.context["txnMessage"]
124 def test_net_change_password(self):
126 dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
127 self.discardSetupMessages(dn)
129 creds = self.insta_creds(template=self.get_credentials())
131 lp = self.get_loadparm()
132 net = Net(creds, lp, server=self.server)
133 password = "newPassword!!42"
135 net.change_password(newpassword=password.encode('utf-8'),
137 oldpassword=USER_PASS)
139 messages = self.waitForMessages(1, net, dn=dn)
140 print("Received %d messages" % len(messages))
143 "Did not receive the expected number of messages")
145 audit = messages[0]["dsdbChange"]
146 self.assertEquals("Modify", audit["operation"])
147 self.assertFalse(audit["performedAsSystem"])
148 self.assertTrue(dn.lower(), audit["dn"].lower())
149 self.assertRegexpMatches(audit["remoteAddress"],
151 session_id = self.get_session()
152 self.assertEquals(session_id, audit["sessionId"])
153 service_description = self.get_service_description()
154 self.assertEquals(service_description, "DCE/RPC")
155 self.assertTrue(self.is_guid(audit["transactionId"]))
157 attributes = audit["attributes"]
158 self.assertEquals(1, len(attributes))
159 actions = attributes["clearTextPassword"]["actions"]
160 self.assertEquals(1, len(actions))
161 self.assertTrue(actions[0]["redacted"])
162 self.assertEquals("replace", actions[0]["action"])
164 def test_net_set_password(self):
166 dn = "CN=" + USER_NAME + ",CN=Users," + self.base_dn
167 self.discardSetupMessages(dn)
169 creds = self.insta_creds(template=self.get_credentials())
171 lp = self.get_loadparm()
172 net = Net(creds, lp, server=self.server)
173 password = "newPassword!!42"
174 domain = lp.get("workgroup")
176 net.set_password(newpassword=password.encode('utf-8'),
177 account_name=USER_NAME,
179 messages = self.waitForMessages(1, net, dn=dn)
180 print("Received %d messages" % len(messages))
183 "Did not receive the expected number of messages")
184 audit = messages[0]["dsdbChange"]
185 self.assertEquals("Modify", audit["operation"])
186 self.assertFalse(audit["performedAsSystem"])
187 self.assertEquals(dn, audit["dn"])
188 self.assertRegexpMatches(audit["remoteAddress"],
190 session_id = self.get_session()
191 self.assertEquals(session_id, audit["sessionId"])
192 service_description = self.get_service_description()
193 self.assertEquals(service_description, "DCE/RPC")
194 self.assertTrue(self.is_guid(audit["transactionId"]))
196 attributes = audit["attributes"]
197 self.assertEquals(1, len(attributes))
198 actions = attributes["clearTextPassword"]["actions"]
199 self.assertEquals(1, len(actions))
200 self.assertTrue(actions[0]["redacted"])
201 self.assertEquals("replace", actions[0]["action"])
203 def test_ldap_change_password(self):
205 dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
206 self.discardSetupMessages(dn)
208 new_password = samba.generate_random_password(32, 32)
209 dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
210 self.ldb.modify_ldif(
212 "changetype: modify\n" +
213 "delete: userPassword\n" +
214 "userPassword: " + USER_PASS + "\n" +
215 "add: userPassword\n" +
216 "userPassword: " + new_password + "\n")
218 messages = self.waitForMessages(1)
219 print("Received %d messages" % len(messages))
222 "Did not receive the expected number of messages")
224 audit = messages[0]["dsdbChange"]
225 self.assertEquals("Modify", audit["operation"])
226 self.assertFalse(audit["performedAsSystem"])
227 self.assertEquals(dn, audit["dn"])
228 self.assertRegexpMatches(audit["remoteAddress"],
230 self.assertTrue(self.is_guid(audit["sessionId"]))
231 session_id = self.get_session()
232 self.assertEquals(session_id, audit["sessionId"])
233 service_description = self.get_service_description()
234 self.assertEquals(service_description, "LDAP")
236 attributes = audit["attributes"]
237 self.assertEquals(1, len(attributes))
238 actions = attributes["userPassword"]["actions"]
239 self.assertEquals(2, len(actions))
240 self.assertTrue(actions[0]["redacted"])
241 self.assertEquals("delete", actions[0]["action"])
242 self.assertTrue(actions[1]["redacted"])
243 self.assertEquals("add", actions[1]["action"])
245 def test_ldap_replace_password(self):
247 dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
248 self.discardSetupMessages(dn)
250 new_password = samba.generate_random_password(32, 32)
251 self.ldb.modify_ldif(
253 "changetype: modify\n" +
254 "replace: userPassword\n" +
255 "userPassword: " + new_password + "\n")
257 messages = self.waitForMessages(1, dn=dn)
258 print("Received %d messages" % len(messages))
261 "Did not receive the expected number of messages")
263 audit = messages[0]["dsdbChange"]
264 self.assertEquals("Modify", audit["operation"])
265 self.assertFalse(audit["performedAsSystem"])
266 self.assertTrue(dn.lower(), audit["dn"].lower())
267 self.assertRegexpMatches(audit["remoteAddress"],
269 self.assertTrue(self.is_guid(audit["sessionId"]))
270 session_id = self.get_session()
271 self.assertEquals(session_id, audit["sessionId"])
272 service_description = self.get_service_description()
273 self.assertEquals(service_description, "LDAP")
274 self.assertTrue(self.is_guid(audit["transactionId"]))
276 attributes = audit["attributes"]
277 self.assertEquals(1, len(attributes))
278 actions = attributes["userPassword"]["actions"]
279 self.assertEquals(1, len(actions))
280 self.assertTrue(actions[0]["redacted"])
281 self.assertEquals("replace", actions[0]["action"])
283 def test_ldap_add_user(self):
285 # The setup code adds a user, so we check for the dsdb events
287 dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
288 messages = self.waitForMessages(2, dn=dn)
289 print("Received %d messages" % len(messages))
292 "Did not receive the expected number of messages")
294 audit = messages[1]["dsdbChange"]
295 self.assertEquals("Add", audit["operation"])
296 self.assertFalse(audit["performedAsSystem"])
297 self.assertEquals(dn, audit["dn"])
298 self.assertRegexpMatches(audit["remoteAddress"],
300 session_id = self.get_session()
301 self.assertEquals(session_id, audit["sessionId"])
302 service_description = self.get_service_description()
303 self.assertEquals(service_description, "LDAP")
304 self.assertTrue(self.is_guid(audit["sessionId"]))
305 self.assertTrue(self.is_guid(audit["transactionId"]))
307 attributes = audit["attributes"]
308 self.assertEquals(3, len(attributes))
310 actions = attributes["objectclass"]["actions"]
311 self.assertEquals(1, len(actions))
312 self.assertEquals("add", actions[0]["action"])
313 self.assertEquals(1, len(actions[0]["values"]))
314 self.assertEquals("user", actions[0]["values"][0]["value"])
316 actions = attributes["sAMAccountName"]["actions"]
317 self.assertEquals(1, len(actions))
318 self.assertEquals("add", actions[0]["action"])
319 self.assertEquals(1, len(actions[0]["values"]))
320 self.assertEquals(USER_NAME, actions[0]["values"][0]["value"])
322 actions = attributes["userPassword"]["actions"]
323 self.assertEquals(1, len(actions))
324 self.assertEquals("add", actions[0]["action"])
325 self.assertTrue(actions[0]["redacted"])
327 def test_samdb_delete_user(self):
329 dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
330 self.discardSetupMessages(dn)
332 self.ldb.deleteuser(USER_NAME)
334 messages = self.waitForMessages(1, dn=dn)
335 print("Received %d messages" % len(messages))
338 "Did not receive the expected number of messages")
340 audit = messages[0]["dsdbChange"]
341 self.assertEquals("Delete", audit["operation"])
342 self.assertFalse(audit["performedAsSystem"])
343 self.assertTrue(dn.lower(), audit["dn"].lower())
344 self.assertRegexpMatches(audit["remoteAddress"],
346 self.assertTrue(self.is_guid(audit["sessionId"]))
347 self.assertEquals(0, audit["statusCode"])
348 self.assertEquals("Success", audit["status"])
349 session_id = self.get_session()
350 self.assertEquals(session_id, audit["sessionId"])
351 service_description = self.get_service_description()
352 self.assertEquals(service_description, "LDAP")
354 transactionId = audit["transactionId"]
355 message = self.waitForTransaction(transactionId)
356 audit = message["dsdbTransaction"]
357 self.assertEquals("commit", audit["action"])
358 self.assertTrue(self.is_guid(audit["transactionId"]))
359 self.assertTrue(audit["duration"] > 0)
361 def test_samdb_delete_non_existent_dn(self):
363 DOES_NOT_EXIST = "doesNotExist"
364 dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
365 self.discardSetupMessages(dn)
367 dn = "cn=" + DOES_NOT_EXIST + ",cn=users," + self.base_dn
370 self.fail("Exception not thrown")
374 messages = self.waitForMessages(1)
375 print("Received %d messages" % len(messages))
378 "Did not receive the expected number of messages")
380 audit = messages[0]["dsdbChange"]
381 self.assertEquals("Delete", audit["operation"])
382 self.assertFalse(audit["performedAsSystem"])
383 self.assertTrue(dn.lower(), audit["dn"].lower())
384 self.assertRegexpMatches(audit["remoteAddress"],
386 self.assertEquals(ERR_NO_SUCH_OBJECT, audit["statusCode"])
387 self.assertEquals("No such object", audit["status"])
388 self.assertTrue(self.is_guid(audit["sessionId"]))
389 session_id = self.get_session()
390 self.assertEquals(session_id, audit["sessionId"])
391 service_description = self.get_service_description()
392 self.assertEquals(service_description, "LDAP")
394 transactionId = audit["transactionId"]
395 message = self.waitForTransaction(transactionId)
396 audit = message["dsdbTransaction"]
397 self.assertEquals("rollback", audit["action"])
398 self.assertTrue(self.is_guid(audit["transactionId"]))
399 self.assertTrue(audit["duration"] > 0)
401 def test_create_and_delete_secret_over_lsa(self):
403 dn = "cn=Test Secret,CN=System," + self.base_dn
404 self.discardSetupMessages(dn)
406 creds = self.insta_creds(template=self.get_credentials())
407 lsa_conn = lsa.lsarpc(
408 "ncacn_np:%s" % self.server,
411 lsa_handle = lsa_conn.OpenPolicy2(
413 attr=lsa.ObjectAttribute(),
414 access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
415 secret_name = lsa.String()
416 secret_name.string = "G$Test"
417 lsa_conn.CreateSecret(
420 access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
422 messages = self.waitForMessages(1, dn=dn)
423 print("Received %d messages" % len(messages))
426 "Did not receive the expected number of messages")
428 audit = messages[0]["dsdbChange"]
429 self.assertEquals("Add", audit["operation"])
430 self.assertTrue(audit["performedAsSystem"])
431 self.assertTrue(dn.lower(), audit["dn"].lower())
432 self.assertRegexpMatches(audit["remoteAddress"],
434 self.assertTrue(self.is_guid(audit["sessionId"]))
435 session_id = self.get_session()
436 self.assertEquals(session_id, audit["sessionId"])
437 service_description = self.get_service_description()
438 self.assertEquals(service_description, "DCE/RPC")
439 attributes = audit["attributes"]
440 self.assertEquals(2, len(attributes))
442 object_class = attributes["objectClass"]
443 self.assertEquals(1, len(object_class["actions"]))
444 action = object_class["actions"][0]
445 self.assertEquals("add", action["action"])
446 values = action["values"]
447 self.assertEquals(1, len(values))
448 self.assertEquals("secret", values[0]["value"])
450 cn = attributes["cn"]
451 self.assertEquals(1, len(cn["actions"]))
452 action = cn["actions"][0]
453 self.assertEquals("add", action["action"])
454 values = action["values"]
455 self.assertEquals(1, len(values))
456 self.assertEquals("Test Secret", values[0]["value"])
459 # Now delete the secret.
460 self.discardMessages()
461 h = lsa_conn.OpenSecret(
464 access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
466 lsa_conn.DeleteObject(h)
467 messages = self.waitForMessages(1, dn=dn)
468 print("Received %d messages" % len(messages))
471 "Did not receive the expected number of messages")
473 dn = "cn=Test Secret,CN=System," + self.base_dn
474 audit = messages[0]["dsdbChange"]
475 self.assertEquals("Delete", audit["operation"])
476 self.assertTrue(audit["performedAsSystem"])
477 self.assertTrue(dn.lower(), audit["dn"].lower())
478 self.assertRegexpMatches(audit["remoteAddress"],
480 self.assertTrue(self.is_guid(audit["sessionId"]))
481 session_id = self.get_session()
482 self.assertEquals(session_id, audit["sessionId"])
483 service_description = self.get_service_description()
484 self.assertEquals(service_description, "DCE/RPC")
486 def test_modify(self):
488 dn = "cn=" + USER_NAME + ",cn=users," + self.base_dn
489 self.discardSetupMessages(dn)
492 # Add an attribute value
494 self.ldb.modify_ldif(
496 "changetype: modify\n" +
497 "add: carLicense\n" +
498 "carLicense: license-01\n")
500 messages = self.waitForMessages(1, dn=dn)
501 print("Received %d messages" % len(messages))
504 "Did not receive the expected number of messages")
506 audit = messages[0]["dsdbChange"]
507 self.assertEquals("Modify", audit["operation"])
508 self.assertFalse(audit["performedAsSystem"])
509 self.assertEquals(dn, audit["dn"])
510 self.assertRegexpMatches(audit["remoteAddress"],
512 self.assertTrue(self.is_guid(audit["sessionId"]))
513 session_id = self.get_session()
514 self.assertEquals(session_id, audit["sessionId"])
515 service_description = self.get_service_description()
516 self.assertEquals(service_description, "LDAP")
518 attributes = audit["attributes"]
519 self.assertEquals(1, len(attributes))
520 actions = attributes["carLicense"]["actions"]
521 self.assertEquals(1, len(actions))
522 self.assertEquals("add", actions[0]["action"])
523 values = actions[0]["values"]
524 self.assertEquals(1, len(values))
525 self.assertEquals("license-01", values[0]["value"])
528 # Add an another value to the attribute
530 self.discardMessages()
531 self.ldb.modify_ldif(
533 "changetype: modify\n" +
534 "add: carLicense\n" +
535 "carLicense: license-02\n")
537 messages = self.waitForMessages(1, dn=dn)
538 print("Received %d messages" % len(messages))
541 "Did not receive the expected number of messages")
542 attributes = messages[0]["dsdbChange"]["attributes"]
543 self.assertEquals(1, len(attributes))
544 actions = attributes["carLicense"]["actions"]
545 self.assertEquals(1, len(actions))
546 self.assertEquals("add", actions[0]["action"])
547 values = actions[0]["values"]
548 self.assertEquals(1, len(values))
549 self.assertEquals("license-02", values[0]["value"])
552 # Add an another two values to the attribute
554 self.discardMessages()
555 self.ldb.modify_ldif(
557 "changetype: modify\n" +
558 "add: carLicense\n" +
559 "carLicense: license-03\n" +
560 "carLicense: license-04\n")
562 messages = self.waitForMessages(1, dn=dn)
563 print("Received %d messages" % len(messages))
566 "Did not receive the expected number of messages")
567 attributes = messages[0]["dsdbChange"]["attributes"]
568 self.assertEquals(1, len(attributes))
569 actions = attributes["carLicense"]["actions"]
570 self.assertEquals(1, len(actions))
571 self.assertEquals("add", actions[0]["action"])
572 values = actions[0]["values"]
573 self.assertEquals(2, len(values))
574 self.assertEquals("license-03", values[0]["value"])
575 self.assertEquals("license-04", values[1]["value"])
578 # delete two values to the attribute
580 self.discardMessages()
581 self.ldb.modify_ldif(
583 "changetype: delete\n" +
584 "delete: carLicense\n" +
585 "carLicense: license-03\n" +
586 "carLicense: license-04\n")
588 messages = self.waitForMessages(1, dn=dn)
589 print("Received %d messages" % len(messages))
592 "Did not receive the expected number of messages")
593 attributes = messages[0]["dsdbChange"]["attributes"]
594 self.assertEquals(1, len(attributes))
595 actions = attributes["carLicense"]["actions"]
596 self.assertEquals(1, len(actions))
597 self.assertEquals("delete", actions[0]["action"])
598 values = actions[0]["values"]
599 self.assertEquals(2, len(values))
600 self.assertEquals("license-03", values[0]["value"])
601 self.assertEquals("license-04", values[1]["value"])
604 # replace two values to the attribute
606 self.discardMessages()
607 self.ldb.modify_ldif(
609 "changetype: delete\n" +
610 "replace: carLicense\n" +
611 "carLicense: license-05\n" +
612 "carLicense: license-06\n")
614 messages = self.waitForMessages(1, dn=dn)
615 print("Received %d messages" % len(messages))
618 "Did not receive the expected number of messages")
619 attributes = messages[0]["dsdbChange"]["attributes"]
620 self.assertEquals(1, len(attributes))
621 actions = attributes["carLicense"]["actions"]
622 self.assertEquals(1, len(actions))
623 self.assertEquals("replace", actions[0]["action"])
624 values = actions[0]["values"]
625 self.assertEquals(2, len(values))
626 self.assertEquals("license-05", values[0]["value"])
627 self.assertEquals("license-06", values[1]["value"])