tests/gkdi: Remove implicit clock skew offset
[Samba.git] / python / samba / samdb.py
blobb831cf562506e86cd0f527d466215f01c9765634
1 # Unix SMB/CIFS implementation.
2 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010
3 # Copyright (C) Matthias Dieter Wallnoefer 2009
5 # Based on the original in EJS:
6 # Copyright (C) Andrew Tridgell <tridge@samba.org> 2005
7 # Copyright (C) Giampaolo Lauria <lauria2@yahoo.com> 2011
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/>.
23 """Convenience functions for using the SAM."""
25 import samba
26 import ldb
27 import time
28 import base64
29 import os
30 import re
31 from samba import dsdb, dsdb_dns
32 from samba.ndr import ndr_unpack, ndr_pack
33 from samba.dcerpc import drsblobs, misc
34 from samba.common import normalise_int32
35 from samba.common import get_bytes, cmp
36 from samba.dcerpc import security
37 from samba import is_ad_dc_built
38 import binascii
40 __docformat__ = "restructuredText"
43 def get_default_backend_store():
44 return "tdb"
46 class SamDBError(Exception):
47 pass
49 class SamDBNotFoundError(SamDBError):
50 pass
52 class SamDB(samba.Ldb):
53 """The SAM database."""
55 hash_oid_name = {}
57 class _CleanUpOnError:
58 def __init__(self, samdb, dn):
59 self.samdb = samdb
60 self.dn = dn
62 def __enter__(self):
63 pass
65 def __exit__(self, exc_type, exc_val, exc_tb):
66 if exc_type is not None:
67 # We failed to modify the account. If we connected to the
68 # database over LDAP, we don't have transactions, and so when
69 # we call transaction_cancel(), the account will still exist in
70 # a half-created state. We'll delete the account to ensure that
71 # doesn't happen.
72 self.samdb.delete(self.dn)
74 # Don't suppress any exceptions
75 return False
77 def __init__(self, url=None, lp=None, modules_dir=None, session_info=None,
78 credentials=None, flags=ldb.FLG_DONT_CREATE_DB,
79 options=None, global_schema=True,
80 auto_connect=True, am_rodc=None):
81 self.lp = lp
82 if not auto_connect:
83 url = None
84 elif url is None and lp is not None:
85 url = lp.samdb_url()
87 self.url = url
89 super().__init__(url=url, lp=lp, modules_dir=modules_dir,
90 session_info=session_info, credentials=credentials, flags=flags,
91 options=options)
93 if global_schema:
94 dsdb._dsdb_set_global_schema(self)
96 if am_rodc is not None:
97 dsdb._dsdb_set_am_rodc(self, am_rodc)
99 def connect(self, url=None, flags=0, options=None):
100 """connect to the database"""
101 if self.lp is not None and not os.path.exists(url):
102 url = self.lp.private_path(url)
103 self.url = url
105 super().connect(url=url, flags=flags, options=options)
107 def __repr__(self):
108 if self.url:
109 return f"<SamDB {id(self):x} ({self.url})>"
111 return f"<SamDB {id(self):x} (no connection)>"
113 __str__ = __repr__
115 def am_rodc(self):
116 """return True if we are an RODC"""
117 return dsdb._am_rodc(self)
119 def am_pdc(self):
120 """return True if we are an PDC emulator"""
121 return dsdb._am_pdc(self)
123 def domain_dn(self):
124 """return the domain DN"""
125 return str(self.get_default_basedn())
127 def schema_dn(self):
128 """return the schema partition dn"""
129 return str(self.get_schema_basedn())
131 def disable_account(self, search_filter):
132 """Disables an account
134 :param search_filter: LDAP filter to find the user (eg
135 samccountname=name)
138 flags = samba.dsdb.UF_ACCOUNTDISABLE
139 self.toggle_userAccountFlags(search_filter, flags, on=True)
141 def enable_account(self, search_filter):
142 """Enables an account
144 :param search_filter: LDAP filter to find the user (eg
145 samccountname=name)
148 flags = samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_PASSWD_NOTREQD
149 self.toggle_userAccountFlags(search_filter, flags, on=False)
151 def toggle_userAccountFlags(self, search_filter, flags, flags_str=None,
152 on=True, strict=False):
153 """Toggle_userAccountFlags
155 :param search_filter: LDAP filter to find the user (eg
156 samccountname=name)
157 :param flags: samba.dsdb.UF_* flags
158 :param on: on=True (default) => set, on=False => unset
159 :param strict: strict=False (default) ignore if no action is needed
160 strict=True raises an Exception if...
162 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
163 expression=search_filter, attrs=["userAccountControl"])
164 if len(res) == 0:
165 raise Exception("Unable to find account where '%s'" % search_filter)
166 assert(len(res) == 1)
167 account_dn = res[0].dn
169 old_uac = int(res[0]["userAccountControl"][0])
170 if on:
171 if strict and (old_uac & flags):
172 error = "Account flag(s) '%s' already set" % flags_str
173 raise Exception(error)
175 new_uac = old_uac | flags
176 else:
177 if strict and not (old_uac & flags):
178 error = "Account flag(s) '%s' already unset" % flags_str
179 raise Exception(error)
181 new_uac = old_uac & ~flags
183 if old_uac == new_uac:
184 return
186 mod = """
187 dn: %s
188 changetype: modify
189 delete: userAccountControl
190 userAccountControl: %u
191 add: userAccountControl
192 userAccountControl: %u
193 """ % (account_dn, old_uac, new_uac)
194 self.modify_ldif(mod)
196 def force_password_change_at_next_login(self, search_filter):
197 """Forces a password change at next login
199 :param search_filter: LDAP filter to find the user (eg
200 samccountname=name)
202 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
203 expression=search_filter, attrs=[])
204 if len(res) == 0:
205 raise Exception('Unable to find user "%s"' % search_filter)
206 assert(len(res) == 1)
207 user_dn = res[0].dn
209 mod = """
210 dn: %s
211 changetype: modify
212 replace: pwdLastSet
213 pwdLastSet: 0
214 """ % (user_dn)
215 self.modify_ldif(mod)
217 def unlock_account(self, search_filter):
218 """Unlock a user account by resetting lockoutTime to 0.
219 This does also reset the badPwdCount to 0.
221 :param search_filter: LDAP filter to find the user (e.g.
222 sAMAccountName=username)
224 res = self.search(base=self.domain_dn(),
225 scope=ldb.SCOPE_SUBTREE,
226 expression=search_filter,
227 attrs=[])
228 if len(res) == 0:
229 raise SamDBNotFoundError('Unable to find user "%s"' % search_filter)
230 if len(res) != 1:
231 raise SamDBError('User "%s" is not unique' % search_filter)
232 user_dn = res[0].dn
234 mod = """
235 dn: %s
236 changetype: modify
237 replace: lockoutTime
238 lockoutTime: 0
239 """ % (user_dn)
240 self.modify_ldif(mod)
242 def newgroup(self, groupname, groupou=None, grouptype=None,
243 description=None, mailaddress=None, notes=None, sd=None,
244 gidnumber=None, nisdomain=None):
245 """Adds a new group with additional parameters
247 :param groupname: Name of the new group
248 :param grouptype: Type of the new group
249 :param description: Description of the new group
250 :param mailaddress: Email address of the new group
251 :param notes: Notes of the new group
252 :param gidnumber: GID Number of the new group
253 :param nisdomain: NIS Domain Name of the new group
254 :param sd: security descriptor of the object
257 if groupou:
258 group_dn = "CN=%s,%s,%s" % (groupname, groupou, self.domain_dn())
259 else:
260 group_dn = "CN=%s,%s" % (groupname, self.get_wellknown_dn(
261 self.get_default_basedn(),
262 dsdb.DS_GUID_USERS_CONTAINER))
264 # The new user record. Note the reliance on the SAMLDB module which
265 # fills in the default information
266 ldbmessage = {"dn": group_dn,
267 "sAMAccountName": groupname,
268 "objectClass": "group"}
270 if grouptype is not None:
271 ldbmessage["groupType"] = normalise_int32(grouptype)
273 if description is not None:
274 ldbmessage["description"] = description
276 if mailaddress is not None:
277 ldbmessage["mail"] = mailaddress
279 if notes is not None:
280 ldbmessage["info"] = notes
282 if gidnumber is not None:
283 ldbmessage["gidNumber"] = normalise_int32(gidnumber)
285 if nisdomain is not None:
286 ldbmessage["msSFU30Name"] = groupname
287 ldbmessage["msSFU30NisDomain"] = nisdomain
289 if sd is not None:
290 ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
292 self.add(ldbmessage)
294 def deletegroup(self, groupname):
295 """Deletes a group
297 :param groupname: Name of the target group
300 groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
301 self.transaction_start()
302 try:
303 targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
304 expression=groupfilter, attrs=[])
305 if len(targetgroup) == 0:
306 raise Exception('Unable to find group "%s"' % groupname)
307 assert(len(targetgroup) == 1)
308 self.delete(targetgroup[0].dn)
309 except:
310 self.transaction_cancel()
311 raise
312 else:
313 self.transaction_commit()
315 def group_member_filter(self, member, member_types):
316 filter = ""
318 all_member_types = [ 'user',
319 'group',
320 'computer',
321 'serviceaccount',
322 'contact',
325 if 'all' in member_types:
326 member_types = all_member_types
328 for member_type in member_types:
329 if member_type not in all_member_types:
330 raise Exception('Invalid group member type "%s". '
331 'Valid types are %s and all.' %
332 (member_type, ", ".join(all_member_types)))
334 if 'user' in member_types:
335 filter += ('(&(sAMAccountName=%s)(samAccountType=%d))' %
336 (ldb.binary_encode(member), dsdb.ATYPE_NORMAL_ACCOUNT))
337 if 'group' in member_types:
338 filter += ('(&(sAMAccountName=%s)'
339 '(objectClass=group)'
340 '(!(groupType:1.2.840.113556.1.4.803:=1)))' %
341 ldb.binary_encode(member))
342 if 'computer' in member_types:
343 samaccountname = member
344 if member[-1] != '$':
345 samaccountname = "%s$" % member
346 filter += ('(&(samAccountType=%d)'
347 '(!(objectCategory=msDS-ManagedServiceAccount))'
348 '(sAMAccountName=%s))' %
349 (dsdb.ATYPE_WORKSTATION_TRUST,
350 ldb.binary_encode(samaccountname)))
351 if 'serviceaccount' in member_types:
352 samaccountname = member
353 if member[-1] != '$':
354 samaccountname = "%s$" % member
355 filter += ('(&(samAccountType=%d)'
356 '(objectCategory=msDS-ManagedServiceAccount)'
357 '(sAMAccountName=%s))' %
358 (dsdb.ATYPE_WORKSTATION_TRUST,
359 ldb.binary_encode(samaccountname)))
360 if 'contact' in member_types:
361 filter += ('(&(objectCategory=Person)(!(objectSid=*))(name=%s))' %
362 ldb.binary_encode(member))
364 filter = "(|%s)" % filter
366 return filter
368 def add_remove_group_members(self, groupname, members,
369 add_members_operation=True,
370 member_types=None,
371 member_base_dn=None):
372 """Adds or removes group members
374 :param groupname: Name of the target group
375 :param members: list of group members
376 :param add_members_operation: Defines if its an add or remove
377 operation
379 if member_types is None:
380 member_types = ['user', 'group', 'computer']
382 groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (
383 ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
385 self.transaction_start()
386 try:
387 targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
388 expression=groupfilter, attrs=['member'])
389 if len(targetgroup) == 0:
390 raise Exception('Unable to find group "%s"' % groupname)
391 assert(len(targetgroup) == 1)
393 modified = False
395 addtargettogroup = """
396 dn: %s
397 changetype: modify
398 """ % (str(targetgroup[0].dn))
400 for member in members:
401 targetmember_dn = None
402 if member_base_dn is None:
403 member_base_dn = self.domain_dn()
405 try:
406 membersid = security.dom_sid(member)
407 targetmember_dn = "<SID=%s>" % str(membersid)
408 except ValueError:
409 pass
411 if targetmember_dn is None:
412 try:
413 member_dn = ldb.Dn(self, member)
414 if member_dn.get_linearized() == member_dn.extended_str(1):
415 full_member_dn = self.normalize_dn_in_domain(member_dn)
416 else:
417 full_member_dn = member_dn
418 targetmember_dn = full_member_dn.extended_str(1)
419 except ValueError as e:
420 pass
422 if targetmember_dn is None:
423 filter = self.group_member_filter(member, member_types)
424 targetmember = self.search(base=member_base_dn,
425 scope=ldb.SCOPE_SUBTREE,
426 expression=filter,
427 attrs=[])
429 if len(targetmember) > 1:
430 targetmemberlist_str = ""
431 for msg in targetmember:
432 targetmemberlist_str += "%s\n" % msg.get("dn")
433 raise Exception('Found multiple results for "%s":\n%s' %
434 (member, targetmemberlist_str))
435 if len(targetmember) != 1:
436 raise Exception('Unable to find "%s". Operation cancelled.' % member)
437 targetmember_dn = targetmember[0].dn.extended_str(1)
439 if add_members_operation is True and (targetgroup[0].get('member') is None or get_bytes(targetmember_dn) not in [str(x) for x in targetgroup[0]['member']]):
440 modified = True
441 addtargettogroup += """add: member
442 member: %s
443 """ % (str(targetmember_dn))
445 elif add_members_operation is False and (targetgroup[0].get('member') is not None and get_bytes(targetmember_dn) in targetgroup[0]['member']):
446 modified = True
447 addtargettogroup += """delete: member
448 member: %s
449 """ % (str(targetmember_dn))
451 if modified is True:
452 self.modify_ldif(addtargettogroup)
454 except:
455 self.transaction_cancel()
456 raise
457 else:
458 self.transaction_commit()
460 def prepare_attr_replace(self, msg, old, attr_name, value):
461 """Changes the MessageElement with the given attr_name of the
462 given Message. If the value is "" set an empty value and the flag
463 FLAG_MOD_DELETE, otherwise set the new value and FLAG_MOD_REPLACE.
464 If the value is None or the Message contains the attr_name with this
465 value, nothing will changed."""
466 # skip unchanged attribute
467 if value is None:
468 return
469 if attr_name in old and str(value) == str(old[attr_name]):
470 return
472 # remove attribute
473 if len(value) == 0:
474 if attr_name in old:
475 el = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr_name)
476 msg.add(el)
477 return
479 # change attribute
480 el = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, attr_name)
481 msg.add(el)
483 def fullname_from_names(self, given_name=None, initials=None, surname=None,
484 old_attrs=None, fallback_default=""):
485 """Prepares new combined fullname, using the name parts.
486 Used for things like displayName or cn.
487 Use the original name values, if no new one is specified."""
488 if old_attrs is None:
489 old_attrs = {}
491 attrs = {"givenName": given_name,
492 "initials": initials,
493 "sn": surname}
495 # if the attribute is not specified, try to use the old one
496 for attr_name, attr_value in attrs.items():
497 if attr_value is None and attr_name in old_attrs:
498 attrs[attr_name] = str(old_attrs[attr_name])
500 # add '.' to initials if initials are not None and not "" and if the initials
501 # don't have already a '.' at the end
502 if attrs["initials"] and not attrs["initials"].endswith('.'):
503 attrs["initials"] += '.'
505 # remove empty values (None and '')
506 attrs_values = list(filter(None, attrs.values()))
508 # fullname is the combination of not-empty values as string, separated by ' '
509 fullname = ' '.join(attrs_values)
511 if fullname == '':
512 return fallback_default
514 return fullname
516 def newuser(self, username, password,
517 force_password_change_at_next_login_req=False,
518 useusernameascn=False, userou=None, surname=None, givenname=None,
519 initials=None, profilepath=None, scriptpath=None, homedrive=None,
520 homedirectory=None, jobtitle=None, department=None, company=None,
521 description=None, mailaddress=None, internetaddress=None,
522 telephonenumber=None, physicaldeliveryoffice=None, sd=None,
523 setpassword=True, uidnumber=None, gidnumber=None, gecos=None,
524 loginshell=None, uid=None, nisdomain=None, unixhome=None,
525 smartcard_required=False):
526 """Adds a new user with additional parameters
528 :param username: Name of the new user
529 :param password: Password for the new user
530 :param force_password_change_at_next_login_req: Force password change
531 :param useusernameascn: Use username as cn rather that firstname +
532 initials + lastname
533 :param userou: Object container (without domainDN postfix) for new user
534 :param surname: Surname of the new user
535 :param givenname: First name of the new user
536 :param initials: Initials of the new user
537 :param profilepath: Profile path of the new user
538 :param scriptpath: Logon script path of the new user
539 :param homedrive: Home drive of the new user
540 :param homedirectory: Home directory of the new user
541 :param jobtitle: Job title of the new user
542 :param department: Department of the new user
543 :param company: Company of the new user
544 :param description: of the new user
545 :param mailaddress: Email address of the new user
546 :param internetaddress: Home page of the new user
547 :param telephonenumber: Phone number of the new user
548 :param physicaldeliveryoffice: Office location of the new user
549 :param sd: security descriptor of the object
550 :param setpassword: optionally disable password reset
551 :param uidnumber: RFC2307 Unix numeric UID of the new user
552 :param gidnumber: RFC2307 Unix primary GID of the new user
553 :param gecos: RFC2307 Unix GECOS field of the new user
554 :param loginshell: RFC2307 Unix login shell of the new user
555 :param uid: RFC2307 Unix username of the new user
556 :param nisdomain: RFC2307 Unix NIS domain of the new user
557 :param unixhome: RFC2307 Unix home directory of the new user
558 :param smartcard_required: set the UF_SMARTCARD_REQUIRED bit of the new user
561 displayname = self.fullname_from_names(given_name=givenname,
562 initials=initials,
563 surname=surname)
564 cn = username
565 if useusernameascn is None and displayname != "":
566 cn = displayname
568 if userou:
569 user_dn = "CN=%s,%s,%s" % (cn, userou, self.domain_dn())
570 else:
571 user_dn = "CN=%s,%s" % (cn, self.get_wellknown_dn(
572 self.get_default_basedn(),
573 dsdb.DS_GUID_USERS_CONTAINER))
575 dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "")
576 user_principal_name = "%s@%s" % (username, dnsdomain)
577 # The new user record. Note the reliance on the SAMLDB module which
578 # fills in the default information
579 ldbmessage = {"dn": user_dn,
580 "sAMAccountName": username,
581 "userPrincipalName": user_principal_name,
582 "objectClass": "user"}
584 if smartcard_required:
585 ldbmessage["userAccountControl"] = str(dsdb.UF_NORMAL_ACCOUNT |
586 dsdb.UF_SMARTCARD_REQUIRED)
587 setpassword = False
589 if surname is not None:
590 ldbmessage["sn"] = surname
592 if givenname is not None:
593 ldbmessage["givenName"] = givenname
595 if displayname != "":
596 ldbmessage["displayName"] = displayname
597 ldbmessage["name"] = displayname
599 if initials is not None:
600 ldbmessage["initials"] = '%s.' % initials
602 if profilepath is not None:
603 ldbmessage["profilePath"] = profilepath
605 if scriptpath is not None:
606 ldbmessage["scriptPath"] = scriptpath
608 if homedrive is not None:
609 ldbmessage["homeDrive"] = homedrive
611 if homedirectory is not None:
612 ldbmessage["homeDirectory"] = homedirectory
614 if jobtitle is not None:
615 ldbmessage["title"] = jobtitle
617 if department is not None:
618 ldbmessage["department"] = department
620 if company is not None:
621 ldbmessage["company"] = company
623 if description is not None:
624 ldbmessage["description"] = description
626 if mailaddress is not None:
627 ldbmessage["mail"] = mailaddress
629 if internetaddress is not None:
630 ldbmessage["wWWHomePage"] = internetaddress
632 if telephonenumber is not None:
633 ldbmessage["telephoneNumber"] = telephonenumber
635 if physicaldeliveryoffice is not None:
636 ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
638 if sd is not None:
639 ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
641 ldbmessage2 = None
642 if any(map(lambda b: b is not None, (uid, uidnumber, gidnumber, gecos,
643 loginshell, nisdomain, unixhome))):
644 ldbmessage2 = ldb.Message()
645 ldbmessage2.dn = ldb.Dn(self, user_dn)
646 if uid is not None:
647 ldbmessage2["uid"] = ldb.MessageElement(str(uid), ldb.FLAG_MOD_REPLACE, 'uid')
648 if uidnumber is not None:
649 ldbmessage2["uidNumber"] = ldb.MessageElement(str(uidnumber), ldb.FLAG_MOD_REPLACE, 'uidNumber')
650 if gidnumber is not None:
651 ldbmessage2["gidNumber"] = ldb.MessageElement(str(gidnumber), ldb.FLAG_MOD_REPLACE, 'gidNumber')
652 if gecos is not None:
653 ldbmessage2["gecos"] = ldb.MessageElement(str(gecos), ldb.FLAG_MOD_REPLACE, 'gecos')
654 if loginshell is not None:
655 ldbmessage2["loginShell"] = ldb.MessageElement(str(loginshell), ldb.FLAG_MOD_REPLACE, 'loginShell')
656 if unixhome is not None:
657 ldbmessage2["unixHomeDirectory"] = ldb.MessageElement(
658 str(unixhome), ldb.FLAG_MOD_REPLACE, 'unixHomeDirectory')
659 if nisdomain is not None:
660 ldbmessage2["msSFU30NisDomain"] = ldb.MessageElement(
661 str(nisdomain), ldb.FLAG_MOD_REPLACE, 'msSFU30NisDomain')
662 ldbmessage2["msSFU30Name"] = ldb.MessageElement(
663 str(username), ldb.FLAG_MOD_REPLACE, 'msSFU30Name')
664 ldbmessage2["unixUserPassword"] = ldb.MessageElement(
665 'ABCD!efgh12345$67890', ldb.FLAG_MOD_REPLACE,
666 'unixUserPassword')
668 self.transaction_start()
669 try:
670 self.add(ldbmessage)
672 with self._CleanUpOnError(self, user_dn):
673 if ldbmessage2:
674 self.modify(ldbmessage2)
676 # Sets the password for it
677 if setpassword:
678 self.setpassword(("(distinguishedName=%s)" %
679 ldb.binary_encode(user_dn)),
680 password,
681 force_password_change_at_next_login_req)
682 except:
683 self.transaction_cancel()
684 raise
685 else:
686 self.transaction_commit()
688 def newcontact(self,
689 fullcontactname=None,
690 ou=None,
691 surname=None,
692 givenname=None,
693 initials=None,
694 displayname=None,
695 jobtitle=None,
696 department=None,
697 company=None,
698 description=None,
699 mailaddress=None,
700 internetaddress=None,
701 telephonenumber=None,
702 mobilenumber=None,
703 physicaldeliveryoffice=None):
704 """Adds a new contact with additional parameters
706 :param fullcontactname: Optional full name of the new contact
707 :param ou: Object container for new contact
708 :param surname: Surname of the new contact
709 :param givenname: First name of the new contact
710 :param initials: Initials of the new contact
711 :param displayname: displayName of the new contact
712 :param jobtitle: Job title of the new contact
713 :param department: Department of the new contact
714 :param company: Company of the new contact
715 :param description: Description of the new contact
716 :param mailaddress: Email address of the new contact
717 :param internetaddress: Home page of the new contact
718 :param telephonenumber: Phone number of the new contact
719 :param mobilenumber: Primary mobile number of the new contact
720 :param physicaldeliveryoffice: Office location of the new contact
723 # Prepare the contact name like the RSAT, using the name parts.
724 cn = self.fullname_from_names(given_name=givenname,
725 initials=initials,
726 surname=surname)
728 # Use the specified fullcontactname instead of the previously prepared
729 # contact name, if it is specified.
730 # This is similar to the "Full name" value of the RSAT.
731 if fullcontactname is not None:
732 cn = fullcontactname
734 if fullcontactname is None and cn == "":
735 raise Exception('No name for contact specified')
737 contactcontainer_dn = self.domain_dn()
738 if ou:
739 contactcontainer_dn = self.normalize_dn_in_domain(ou)
741 contact_dn = "CN=%s,%s" % (cn, contactcontainer_dn)
743 ldbmessage = {"dn": contact_dn,
744 "objectClass": "contact",
747 if surname is not None:
748 ldbmessage["sn"] = surname
750 if givenname is not None:
751 ldbmessage["givenName"] = givenname
753 if displayname is not None:
754 ldbmessage["displayName"] = displayname
756 if initials is not None:
757 ldbmessage["initials"] = '%s.' % initials
759 if jobtitle is not None:
760 ldbmessage["title"] = jobtitle
762 if department is not None:
763 ldbmessage["department"] = department
765 if company is not None:
766 ldbmessage["company"] = company
768 if description is not None:
769 ldbmessage["description"] = description
771 if mailaddress is not None:
772 ldbmessage["mail"] = mailaddress
774 if internetaddress is not None:
775 ldbmessage["wWWHomePage"] = internetaddress
777 if telephonenumber is not None:
778 ldbmessage["telephoneNumber"] = telephonenumber
780 if mobilenumber is not None:
781 ldbmessage["mobile"] = mobilenumber
783 if physicaldeliveryoffice is not None:
784 ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
786 self.add(ldbmessage)
788 return cn
790 def newcomputer(self, computername, computerou=None, description=None,
791 prepare_oldjoin=False, ip_address_list=None,
792 service_principal_name_list=None):
793 """Adds a new user with additional parameters
795 :param computername: Name of the new computer
796 :param computerou: Object container for new computer
797 :param description: Description of the new computer
798 :param prepare_oldjoin: Preset computer password for oldjoin mechanism
799 :param ip_address_list: ip address list for DNS A or AAAA record
800 :param service_principal_name_list: string list of servicePincipalName
803 cn = re.sub(r"\$$", "", computername)
804 if cn.count('$'):
805 raise Exception('Illegal computername "%s"' % computername)
806 samaccountname = "%s$" % cn
808 computercontainer_dn = self.get_wellknown_dn(self.get_default_basedn(),
809 dsdb.DS_GUID_COMPUTERS_CONTAINER)
810 if computerou:
811 computercontainer_dn = self.normalize_dn_in_domain(computerou)
813 computer_dn = "CN=%s,%s" % (cn, computercontainer_dn)
815 ldbmessage = {"dn": computer_dn,
816 "sAMAccountName": samaccountname,
817 "objectClass": "computer",
820 if description is not None:
821 ldbmessage["description"] = description
823 if service_principal_name_list:
824 ldbmessage["servicePrincipalName"] = service_principal_name_list
826 accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
827 dsdb.UF_ACCOUNTDISABLE)
828 if prepare_oldjoin:
829 accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
830 ldbmessage["userAccountControl"] = accountcontrol
832 if ip_address_list:
833 ldbmessage['dNSHostName'] = '{}.{}'.format(
834 cn, self.domain_dns_name())
836 self.transaction_start()
837 try:
838 self.add(ldbmessage)
840 if prepare_oldjoin:
841 password = cn.lower()
842 with self._CleanUpOnError(self, computer_dn):
843 self.setpassword(("(distinguishedName=%s)" %
844 ldb.binary_encode(computer_dn)),
845 password, False)
846 except:
847 self.transaction_cancel()
848 raise
849 else:
850 self.transaction_commit()
852 def deleteuser(self, username):
853 """Deletes a user
855 :param username: Name of the target user
858 filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn())
859 self.transaction_start()
860 try:
861 target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
862 expression=filter, attrs=[])
863 if len(target) == 0:
864 raise Exception('Unable to find user "%s"' % username)
865 assert(len(target) == 1)
866 self.delete(target[0].dn)
867 except:
868 self.transaction_cancel()
869 raise
870 else:
871 self.transaction_commit()
873 def setpassword(self, search_filter, password,
874 force_change_at_next_login=False, username=None):
875 """Sets the password for a user
877 :param search_filter: LDAP filter to find the user (eg
878 samccountname=name)
879 :param password: Password for the user
880 :param force_change_at_next_login: Force password change
882 self.transaction_start()
883 try:
884 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
885 expression=search_filter, attrs=[])
886 if len(res) == 0:
887 raise Exception('Unable to find user "%s"' % (username or search_filter))
888 if len(res) > 1:
889 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter))
890 user_dn = res[0].dn
891 if not isinstance(password, str):
892 pw = password.decode('utf-8')
893 else:
894 pw = password
895 pw = ('"' + pw + '"').encode('utf-16-le')
896 setpw = """
897 dn: %s
898 changetype: modify
899 replace: unicodePwd
900 unicodePwd:: %s
901 """ % (user_dn, base64.b64encode(pw).decode('utf-8'))
903 self.modify_ldif(setpw)
905 if force_change_at_next_login:
906 self.force_password_change_at_next_login(
907 "(distinguishedName=" + str(user_dn) + ")")
909 # modify the userAccountControl to remove the disabled bit
910 self.enable_account(search_filter)
911 except:
912 self.transaction_cancel()
913 raise
914 else:
915 self.transaction_commit()
917 def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False):
918 """Sets the account expiry for a user
920 :param search_filter: LDAP filter to find the user (eg
921 samaccountname=name)
922 :param expiry_seconds: expiry time from now in seconds
923 :param no_expiry_req: if set, then don't expire password
925 self.transaction_start()
926 try:
927 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
928 expression=search_filter,
929 attrs=["userAccountControl", "accountExpires"])
930 if len(res) == 0:
931 raise Exception('Unable to find user "%s"' % search_filter)
932 assert(len(res) == 1)
933 user_dn = res[0].dn
935 userAccountControl = int(res[0]["userAccountControl"][0])
936 if no_expiry_req:
937 userAccountControl = userAccountControl | 0x10000
938 accountExpires = 0
939 else:
940 userAccountControl = userAccountControl & ~0x10000
941 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
943 setexp = """
944 dn: %s
945 changetype: modify
946 replace: userAccountControl
947 userAccountControl: %u
948 replace: accountExpires
949 accountExpires: %u
950 """ % (user_dn, userAccountControl, accountExpires)
952 self.modify_ldif(setexp)
953 except:
954 self.transaction_cancel()
955 raise
956 else:
957 self.transaction_commit()
959 def set_domain_sid(self, sid):
960 """Change the domain SID used by this LDB.
962 :param sid: The new domain sid to use.
964 dsdb._samdb_set_domain_sid(self, sid)
966 def get_domain_sid(self):
967 """Read the domain SID used by this LDB. """
968 return dsdb._samdb_get_domain_sid(self)
970 domain_sid = property(get_domain_sid, set_domain_sid,
971 doc="SID for the domain")
973 def get_connecting_user_sid(self):
974 """Returns the SID of the connected user."""
975 msg = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])[0]
976 return str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
978 connecting_user_sid = property(get_connecting_user_sid,
979 doc="SID of the connecting user")
981 def set_invocation_id(self, invocation_id):
982 """Set the invocation id for this SamDB handle.
984 :param invocation_id: GUID of the invocation id.
986 dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
988 def get_invocation_id(self):
989 """Get the invocation_id id"""
990 return dsdb._samdb_ntds_invocation_id(self)
992 invocation_id = property(get_invocation_id, set_invocation_id,
993 doc="Invocation ID GUID")
995 def get_oid_from_attid(self, attid):
996 return dsdb._dsdb_get_oid_from_attid(self, attid)
998 def get_attid_from_lDAPDisplayName(self, ldap_display_name,
999 is_schema_nc=False):
1000 """return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI"""
1001 return dsdb._dsdb_get_attid_from_lDAPDisplayName(self,
1002 ldap_display_name, is_schema_nc)
1004 def get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name):
1005 """return the syntax OID for a LDAP attribute as a string"""
1006 return dsdb._dsdb_get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name)
1008 def get_systemFlags_from_lDAPDisplayName(self, ldap_display_name):
1009 """return the systemFlags for a LDAP attribute as a integer"""
1010 return dsdb._dsdb_get_systemFlags_from_lDAPDisplayName(self, ldap_display_name)
1012 def get_linkId_from_lDAPDisplayName(self, ldap_display_name):
1013 """return the linkID for a LDAP attribute as a integer"""
1014 return dsdb._dsdb_get_linkId_from_lDAPDisplayName(self, ldap_display_name)
1016 def get_lDAPDisplayName_by_attid(self, attid):
1017 """return the lDAPDisplayName from an integer DRS attribute ID"""
1018 return dsdb._dsdb_get_lDAPDisplayName_by_attid(self, attid)
1020 def get_backlink_from_lDAPDisplayName(self, ldap_display_name):
1021 """return the attribute name of the corresponding backlink from the name
1022 of a forward link attribute. If there is no backlink return None"""
1023 return dsdb._dsdb_get_backlink_from_lDAPDisplayName(self, ldap_display_name)
1025 def set_ntds_settings_dn(self, ntds_settings_dn):
1026 """Set the NTDS Settings DN, as would be returned on the dsServiceName
1027 rootDSE attribute.
1029 This allows the DN to be set before the database fully exists
1031 :param ntds_settings_dn: The new DN to use
1033 dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
1035 def get_ntds_GUID(self):
1036 """Get the NTDS objectGUID"""
1037 return dsdb._samdb_ntds_objectGUID(self)
1039 def get_timestr(self):
1040 """Get the current time as generalized time string"""
1041 res = self.search(base="",
1042 scope=ldb.SCOPE_BASE,
1043 attrs=["currentTime"])
1044 return str(res[0]["currentTime"][0])
1046 def get_time(self):
1047 """Get the current time as UNIX time"""
1048 return ldb.string_to_time(self.get_timestr())
1050 def get_nttime(self):
1051 """Get the current time as NT time"""
1052 return samba.unix2nttime(self.get_time())
1054 def server_site_name(self):
1055 """Get the server site name"""
1056 return dsdb._samdb_server_site_name(self)
1058 def host_dns_name(self):
1059 """return the DNS name of this host"""
1060 res = self.search(base='', scope=ldb.SCOPE_BASE, attrs=['dNSHostName'])
1061 return str(res[0]['dNSHostName'][0])
1063 def domain_dns_name(self):
1064 """return the DNS name of the domain root"""
1065 domain_dn = self.get_default_basedn()
1066 return domain_dn.canonical_str().split('/')[0]
1068 def domain_netbios_name(self):
1069 """return the NetBIOS name of the domain root"""
1070 domain_dn = self.get_default_basedn()
1071 dns_name = self.domain_dns_name()
1072 filter = "(&(objectClass=crossRef)(nETBIOSName=*)(ncName=%s)(dnsroot=%s))" % (domain_dn, dns_name)
1073 partitions_dn = self.get_partitions_dn()
1074 res = self.search(partitions_dn,
1075 scope=ldb.SCOPE_ONELEVEL,
1076 expression=filter)
1077 try:
1078 netbios_domain = res[0]["nETBIOSName"][0].decode()
1079 except IndexError:
1080 return None
1081 return netbios_domain
1083 def forest_dns_name(self):
1084 """return the DNS name of the forest root"""
1085 forest_dn = self.get_root_basedn()
1086 return forest_dn.canonical_str().split('/')[0]
1088 def load_partition_usn(self, base_dn):
1089 return dsdb._dsdb_load_partition_usn(self, base_dn)
1091 def set_schema(self, schema, write_indices_and_attributes=True):
1092 self.set_schema_from_ldb(schema.ldb, write_indices_and_attributes=write_indices_and_attributes)
1094 def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True):
1095 dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes)
1097 def set_schema_update_now(self):
1098 ldif = """
1100 changetype: modify
1101 add: schemaUpdateNow
1102 schemaUpdateNow: 1
1104 self.modify_ldif(ldif)
1106 def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
1107 """convert a list of attribute values to a DRSUAPI DsReplicaAttribute"""
1108 return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
1110 def dsdb_normalise_attributes(self, ldb, ldap_display_name, ldif_elements):
1111 """normalise a list of attribute values"""
1112 return dsdb._dsdb_normalise_attributes(ldb, ldap_display_name, ldif_elements)
1114 def get_attribute_from_attid(self, attid):
1115 """ Get from an attid the associated attribute
1117 :param attid: The attribute id for searched attribute
1118 :return: The name of the attribute associated with this id
1120 if len(self.hash_oid_name.keys()) == 0:
1121 self._populate_oid_attid()
1122 if self.get_oid_from_attid(attid) in self.hash_oid_name:
1123 return self.hash_oid_name[self.get_oid_from_attid(attid)]
1124 else:
1125 return None
1127 def _populate_oid_attid(self):
1128 """Populate the hash hash_oid_name.
1130 This hash contains the oid of the attribute as a key and
1131 its display name as a value
1133 self.hash_oid_name = {}
1134 res = self.search(expression="objectClass=attributeSchema",
1135 controls=["search_options:1:2"],
1136 attrs=["attributeID",
1137 "lDAPDisplayName"])
1138 if len(res) > 0:
1139 for e in res:
1140 strDisplay = str(e.get("lDAPDisplayName"))
1141 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
1143 def get_attribute_replmetadata_version(self, dn, att):
1144 """Get the version field trom the replPropertyMetaData for
1145 the given field
1147 :param dn: The on which we want to get the version
1148 :param att: The name of the attribute
1149 :return: The value of the version field in the replPropertyMetaData
1150 for the given attribute. None if the attribute is not replicated
1153 res = self.search(expression="distinguishedName=%s" % dn,
1154 scope=ldb.SCOPE_SUBTREE,
1155 controls=["search_options:1:2"],
1156 attrs=["replPropertyMetaData"])
1157 if len(res) == 0:
1158 return None
1160 repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
1161 res[0]["replPropertyMetaData"][0])
1162 ctr = repl.ctr
1163 if len(self.hash_oid_name.keys()) == 0:
1164 self._populate_oid_attid()
1165 for o in ctr.array:
1166 # Search for Description
1167 att_oid = self.get_oid_from_attid(o.attid)
1168 if att_oid in self.hash_oid_name and\
1169 att.lower() == self.hash_oid_name[att_oid].lower():
1170 return o.version
1171 return None
1173 def set_attribute_replmetadata_version(self, dn, att, value,
1174 addifnotexist=False):
1175 res = self.search(expression="distinguishedName=%s" % dn,
1176 scope=ldb.SCOPE_SUBTREE,
1177 controls=["search_options:1:2"],
1178 attrs=["replPropertyMetaData"])
1179 if len(res) == 0:
1180 return None
1182 repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
1183 res[0]["replPropertyMetaData"][0])
1184 ctr = repl.ctr
1185 now = samba.unix2nttime(int(time.time()))
1186 found = False
1187 if len(self.hash_oid_name.keys()) == 0:
1188 self._populate_oid_attid()
1189 for o in ctr.array:
1190 # Search for Description
1191 att_oid = self.get_oid_from_attid(o.attid)
1192 if att_oid in self.hash_oid_name and\
1193 att.lower() == self.hash_oid_name[att_oid].lower():
1194 found = True
1195 seq = self.sequence_number(ldb.SEQ_NEXT)
1196 o.version = value
1197 o.originating_change_time = now
1198 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
1199 o.originating_usn = seq
1200 o.local_usn = seq
1202 if not found and addifnotexist and len(ctr.array) > 0:
1203 o2 = drsblobs.replPropertyMetaData1()
1204 o2.attid = 589914
1205 att_oid = self.get_oid_from_attid(o2.attid)
1206 seq = self.sequence_number(ldb.SEQ_NEXT)
1207 o2.version = value
1208 o2.originating_change_time = now
1209 o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
1210 o2.originating_usn = seq
1211 o2.local_usn = seq
1212 found = True
1213 tab = ctr.array
1214 tab.append(o2)
1215 ctr.count = ctr.count + 1
1216 ctr.array = tab
1218 if found:
1219 replBlob = ndr_pack(repl)
1220 msg = ldb.Message()
1221 msg.dn = res[0].dn
1222 msg["replPropertyMetaData"] = \
1223 ldb.MessageElement(replBlob,
1224 ldb.FLAG_MOD_REPLACE,
1225 "replPropertyMetaData")
1226 self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
1228 def write_prefixes_from_schema(self):
1229 dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
1231 def get_partitions_dn(self):
1232 return dsdb._dsdb_get_partitions_dn(self)
1234 def get_nc_root(self, dn):
1235 return dsdb._dsdb_get_nc_root(self, dn)
1237 def get_wellknown_dn(self, nc_root, wkguid):
1238 return dsdb._dsdb_get_wellknown_dn(self, nc_root, wkguid)
1240 def set_minPwdAge(self, value):
1241 if not isinstance(value, bytes):
1242 value = str(value).encode('utf8')
1243 m = ldb.Message()
1244 m.dn = ldb.Dn(self, self.domain_dn())
1245 m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
1246 self.modify(m)
1248 def get_minPwdAge(self):
1249 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
1250 if len(res) == 0:
1251 return None
1252 elif "minPwdAge" not in res[0]:
1253 return None
1254 else:
1255 return int(res[0]["minPwdAge"][0])
1257 def set_maxPwdAge(self, value):
1258 if not isinstance(value, bytes):
1259 value = str(value).encode('utf8')
1260 m = ldb.Message()
1261 m.dn = ldb.Dn(self, self.domain_dn())
1262 m["maxPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "maxPwdAge")
1263 self.modify(m)
1265 def get_maxPwdAge(self):
1266 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["maxPwdAge"])
1267 if len(res) == 0:
1268 return None
1269 elif "maxPwdAge" not in res[0]:
1270 return None
1271 else:
1272 return int(res[0]["maxPwdAge"][0])
1274 def set_minPwdLength(self, value):
1275 if not isinstance(value, bytes):
1276 value = str(value).encode('utf8')
1277 m = ldb.Message()
1278 m.dn = ldb.Dn(self, self.domain_dn())
1279 m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength")
1280 self.modify(m)
1282 def get_minPwdLength(self):
1283 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"])
1284 if len(res) == 0:
1285 return None
1286 elif "minPwdLength" not in res[0]:
1287 return None
1288 else:
1289 return int(res[0]["minPwdLength"][0])
1291 def set_pwdProperties(self, value):
1292 if not isinstance(value, bytes):
1293 value = str(value).encode('utf8')
1294 m = ldb.Message()
1295 m.dn = ldb.Dn(self, self.domain_dn())
1296 m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties")
1297 self.modify(m)
1299 def get_pwdProperties(self):
1300 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"])
1301 if len(res) == 0:
1302 return None
1303 elif "pwdProperties" not in res[0]:
1304 return None
1305 else:
1306 return int(res[0]["pwdProperties"][0])
1308 def set_dsheuristics(self, dsheuristics):
1309 m = ldb.Message()
1310 m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
1311 % self.get_config_basedn().get_linearized())
1312 if dsheuristics is not None:
1313 m["dSHeuristics"] = \
1314 ldb.MessageElement(dsheuristics,
1315 ldb.FLAG_MOD_REPLACE,
1316 "dSHeuristics")
1317 else:
1318 m["dSHeuristics"] = \
1319 ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
1320 "dSHeuristics")
1321 self.modify(m)
1323 def get_dsheuristics(self):
1324 res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
1325 % self.get_config_basedn().get_linearized(),
1326 scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
1327 if len(res) == 0:
1328 dsheuristics = None
1329 elif "dSHeuristics" in res[0]:
1330 dsheuristics = res[0]["dSHeuristics"][0]
1331 else:
1332 dsheuristics = None
1334 return dsheuristics
1336 def create_ou(self, ou_dn, description=None, name=None, sd=None):
1337 """Creates an organizationalUnit object
1338 :param ou_dn: dn of the new object
1339 :param description: description attribute
1340 :param name: name attribute
1341 :param sd: security descriptor of the object, can be
1342 an SDDL string or security.descriptor type
1344 m = {"dn": ou_dn,
1345 "objectClass": "organizationalUnit"}
1347 if description:
1348 m["description"] = description
1349 if name:
1350 m["name"] = name
1352 if sd:
1353 m["nTSecurityDescriptor"] = ndr_pack(sd)
1354 self.add(m)
1356 def sequence_number(self, seq_type):
1357 """Returns the value of the sequence number according to the requested type
1358 :param seq_type: type of sequence number
1360 self.transaction_start()
1361 try:
1362 seq = super().sequence_number(seq_type)
1363 except:
1364 self.transaction_cancel()
1365 raise
1366 else:
1367 self.transaction_commit()
1368 return seq
1370 def get_dsServiceName(self):
1371 """get the NTDS DN from the rootDSE"""
1372 res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
1373 return str(res[0]["dsServiceName"][0])
1375 def get_serverName(self):
1376 """get the server DN from the rootDSE"""
1377 res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["serverName"])
1378 return str(res[0]["serverName"][0])
1380 def dns_lookup(self, dns_name, dns_partition=None):
1381 """Do a DNS lookup in the database, returns the NDR database structures"""
1382 if dns_partition is None:
1383 return dsdb_dns.lookup(self, dns_name)
1384 else:
1385 return dsdb_dns.lookup(self, dns_name,
1386 dns_partition=dns_partition)
1388 def dns_extract(self, el):
1389 """Return the NDR database structures from a dnsRecord element"""
1390 return dsdb_dns.extract(self, el)
1392 def dns_replace(self, dns_name, new_records):
1393 """Do a DNS modification on the database, sets the NDR database
1394 structures on a DNS name
1396 return dsdb_dns.replace(self, dns_name, new_records)
1398 def dns_replace_by_dn(self, dn, new_records):
1399 """Do a DNS modification on the database, sets the NDR database
1400 structures on a LDB DN
1402 This routine is important because if the last record on the DN
1403 is removed, this routine will put a tombstone in the record.
1405 return dsdb_dns.replace_by_dn(self, dn, new_records)
1407 def garbage_collect_tombstones(self, dn, current_time,
1408 tombstone_lifetime=None):
1409 """garbage_collect_tombstones(lp, samdb, [dn], current_time, tombstone_lifetime)
1410 -> (num_objects_expunged, num_links_expunged)"""
1412 if not is_ad_dc_built():
1413 raise SamDBError('Cannot garbage collect tombstones: '
1414 'AD DC was not built')
1416 if tombstone_lifetime is None:
1417 return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1418 current_time)
1419 else:
1420 return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1421 current_time,
1422 tombstone_lifetime)
1424 def create_own_rid_set(self):
1425 """create a RID set for this DSA"""
1426 return dsdb._dsdb_create_own_rid_set(self)
1428 def allocate_rid(self):
1429 """return a new RID from the RID Pool on this DSA"""
1430 return dsdb._dsdb_allocate_rid(self)
1432 def next_free_rid(self):
1433 """return the next free RID from the RID Pool on this DSA.
1435 :note: This function is not intended for general use, and care must be
1436 taken if it is used to generate objectSIDs. The returned RID is not
1437 formally reserved for use, creating the possibility of duplicate
1438 objectSIDs.
1440 rid, _ = self.free_rid_bounds()
1441 return rid
1443 def free_rid_bounds(self):
1444 """return the low and high bounds (inclusive) of RIDs that are
1445 available for use in this DSA's current RID pool.
1447 :note: This function is not intended for general use, and care must be
1448 taken if it is used to generate objectSIDs. The returned range of
1449 RIDs is not formally reserved for use, creating the possibility of
1450 duplicate objectSIDs.
1452 # Get DN of this server's RID Set
1453 server_name_dn = ldb.Dn(self, self.get_serverName())
1454 res = self.search(base=server_name_dn,
1455 scope=ldb.SCOPE_BASE,
1456 attrs=["serverReference"])
1457 try:
1458 server_ref = res[0]["serverReference"]
1459 except KeyError:
1460 raise ldb.LdbError(
1461 ldb.ERR_NO_SUCH_ATTRIBUTE,
1462 "No RID Set DN - "
1463 "Cannot find attribute serverReference of %s "
1464 "to calculate reference dn" % server_name_dn) from None
1465 server_ref_dn = ldb.Dn(self, server_ref[0].decode("utf-8"))
1467 res = self.search(base=server_ref_dn,
1468 scope=ldb.SCOPE_BASE,
1469 attrs=["rIDSetReferences"])
1470 try:
1471 rid_set_refs = res[0]["rIDSetReferences"]
1472 except KeyError:
1473 raise ldb.LdbError(
1474 ldb.ERR_NO_SUCH_ATTRIBUTE,
1475 "No RID Set DN - "
1476 "Cannot find attribute rIDSetReferences of %s "
1477 "to calculate reference dn" % server_ref_dn) from None
1478 rid_set_dn = ldb.Dn(self, rid_set_refs[0].decode("utf-8"))
1480 # Get the alloc pools and next RID of this RID Set
1481 res = self.search(base=rid_set_dn,
1482 scope=ldb.SCOPE_BASE,
1483 attrs=["rIDAllocationPool",
1484 "rIDPreviousAllocationPool",
1485 "rIDNextRID"])
1487 uint32_max = 2**32 - 1
1488 uint64_max = 2**64 - 1
1490 try:
1491 alloc_pool = int(res[0]["rIDAllocationPool"][0])
1492 except KeyError:
1493 alloc_pool = uint64_max
1494 if alloc_pool == uint64_max:
1495 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1496 "Bad RID Set %s" % rid_set_dn)
1498 try:
1499 prev_pool = int(res[0]["rIDPreviousAllocationPool"][0])
1500 except KeyError:
1501 prev_pool = uint64_max
1502 try:
1503 next_rid = int(res[0]["rIDNextRID"][0])
1504 except KeyError:
1505 next_rid = uint32_max
1507 # If we never used a pool, set up our first pool
1508 if prev_pool == uint64_max or next_rid == uint32_max:
1509 prev_pool = alloc_pool
1510 next_rid = prev_pool & uint32_max
1511 else:
1512 next_rid += 1
1514 # Now check if our current pool is still usable
1515 prev_pool_lo = prev_pool & uint32_max
1516 prev_pool_hi = prev_pool >> 32
1517 if next_rid > prev_pool_hi:
1518 # We need a new pool, check if we already have a new one
1519 # Otherwise we return an error code.
1520 if alloc_pool == prev_pool:
1521 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1522 "RID pools out of RIDs")
1524 # Now use the new pool
1525 prev_pool = alloc_pool
1526 prev_pool_lo = prev_pool & uint32_max
1527 prev_pool_hi = prev_pool >> 32
1528 next_rid = prev_pool_lo
1530 if next_rid < prev_pool_lo or next_rid > prev_pool_hi:
1531 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1532 "Bad RID chosen %d from range %d-%d" %
1533 (next_rid, prev_pool_lo, prev_pool_hi))
1535 return next_rid, prev_pool_hi
1537 def normalize_dn_in_domain(self, dn):
1538 """return a new DN expanded by adding the domain DN
1540 If the dn is already a child of the domain DN, just
1541 return it as-is.
1543 :param dn: relative dn
1545 domain_dn = ldb.Dn(self, self.domain_dn())
1547 if isinstance(dn, ldb.Dn):
1548 dn = str(dn)
1550 full_dn = ldb.Dn(self, dn)
1551 if not full_dn.is_child_of(domain_dn):
1552 full_dn.add_base(domain_dn)
1553 return full_dn
1555 def new_gkdi_root_key(self, *args, **kwargs):
1556 """ """
1557 dn = dsdb._dsdb_create_gkdi_root_key(self, *args, **kwargs)
1558 return dn
1561 class dsdb_Dn(object):
1562 """a class for binary DN"""
1564 def __init__(self, samdb, dnstring, syntax_oid=None):
1565 """create a dsdb_Dn"""
1566 if syntax_oid is None:
1567 # auto-detect based on string
1568 if dnstring.startswith("B:"):
1569 syntax_oid = dsdb.DSDB_SYNTAX_BINARY_DN
1570 elif dnstring.startswith("S:"):
1571 syntax_oid = dsdb.DSDB_SYNTAX_STRING_DN
1572 else:
1573 syntax_oid = dsdb.DSDB_SYNTAX_OR_NAME
1574 if syntax_oid in [dsdb.DSDB_SYNTAX_BINARY_DN, dsdb.DSDB_SYNTAX_STRING_DN]:
1575 # it is a binary DN
1576 colons = dnstring.split(':')
1577 if len(colons) < 4:
1578 raise RuntimeError("Invalid DN %s" % dnstring)
1579 prefix_len = 4 + len(colons[1]) + int(colons[1])
1580 self.prefix = dnstring[0:prefix_len]
1581 self.binary = self.prefix[3 + len(colons[1]):-1]
1582 self.dnstring = dnstring[prefix_len:]
1583 else:
1584 self.dnstring = dnstring
1585 self.prefix = ''
1586 self.binary = ''
1587 self.dn = ldb.Dn(samdb, self.dnstring)
1589 def __str__(self):
1590 return self.prefix + str(self.dn.extended_str(mode=1))
1592 def __cmp__(self, other):
1593 """ compare dsdb_Dn values similar to parsed_dn_compare()"""
1594 dn1 = self
1595 dn2 = other
1596 guid1 = dn1.dn.get_extended_component("GUID")
1597 guid2 = dn2.dn.get_extended_component("GUID")
1599 v = cmp(guid1, guid2)
1600 if v != 0:
1601 return v
1602 v = cmp(dn1.binary, dn2.binary)
1603 return v
1605 # In Python3, __cmp__ is replaced by these 6 methods
1606 def __eq__(self, other):
1607 return self.__cmp__(other) == 0
1609 def __ne__(self, other):
1610 return self.__cmp__(other) != 0
1612 def __lt__(self, other):
1613 return self.__cmp__(other) < 0
1615 def __le__(self, other):
1616 return self.__cmp__(other) <= 0
1618 def __gt__(self, other):
1619 return self.__cmp__(other) > 0
1621 def __ge__(self, other):
1622 return self.__cmp__(other) >= 0
1624 def get_binary_integer(self):
1625 """return binary part of a dsdb_Dn as an integer, or None"""
1626 if self.prefix == '':
1627 return None
1628 return int(self.binary, 16)
1630 def get_bytes(self):
1631 """return binary as a byte string"""
1632 return binascii.unhexlify(self.binary)