lookup_name: allow lookup names prefixed with DNS forest root for FreeIPA DC
[Samba.git] / python / samba / samdb.py
bloba0a7dbf1c509ada46102ded63c92c0926be6c4ac
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 import binascii
39 __docformat__ = "restructuredText"
42 def get_default_backend_store():
43 return "tdb"
45 class SamDBError(Exception):
46 pass
48 class SamDBNotFoundError(SamDBError):
49 pass
51 class SamDB(samba.Ldb):
52 """The SAM database."""
54 hash_oid_name = {}
55 hash_well_known = {}
57 def __init__(self, url=None, lp=None, modules_dir=None, session_info=None,
58 credentials=None, flags=ldb.FLG_DONT_CREATE_DB,
59 options=None, global_schema=True,
60 auto_connect=True, am_rodc=None):
61 self.lp = lp
62 if not auto_connect:
63 url = None
64 elif url is None and lp is not None:
65 url = lp.samdb_url()
67 self.url = url
69 super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir,
70 session_info=session_info, credentials=credentials, flags=flags,
71 options=options)
73 if global_schema:
74 dsdb._dsdb_set_global_schema(self)
76 if am_rodc is not None:
77 dsdb._dsdb_set_am_rodc(self, am_rodc)
79 def connect(self, url=None, flags=0, options=None):
80 '''connect to the database'''
81 if self.lp is not None and not os.path.exists(url):
82 url = self.lp.private_path(url)
83 self.url = url
85 super(SamDB, self).connect(url=url, flags=flags,
86 options=options)
88 def am_rodc(self):
89 '''return True if we are an RODC'''
90 return dsdb._am_rodc(self)
92 def am_pdc(self):
93 '''return True if we are an PDC emulator'''
94 return dsdb._am_pdc(self)
96 def domain_dn(self):
97 '''return the domain DN'''
98 return str(self.get_default_basedn())
100 def schema_dn(self):
101 '''return the schema partition dn'''
102 return str(self.get_schema_basedn())
104 def disable_account(self, search_filter):
105 """Disables an account
107 :param search_filter: LDAP filter to find the user (eg
108 samccountname=name)
111 flags = samba.dsdb.UF_ACCOUNTDISABLE
112 self.toggle_userAccountFlags(search_filter, flags, on=True)
114 def enable_account(self, search_filter):
115 """Enables an account
117 :param search_filter: LDAP filter to find the user (eg
118 samccountname=name)
121 flags = samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_PASSWD_NOTREQD
122 self.toggle_userAccountFlags(search_filter, flags, on=False)
124 def toggle_userAccountFlags(self, search_filter, flags, flags_str=None,
125 on=True, strict=False):
126 """Toggle_userAccountFlags
128 :param search_filter: LDAP filter to find the user (eg
129 samccountname=name)
130 :param flags: samba.dsdb.UF_* flags
131 :param on: on=True (default) => set, on=False => unset
132 :param strict: strict=False (default) ignore if no action is needed
133 strict=True raises an Exception if...
135 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
136 expression=search_filter, attrs=["userAccountControl"])
137 if len(res) == 0:
138 raise Exception("Unable to find account where '%s'" % search_filter)
139 assert(len(res) == 1)
140 account_dn = res[0].dn
142 old_uac = int(res[0]["userAccountControl"][0])
143 if on:
144 if strict and (old_uac & flags):
145 error = "Account flag(s) '%s' already set" % flags_str
146 raise Exception(error)
148 new_uac = old_uac | flags
149 else:
150 if strict and not (old_uac & flags):
151 error = "Account flag(s) '%s' already unset" % flags_str
152 raise Exception(error)
154 new_uac = old_uac & ~flags
156 if old_uac == new_uac:
157 return
159 mod = """
160 dn: %s
161 changetype: modify
162 delete: userAccountControl
163 userAccountControl: %u
164 add: userAccountControl
165 userAccountControl: %u
166 """ % (account_dn, old_uac, new_uac)
167 self.modify_ldif(mod)
169 def force_password_change_at_next_login(self, search_filter):
170 """Forces a password change at next login
172 :param search_filter: LDAP filter to find the user (eg
173 samccountname=name)
175 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
176 expression=search_filter, attrs=[])
177 if len(res) == 0:
178 raise Exception('Unable to find user "%s"' % search_filter)
179 assert(len(res) == 1)
180 user_dn = res[0].dn
182 mod = """
183 dn: %s
184 changetype: modify
185 replace: pwdLastSet
186 pwdLastSet: 0
187 """ % (user_dn)
188 self.modify_ldif(mod)
190 def unlock_account(self, search_filter):
191 """Unlock a user account by resetting lockoutTime to 0.
192 This does also reset the badPwdCount to 0.
194 :param search_filter: LDAP filter to find the user (e.g.
195 sAMAccountName=username)
197 res = self.search(base=self.domain_dn(),
198 scope=ldb.SCOPE_SUBTREE,
199 expression=search_filter,
200 attrs=[])
201 if len(res) == 0:
202 raise SamDBNotFoundError('Unable to find user "%s"' % search_filter)
203 if len(res) != 1:
204 raise SamDBError('User "%s" is not unique' % search_filter)
205 user_dn = res[0].dn
207 mod = """
208 dn: %s
209 changetype: modify
210 replace: lockoutTime
211 lockoutTime: 0
212 """ % (user_dn)
213 self.modify_ldif(mod)
215 def newgroup(self, groupname, groupou=None, grouptype=None,
216 description=None, mailaddress=None, notes=None, sd=None,
217 gidnumber=None, nisdomain=None):
218 """Adds a new group with additional parameters
220 :param groupname: Name of the new group
221 :param grouptype: Type of the new group
222 :param description: Description of the new group
223 :param mailaddress: Email address of the new group
224 :param notes: Notes of the new group
225 :param gidnumber: GID Number of the new group
226 :param nisdomain: NIS Domain Name of the new group
227 :param sd: security descriptor of the object
230 group_dn = "CN=%s,%s,%s" % (groupname, (groupou or "CN=Users"), self.domain_dn())
232 # The new user record. Note the reliance on the SAMLDB module which
233 # fills in the default information
234 ldbmessage = {"dn": group_dn,
235 "sAMAccountName": groupname,
236 "objectClass": "group"}
238 if grouptype is not None:
239 ldbmessage["groupType"] = normalise_int32(grouptype)
241 if description is not None:
242 ldbmessage["description"] = description
244 if mailaddress is not None:
245 ldbmessage["mail"] = mailaddress
247 if notes is not None:
248 ldbmessage["info"] = notes
250 if gidnumber is not None:
251 ldbmessage["gidNumber"] = normalise_int32(gidnumber)
253 if nisdomain is not None:
254 ldbmessage["msSFU30Name"] = groupname
255 ldbmessage["msSFU30NisDomain"] = nisdomain
257 if sd is not None:
258 ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
260 self.add(ldbmessage)
262 def deletegroup(self, groupname):
263 """Deletes a group
265 :param groupname: Name of the target group
268 groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
269 self.transaction_start()
270 try:
271 targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
272 expression=groupfilter, attrs=[])
273 if len(targetgroup) == 0:
274 raise Exception('Unable to find group "%s"' % groupname)
275 assert(len(targetgroup) == 1)
276 self.delete(targetgroup[0].dn)
277 except:
278 self.transaction_cancel()
279 raise
280 else:
281 self.transaction_commit()
283 def group_member_filter(self, member, member_types):
284 filter = ""
286 all_member_types = [ 'user',
287 'group',
288 'computer',
289 'serviceaccount',
290 'contact',
293 if 'all' in member_types:
294 member_types = all_member_types
296 for member_type in member_types:
297 if member_type not in all_member_types:
298 raise Exception('Invalid group member type "%s". '
299 'Valid types are %s and all.' %
300 (member_type, ", ".join(all_member_types)))
302 if 'user' in member_types:
303 filter += ('(&(sAMAccountName=%s)(samAccountType=%d))' %
304 (ldb.binary_encode(member), dsdb.ATYPE_NORMAL_ACCOUNT))
305 if 'group' in member_types:
306 filter += ('(&(sAMAccountName=%s)'
307 '(objectClass=group)'
308 '(!(groupType:1.2.840.113556.1.4.803:=1)))' %
309 ldb.binary_encode(member))
310 if 'computer' in member_types:
311 samaccountname = member
312 if member[-1] != '$':
313 samaccountname = "%s$" % member
314 filter += ('(&(samAccountType=%d)'
315 '(!(objectCategory=msDS-ManagedServiceAccount))'
316 '(sAMAccountName=%s))' %
317 (dsdb.ATYPE_WORKSTATION_TRUST,
318 ldb.binary_encode(samaccountname)))
319 if 'serviceaccount' in member_types:
320 samaccountname = member
321 if member[-1] != '$':
322 samaccountname = "%s$" % member
323 filter += ('(&(samAccountType=%d)'
324 '(objectCategory=msDS-ManagedServiceAccount)'
325 '(sAMAccountName=%s))' %
326 (dsdb.ATYPE_WORKSTATION_TRUST,
327 ldb.binary_encode(samaccountname)))
328 if 'contact' in member_types:
329 filter += ('(&(objectCategory=Person)(!(objectSid=*))(name=%s))' %
330 ldb.binary_encode(member))
332 filter = "(|%s)" % filter
334 return filter
336 def add_remove_group_members(self, groupname, members,
337 add_members_operation=True,
338 member_types=[ 'user', 'group', 'computer' ],
339 member_base_dn=None):
340 """Adds or removes group members
342 :param groupname: Name of the target group
343 :param members: list of group members
344 :param add_members_operation: Defines if its an add or remove
345 operation
348 groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (
349 ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
351 self.transaction_start()
352 try:
353 targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
354 expression=groupfilter, attrs=['member'])
355 if len(targetgroup) == 0:
356 raise Exception('Unable to find group "%s"' % groupname)
357 assert(len(targetgroup) == 1)
359 modified = False
361 addtargettogroup = """
362 dn: %s
363 changetype: modify
364 """ % (str(targetgroup[0].dn))
366 for member in members:
367 targetmember_dn = None
368 if member_base_dn is None:
369 member_base_dn = self.domain_dn()
371 try:
372 membersid = security.dom_sid(member)
373 targetmember_dn = "<SID=%s>" % str(membersid)
374 except TypeError as e:
375 pass
377 if targetmember_dn is None:
378 try:
379 member_dn = ldb.Dn(self, member)
380 if member_dn.get_linearized() == member_dn.extended_str(1):
381 full_member_dn = self.normalize_dn_in_domain(member_dn)
382 else:
383 full_member_dn = member_dn
384 targetmember_dn = full_member_dn.extended_str(1)
385 except ValueError as e:
386 pass
388 if targetmember_dn is None:
389 filter = self.group_member_filter(member, member_types)
390 targetmember = self.search(base=member_base_dn,
391 scope=ldb.SCOPE_SUBTREE,
392 expression=filter,
393 attrs=[])
395 if len(targetmember) > 1:
396 targetmemberlist_str = ""
397 for msg in targetmember:
398 targetmemberlist_str += "%s\n" % msg.get("dn")
399 raise Exception('Found multiple results for "%s":\n%s' %
400 (member, targetmemberlist_str))
401 if len(targetmember) != 1:
402 raise Exception('Unable to find "%s". Operation cancelled.' % member)
403 targetmember_dn = targetmember[0].dn.extended_str(1)
405 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']]):
406 modified = True
407 addtargettogroup += """add: member
408 member: %s
409 """ % (str(targetmember_dn))
411 elif add_members_operation is False and (targetgroup[0].get('member') is not None and get_bytes(targetmember_dn) in targetgroup[0]['member']):
412 modified = True
413 addtargettogroup += """delete: member
414 member: %s
415 """ % (str(targetmember_dn))
417 if modified is True:
418 self.modify_ldif(addtargettogroup)
420 except:
421 self.transaction_cancel()
422 raise
423 else:
424 self.transaction_commit()
426 def prepare_attr_replace(self, msg, old, attr_name, value):
427 """Changes the MessageElement with the given attr_name of the
428 given Message. If the value is "" set an empty value and the flag
429 FLAG_MOD_DELETE, otherwise set the new value and FLAG_MOD_REPLACE.
430 If the value is None or the Message contains the attr_name with this
431 value, nothing will changed."""
432 # skip unchanged attribute
433 if value is None:
434 return
435 if attr_name in old and str(value) == str(old[attr_name]):
436 return
438 # remove attribute
439 if len(value) == 0:
440 if attr_name in old:
441 el = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr_name)
442 msg.add(el)
443 return
445 # change attribute
446 el = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, attr_name)
447 msg.add(el)
449 def fullname_from_names(self, given_name=None, initials=None, surname=None,
450 old_attrs={}, fallback_default=""):
451 """Prepares new combined fullname, using the name parts.
452 Used for things like displayName or cn.
453 Use the original name values, if no new one is specified."""
455 attrs = {"givenName": given_name,
456 "initials": initials,
457 "sn": surname}
459 # if the attribute is not specified, try to use the old one
460 for attr_name, attr_value in attrs.items():
461 if attr_value == None and attr_name in old_attrs:
462 attrs[attr_name] = str(old_attrs[attr_name])
464 # add '.' to initials if initals are not None and not "" and if the initials
465 # don't have already a '.' at the end
466 if attrs["initials"] and not attrs["initials"].endswith('.'):
467 attrs["initials"] += '.'
469 # remove empty values (None and '')
470 attrs_values = list(filter(None, attrs.values()))
472 # fullname is the combination of not-empty values as string, separated by ' '
473 fullname = ' '.join(attrs_values)
475 if fullname == '':
476 return fallback_default
478 return fullname
480 def newuser(self, username, password,
481 force_password_change_at_next_login_req=False,
482 useusernameascn=False, userou=None, surname=None, givenname=None,
483 initials=None, profilepath=None, scriptpath=None, homedrive=None,
484 homedirectory=None, jobtitle=None, department=None, company=None,
485 description=None, mailaddress=None, internetaddress=None,
486 telephonenumber=None, physicaldeliveryoffice=None, sd=None,
487 setpassword=True, uidnumber=None, gidnumber=None, gecos=None,
488 loginshell=None, uid=None, nisdomain=None, unixhome=None,
489 smartcard_required=False):
490 """Adds a new user with additional parameters
492 :param username: Name of the new user
493 :param password: Password for the new user
494 :param force_password_change_at_next_login_req: Force password change
495 :param useusernameascn: Use username as cn rather that firstname +
496 initials + lastname
497 :param userou: Object container (without domainDN postfix) for new user
498 :param surname: Surname of the new user
499 :param givenname: First name of the new user
500 :param initials: Initials of the new user
501 :param profilepath: Profile path of the new user
502 :param scriptpath: Logon script path of the new user
503 :param homedrive: Home drive of the new user
504 :param homedirectory: Home directory of the new user
505 :param jobtitle: Job title of the new user
506 :param department: Department of the new user
507 :param company: Company of the new user
508 :param description: of the new user
509 :param mailaddress: Email address of the new user
510 :param internetaddress: Home page of the new user
511 :param telephonenumber: Phone number of the new user
512 :param physicaldeliveryoffice: Office location of the new user
513 :param sd: security descriptor of the object
514 :param setpassword: optionally disable password reset
515 :param uidnumber: RFC2307 Unix numeric UID of the new user
516 :param gidnumber: RFC2307 Unix primary GID of the new user
517 :param gecos: RFC2307 Unix GECOS field of the new user
518 :param loginshell: RFC2307 Unix login shell of the new user
519 :param uid: RFC2307 Unix username of the new user
520 :param nisdomain: RFC2307 Unix NIS domain of the new user
521 :param unixhome: RFC2307 Unix home directory of the new user
522 :param smartcard_required: set the UF_SMARTCARD_REQUIRED bit of the new user
525 displayname = self.fullname_from_names(given_name=givenname,
526 initials=initials,
527 surname=surname)
528 cn = username
529 if useusernameascn is None and displayname != "":
530 cn = displayname
532 user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn())
534 dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "")
535 user_principal_name = "%s@%s" % (username, dnsdomain)
536 # The new user record. Note the reliance on the SAMLDB module which
537 # fills in the default information
538 ldbmessage = {"dn": user_dn,
539 "sAMAccountName": username,
540 "userPrincipalName": user_principal_name,
541 "objectClass": "user"}
543 if smartcard_required:
544 ldbmessage["userAccountControl"] = str(dsdb.UF_NORMAL_ACCOUNT |
545 dsdb.UF_SMARTCARD_REQUIRED)
546 setpassword = False
548 if surname is not None:
549 ldbmessage["sn"] = surname
551 if givenname is not None:
552 ldbmessage["givenName"] = givenname
554 if displayname != "":
555 ldbmessage["displayName"] = displayname
556 ldbmessage["name"] = displayname
558 if initials is not None:
559 ldbmessage["initials"] = '%s.' % initials
561 if profilepath is not None:
562 ldbmessage["profilePath"] = profilepath
564 if scriptpath is not None:
565 ldbmessage["scriptPath"] = scriptpath
567 if homedrive is not None:
568 ldbmessage["homeDrive"] = homedrive
570 if homedirectory is not None:
571 ldbmessage["homeDirectory"] = homedirectory
573 if jobtitle is not None:
574 ldbmessage["title"] = jobtitle
576 if department is not None:
577 ldbmessage["department"] = department
579 if company is not None:
580 ldbmessage["company"] = company
582 if description is not None:
583 ldbmessage["description"] = description
585 if mailaddress is not None:
586 ldbmessage["mail"] = mailaddress
588 if internetaddress is not None:
589 ldbmessage["wWWHomePage"] = internetaddress
591 if telephonenumber is not None:
592 ldbmessage["telephoneNumber"] = telephonenumber
594 if physicaldeliveryoffice is not None:
595 ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
597 if sd is not None:
598 ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
600 ldbmessage2 = None
601 if any(map(lambda b: b is not None, (uid, uidnumber, gidnumber, gecos,
602 loginshell, nisdomain, unixhome))):
603 ldbmessage2 = ldb.Message()
604 ldbmessage2.dn = ldb.Dn(self, user_dn)
605 if uid is not None:
606 ldbmessage2["uid"] = ldb.MessageElement(str(uid), ldb.FLAG_MOD_REPLACE, 'uid')
607 if uidnumber is not None:
608 ldbmessage2["uidNumber"] = ldb.MessageElement(str(uidnumber), ldb.FLAG_MOD_REPLACE, 'uidNumber')
609 if gidnumber is not None:
610 ldbmessage2["gidNumber"] = ldb.MessageElement(str(gidnumber), ldb.FLAG_MOD_REPLACE, 'gidNumber')
611 if gecos is not None:
612 ldbmessage2["gecos"] = ldb.MessageElement(str(gecos), ldb.FLAG_MOD_REPLACE, 'gecos')
613 if loginshell is not None:
614 ldbmessage2["loginShell"] = ldb.MessageElement(str(loginshell), ldb.FLAG_MOD_REPLACE, 'loginShell')
615 if unixhome is not None:
616 ldbmessage2["unixHomeDirectory"] = ldb.MessageElement(
617 str(unixhome), ldb.FLAG_MOD_REPLACE, 'unixHomeDirectory')
618 if nisdomain is not None:
619 ldbmessage2["msSFU30NisDomain"] = ldb.MessageElement(
620 str(nisdomain), ldb.FLAG_MOD_REPLACE, 'msSFU30NisDomain')
621 ldbmessage2["msSFU30Name"] = ldb.MessageElement(
622 str(username), ldb.FLAG_MOD_REPLACE, 'msSFU30Name')
623 ldbmessage2["unixUserPassword"] = ldb.MessageElement(
624 'ABCD!efgh12345$67890', ldb.FLAG_MOD_REPLACE,
625 'unixUserPassword')
627 self.transaction_start()
628 try:
629 self.add(ldbmessage)
630 if ldbmessage2:
631 self.modify(ldbmessage2)
633 # Sets the password for it
634 if setpassword:
635 self.setpassword(("(distinguishedName=%s)" %
636 ldb.binary_encode(user_dn)),
637 password,
638 force_password_change_at_next_login_req)
639 except:
640 self.transaction_cancel()
641 raise
642 else:
643 self.transaction_commit()
645 def newcontact(self,
646 fullcontactname=None,
647 ou=None,
648 surname=None,
649 givenname=None,
650 initials=None,
651 displayname=None,
652 jobtitle=None,
653 department=None,
654 company=None,
655 description=None,
656 mailaddress=None,
657 internetaddress=None,
658 telephonenumber=None,
659 mobilenumber=None,
660 physicaldeliveryoffice=None):
661 """Adds a new contact with additional parameters
663 :param fullcontactname: Optional full name of the new contact
664 :param ou: Object container for new contact
665 :param surname: Surname of the new contact
666 :param givenname: First name of the new contact
667 :param initials: Initials of the new contact
668 :param displayname: displayName of the new contact
669 :param jobtitle: Job title of the new contact
670 :param department: Department of the new contact
671 :param company: Company of the new contact
672 :param description: Description of the new contact
673 :param mailaddress: Email address of the new contact
674 :param internetaddress: Home page of the new contact
675 :param telephonenumber: Phone number of the new contact
676 :param mobilenumber: Primary mobile number of the new contact
677 :param physicaldeliveryoffice: Office location of the new contact
680 # Prepare the contact name like the RSAT, using the name parts.
681 cn = self.fullname_from_names(given_name=givenname,
682 initials=initials,
683 surname=surname)
685 # Use the specified fullcontactname instead of the previously prepared
686 # contact name, if it is specified.
687 # This is similar to the "Full name" value of the RSAT.
688 if fullcontactname is not None:
689 cn = fullcontactname
691 if fullcontactname is None and cn == "":
692 raise Exception('No name for contact specified')
694 contactcontainer_dn = self.domain_dn()
695 if ou:
696 contactcontainer_dn = self.normalize_dn_in_domain(ou)
698 contact_dn = "CN=%s,%s" % (cn, contactcontainer_dn)
700 ldbmessage = {"dn": contact_dn,
701 "objectClass": "contact",
704 if surname is not None:
705 ldbmessage["sn"] = surname
707 if givenname is not None:
708 ldbmessage["givenName"] = givenname
710 if displayname is not None:
711 ldbmessage["displayName"] = displayname
713 if initials is not None:
714 ldbmessage["initials"] = '%s.' % initials
716 if jobtitle is not None:
717 ldbmessage["title"] = jobtitle
719 if department is not None:
720 ldbmessage["department"] = department
722 if company is not None:
723 ldbmessage["company"] = company
725 if description is not None:
726 ldbmessage["description"] = description
728 if mailaddress is not None:
729 ldbmessage["mail"] = mailaddress
731 if internetaddress is not None:
732 ldbmessage["wWWHomePage"] = internetaddress
734 if telephonenumber is not None:
735 ldbmessage["telephoneNumber"] = telephonenumber
737 if mobilenumber is not None:
738 ldbmessage["mobile"] = mobilenumber
740 if physicaldeliveryoffice is not None:
741 ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
743 self.add(ldbmessage)
745 return cn
747 def newcomputer(self, computername, computerou=None, description=None,
748 prepare_oldjoin=False, ip_address_list=None,
749 service_principal_name_list=None):
750 """Adds a new user with additional parameters
752 :param computername: Name of the new computer
753 :param computerou: Object container for new computer
754 :param description: Description of the new computer
755 :param prepare_oldjoin: Preset computer password for oldjoin mechanism
756 :param ip_address_list: ip address list for DNS A or AAAA record
757 :param service_principal_name_list: string list of servicePincipalName
760 cn = re.sub(r"\$$", "", computername)
761 if cn.count('$'):
762 raise Exception('Illegal computername "%s"' % computername)
763 samaccountname = "%s$" % cn
765 computercontainer_dn = "CN=Computers,%s" % self.domain_dn()
766 if computerou:
767 computercontainer_dn = self.normalize_dn_in_domain(computerou)
769 computer_dn = "CN=%s,%s" % (cn, computercontainer_dn)
771 ldbmessage = {"dn": computer_dn,
772 "sAMAccountName": samaccountname,
773 "objectClass": "computer",
776 if description is not None:
777 ldbmessage["description"] = description
779 if service_principal_name_list:
780 ldbmessage["servicePrincipalName"] = service_principal_name_list
782 accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
783 dsdb.UF_ACCOUNTDISABLE)
784 if prepare_oldjoin:
785 accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
786 ldbmessage["userAccountControl"] = accountcontrol
788 if ip_address_list:
789 ldbmessage['dNSHostName'] = '{}.{}'.format(
790 cn, self.domain_dns_name())
792 self.transaction_start()
793 try:
794 self.add(ldbmessage)
796 if prepare_oldjoin:
797 password = cn.lower()
798 self.setpassword(("(distinguishedName=%s)" %
799 ldb.binary_encode(computer_dn)),
800 password, False)
801 except:
802 self.transaction_cancel()
803 raise
804 else:
805 self.transaction_commit()
807 def deleteuser(self, username):
808 """Deletes a user
810 :param username: Name of the target user
813 filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn())
814 self.transaction_start()
815 try:
816 target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
817 expression=filter, attrs=[])
818 if len(target) == 0:
819 raise Exception('Unable to find user "%s"' % username)
820 assert(len(target) == 1)
821 self.delete(target[0].dn)
822 except:
823 self.transaction_cancel()
824 raise
825 else:
826 self.transaction_commit()
828 def setpassword(self, search_filter, password,
829 force_change_at_next_login=False, username=None):
830 """Sets the password for a user
832 :param search_filter: LDAP filter to find the user (eg
833 samccountname=name)
834 :param password: Password for the user
835 :param force_change_at_next_login: Force password change
837 self.transaction_start()
838 try:
839 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
840 expression=search_filter, attrs=[])
841 if len(res) == 0:
842 raise Exception('Unable to find user "%s"' % (username or search_filter))
843 if len(res) > 1:
844 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter))
845 user_dn = res[0].dn
846 if not isinstance(password, str):
847 pw = password.decode('utf-8')
848 else:
849 pw = password
850 pw = ('"' + pw + '"').encode('utf-16-le')
851 setpw = """
852 dn: %s
853 changetype: modify
854 replace: unicodePwd
855 unicodePwd:: %s
856 """ % (user_dn, base64.b64encode(pw).decode('utf-8'))
858 self.modify_ldif(setpw)
860 if force_change_at_next_login:
861 self.force_password_change_at_next_login(
862 "(distinguishedName=" + str(user_dn) + ")")
864 # modify the userAccountControl to remove the disabled bit
865 self.enable_account(search_filter)
866 except:
867 self.transaction_cancel()
868 raise
869 else:
870 self.transaction_commit()
872 def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False):
873 """Sets the account expiry for a user
875 :param search_filter: LDAP filter to find the user (eg
876 samaccountname=name)
877 :param expiry_seconds: expiry time from now in seconds
878 :param no_expiry_req: if set, then don't expire password
880 self.transaction_start()
881 try:
882 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
883 expression=search_filter,
884 attrs=["userAccountControl", "accountExpires"])
885 if len(res) == 0:
886 raise Exception('Unable to find user "%s"' % search_filter)
887 assert(len(res) == 1)
888 user_dn = res[0].dn
890 userAccountControl = int(res[0]["userAccountControl"][0])
891 accountExpires = int(res[0]["accountExpires"][0])
892 if no_expiry_req:
893 userAccountControl = userAccountControl | 0x10000
894 accountExpires = 0
895 else:
896 userAccountControl = userAccountControl & ~0x10000
897 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
899 setexp = """
900 dn: %s
901 changetype: modify
902 replace: userAccountControl
903 userAccountControl: %u
904 replace: accountExpires
905 accountExpires: %u
906 """ % (user_dn, userAccountControl, accountExpires)
908 self.modify_ldif(setexp)
909 except:
910 self.transaction_cancel()
911 raise
912 else:
913 self.transaction_commit()
915 def set_domain_sid(self, sid):
916 """Change the domain SID used by this LDB.
918 :param sid: The new domain sid to use.
920 dsdb._samdb_set_domain_sid(self, sid)
922 def get_domain_sid(self):
923 """Read the domain SID used by this LDB. """
924 return dsdb._samdb_get_domain_sid(self)
926 domain_sid = property(get_domain_sid, set_domain_sid,
927 doc="SID for the domain")
929 def set_invocation_id(self, invocation_id):
930 """Set the invocation id for this SamDB handle.
932 :param invocation_id: GUID of the invocation id.
934 dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
936 def get_invocation_id(self):
937 """Get the invocation_id id"""
938 return dsdb._samdb_ntds_invocation_id(self)
940 invocation_id = property(get_invocation_id, set_invocation_id,
941 doc="Invocation ID GUID")
943 def get_oid_from_attid(self, attid):
944 return dsdb._dsdb_get_oid_from_attid(self, attid)
946 def get_attid_from_lDAPDisplayName(self, ldap_display_name,
947 is_schema_nc=False):
948 '''return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI'''
949 return dsdb._dsdb_get_attid_from_lDAPDisplayName(self,
950 ldap_display_name, is_schema_nc)
952 def get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name):
953 '''return the syntax OID for a LDAP attribute as a string'''
954 return dsdb._dsdb_get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name)
956 def get_systemFlags_from_lDAPDisplayName(self, ldap_display_name):
957 '''return the systemFlags for a LDAP attribute as a integer'''
958 return dsdb._dsdb_get_systemFlags_from_lDAPDisplayName(self, ldap_display_name)
960 def get_linkId_from_lDAPDisplayName(self, ldap_display_name):
961 '''return the linkID for a LDAP attribute as a integer'''
962 return dsdb._dsdb_get_linkId_from_lDAPDisplayName(self, ldap_display_name)
964 def get_lDAPDisplayName_by_attid(self, attid):
965 '''return the lDAPDisplayName from an integer DRS attribute ID'''
966 return dsdb._dsdb_get_lDAPDisplayName_by_attid(self, attid)
968 def get_backlink_from_lDAPDisplayName(self, ldap_display_name):
969 '''return the attribute name of the corresponding backlink from the name
970 of a forward link attribute. If there is no backlink return None'''
971 return dsdb._dsdb_get_backlink_from_lDAPDisplayName(self, ldap_display_name)
973 def set_ntds_settings_dn(self, ntds_settings_dn):
974 """Set the NTDS Settings DN, as would be returned on the dsServiceName
975 rootDSE attribute.
977 This allows the DN to be set before the database fully exists
979 :param ntds_settings_dn: The new DN to use
981 dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
983 def get_ntds_GUID(self):
984 """Get the NTDS objectGUID"""
985 return dsdb._samdb_ntds_objectGUID(self)
987 def server_site_name(self):
988 """Get the server site name"""
989 return dsdb._samdb_server_site_name(self)
991 def host_dns_name(self):
992 """return the DNS name of this host"""
993 res = self.search(base='', scope=ldb.SCOPE_BASE, attrs=['dNSHostName'])
994 return str(res[0]['dNSHostName'][0])
996 def domain_dns_name(self):
997 """return the DNS name of the domain root"""
998 domain_dn = self.get_default_basedn()
999 return domain_dn.canonical_str().split('/')[0]
1001 def domain_netbios_name(self):
1002 """return the NetBIOS name of the domain root"""
1003 domain_dn = self.get_default_basedn()
1004 dns_name = self.domain_dns_name()
1005 filter = "(&(objectClass=crossRef)(nETBIOSName=*)(ncName=%s)(dnsroot=%s))" % (domain_dn, dns_name)
1006 partitions_dn = self.get_partitions_dn()
1007 res = self.search(partitions_dn,
1008 scope=ldb.SCOPE_ONELEVEL,
1009 expression=filter)
1010 try:
1011 netbios_domain = res[0]["nETBIOSName"][0].decode()
1012 except IndexError:
1013 return None
1014 return netbios_domain
1016 def forest_dns_name(self):
1017 """return the DNS name of the forest root"""
1018 forest_dn = self.get_root_basedn()
1019 return forest_dn.canonical_str().split('/')[0]
1021 def load_partition_usn(self, base_dn):
1022 return dsdb._dsdb_load_partition_usn(self, base_dn)
1024 def set_schema(self, schema, write_indices_and_attributes=True):
1025 self.set_schema_from_ldb(schema.ldb, write_indices_and_attributes=write_indices_and_attributes)
1027 def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True):
1028 dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes)
1030 def set_schema_update_now(self):
1031 ldif = """
1033 changetype: modify
1034 add: schemaUpdateNow
1035 schemaUpdateNow: 1
1037 self.modify_ldif(ldif)
1039 def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
1040 '''convert a list of attribute values to a DRSUAPI DsReplicaAttribute'''
1041 return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
1043 def dsdb_normalise_attributes(self, ldb, ldap_display_name, ldif_elements):
1044 '''normalise a list of attribute values'''
1045 return dsdb._dsdb_normalise_attributes(ldb, ldap_display_name, ldif_elements)
1047 def get_attribute_from_attid(self, attid):
1048 """ Get from an attid the associated attribute
1050 :param attid: The attribute id for searched attribute
1051 :return: The name of the attribute associated with this id
1053 if len(self.hash_oid_name.keys()) == 0:
1054 self._populate_oid_attid()
1055 if self.get_oid_from_attid(attid) in self.hash_oid_name:
1056 return self.hash_oid_name[self.get_oid_from_attid(attid)]
1057 else:
1058 return None
1060 def _populate_oid_attid(self):
1061 """Populate the hash hash_oid_name.
1063 This hash contains the oid of the attribute as a key and
1064 its display name as a value
1066 self.hash_oid_name = {}
1067 res = self.search(expression="objectClass=attributeSchema",
1068 controls=["search_options:1:2"],
1069 attrs=["attributeID",
1070 "lDAPDisplayName"])
1071 if len(res) > 0:
1072 for e in res:
1073 strDisplay = str(e.get("lDAPDisplayName"))
1074 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
1076 def get_attribute_replmetadata_version(self, dn, att):
1077 """Get the version field trom the replPropertyMetaData for
1078 the given field
1080 :param dn: The on which we want to get the version
1081 :param att: The name of the attribute
1082 :return: The value of the version field in the replPropertyMetaData
1083 for the given attribute. None if the attribute is not replicated
1086 res = self.search(expression="distinguishedName=%s" % dn,
1087 scope=ldb.SCOPE_SUBTREE,
1088 controls=["search_options:1:2"],
1089 attrs=["replPropertyMetaData"])
1090 if len(res) == 0:
1091 return None
1093 repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
1094 res[0]["replPropertyMetaData"][0])
1095 ctr = repl.ctr
1096 if len(self.hash_oid_name.keys()) == 0:
1097 self._populate_oid_attid()
1098 for o in ctr.array:
1099 # Search for Description
1100 att_oid = self.get_oid_from_attid(o.attid)
1101 if att_oid in self.hash_oid_name and\
1102 att.lower() == self.hash_oid_name[att_oid].lower():
1103 return o.version
1104 return None
1106 def set_attribute_replmetadata_version(self, dn, att, value,
1107 addifnotexist=False):
1108 res = self.search(expression="distinguishedName=%s" % dn,
1109 scope=ldb.SCOPE_SUBTREE,
1110 controls=["search_options:1:2"],
1111 attrs=["replPropertyMetaData"])
1112 if len(res) == 0:
1113 return None
1115 repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
1116 res[0]["replPropertyMetaData"][0])
1117 ctr = repl.ctr
1118 now = samba.unix2nttime(int(time.time()))
1119 found = False
1120 if len(self.hash_oid_name.keys()) == 0:
1121 self._populate_oid_attid()
1122 for o in ctr.array:
1123 # Search for Description
1124 att_oid = self.get_oid_from_attid(o.attid)
1125 if att_oid in self.hash_oid_name and\
1126 att.lower() == self.hash_oid_name[att_oid].lower():
1127 found = True
1128 seq = self.sequence_number(ldb.SEQ_NEXT)
1129 o.version = value
1130 o.originating_change_time = now
1131 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
1132 o.originating_usn = seq
1133 o.local_usn = seq
1135 if not found and addifnotexist and len(ctr.array) > 0:
1136 o2 = drsblobs.replPropertyMetaData1()
1137 o2.attid = 589914
1138 att_oid = self.get_oid_from_attid(o2.attid)
1139 seq = self.sequence_number(ldb.SEQ_NEXT)
1140 o2.version = value
1141 o2.originating_change_time = now
1142 o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
1143 o2.originating_usn = seq
1144 o2.local_usn = seq
1145 found = True
1146 tab = ctr.array
1147 tab.append(o2)
1148 ctr.count = ctr.count + 1
1149 ctr.array = tab
1151 if found:
1152 replBlob = ndr_pack(repl)
1153 msg = ldb.Message()
1154 msg.dn = res[0].dn
1155 msg["replPropertyMetaData"] = \
1156 ldb.MessageElement(replBlob,
1157 ldb.FLAG_MOD_REPLACE,
1158 "replPropertyMetaData")
1159 self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
1161 def write_prefixes_from_schema(self):
1162 dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
1164 def get_partitions_dn(self):
1165 return dsdb._dsdb_get_partitions_dn(self)
1167 def get_nc_root(self, dn):
1168 return dsdb._dsdb_get_nc_root(self, dn)
1170 def get_wellknown_dn(self, nc_root, wkguid):
1171 h_nc = self.hash_well_known.get(str(nc_root))
1172 dn = None
1173 if h_nc is not None:
1174 dn = h_nc.get(wkguid)
1175 if dn is None:
1176 dn = dsdb._dsdb_get_wellknown_dn(self, nc_root, wkguid)
1177 if dn is None:
1178 return dn
1179 if h_nc is None:
1180 self.hash_well_known[str(nc_root)] = {}
1181 h_nc = self.hash_well_known[str(nc_root)]
1182 h_nc[wkguid] = dn
1183 return dn
1185 def set_minPwdAge(self, value):
1186 if not isinstance(value, bytes):
1187 value = str(value).encode('utf8')
1188 m = ldb.Message()
1189 m.dn = ldb.Dn(self, self.domain_dn())
1190 m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
1191 self.modify(m)
1193 def get_minPwdAge(self):
1194 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
1195 if len(res) == 0:
1196 return None
1197 elif "minPwdAge" not in res[0]:
1198 return None
1199 else:
1200 return int(res[0]["minPwdAge"][0])
1202 def set_maxPwdAge(self, value):
1203 if not isinstance(value, bytes):
1204 value = str(value).encode('utf8')
1205 m = ldb.Message()
1206 m.dn = ldb.Dn(self, self.domain_dn())
1207 m["maxPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "maxPwdAge")
1208 self.modify(m)
1210 def get_maxPwdAge(self):
1211 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["maxPwdAge"])
1212 if len(res) == 0:
1213 return None
1214 elif "maxPwdAge" not in res[0]:
1215 return None
1216 else:
1217 return int(res[0]["maxPwdAge"][0])
1219 def set_minPwdLength(self, value):
1220 if not isinstance(value, bytes):
1221 value = str(value).encode('utf8')
1222 m = ldb.Message()
1223 m.dn = ldb.Dn(self, self.domain_dn())
1224 m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength")
1225 self.modify(m)
1227 def get_minPwdLength(self):
1228 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"])
1229 if len(res) == 0:
1230 return None
1231 elif "minPwdLength" not in res[0]:
1232 return None
1233 else:
1234 return int(res[0]["minPwdLength"][0])
1236 def set_pwdProperties(self, value):
1237 if not isinstance(value, bytes):
1238 value = str(value).encode('utf8')
1239 m = ldb.Message()
1240 m.dn = ldb.Dn(self, self.domain_dn())
1241 m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties")
1242 self.modify(m)
1244 def get_pwdProperties(self):
1245 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"])
1246 if len(res) == 0:
1247 return None
1248 elif "pwdProperties" not in res[0]:
1249 return None
1250 else:
1251 return int(res[0]["pwdProperties"][0])
1253 def set_dsheuristics(self, dsheuristics):
1254 m = ldb.Message()
1255 m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
1256 % self.get_config_basedn().get_linearized())
1257 if dsheuristics is not None:
1258 m["dSHeuristics"] = \
1259 ldb.MessageElement(dsheuristics,
1260 ldb.FLAG_MOD_REPLACE,
1261 "dSHeuristics")
1262 else:
1263 m["dSHeuristics"] = \
1264 ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
1265 "dSHeuristics")
1266 self.modify(m)
1268 def get_dsheuristics(self):
1269 res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
1270 % self.get_config_basedn().get_linearized(),
1271 scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
1272 if len(res) == 0:
1273 dsheuristics = None
1274 elif "dSHeuristics" in res[0]:
1275 dsheuristics = res[0]["dSHeuristics"][0]
1276 else:
1277 dsheuristics = None
1279 return dsheuristics
1281 def create_ou(self, ou_dn, description=None, name=None, sd=None):
1282 """Creates an organizationalUnit object
1283 :param ou_dn: dn of the new object
1284 :param description: description attribute
1285 :param name: name atttribute
1286 :param sd: security descriptor of the object, can be
1287 an SDDL string or security.descriptor type
1289 m = {"dn": ou_dn,
1290 "objectClass": "organizationalUnit"}
1292 if description:
1293 m["description"] = description
1294 if name:
1295 m["name"] = name
1297 if sd:
1298 m["nTSecurityDescriptor"] = ndr_pack(sd)
1299 self.add(m)
1301 def sequence_number(self, seq_type):
1302 """Returns the value of the sequence number according to the requested type
1303 :param seq_type: type of sequence number
1305 self.transaction_start()
1306 try:
1307 seq = super(SamDB, self).sequence_number(seq_type)
1308 except:
1309 self.transaction_cancel()
1310 raise
1311 else:
1312 self.transaction_commit()
1313 return seq
1315 def get_dsServiceName(self):
1316 '''get the NTDS DN from the rootDSE'''
1317 res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
1318 return str(res[0]["dsServiceName"][0])
1320 def get_serverName(self):
1321 '''get the server DN from the rootDSE'''
1322 res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["serverName"])
1323 return str(res[0]["serverName"][0])
1325 def dns_lookup(self, dns_name, dns_partition=None):
1326 '''Do a DNS lookup in the database, returns the NDR database structures'''
1327 if dns_partition is None:
1328 return dsdb_dns.lookup(self, dns_name)
1329 else:
1330 return dsdb_dns.lookup(self, dns_name,
1331 dns_partition=dns_partition)
1333 def dns_extract(self, el):
1334 '''Return the NDR database structures from a dnsRecord element'''
1335 return dsdb_dns.extract(self, el)
1337 def dns_replace(self, dns_name, new_records):
1338 '''Do a DNS modification on the database, sets the NDR database
1339 structures on a DNS name
1341 return dsdb_dns.replace(self, dns_name, new_records)
1343 def dns_replace_by_dn(self, dn, new_records):
1344 '''Do a DNS modification on the database, sets the NDR database
1345 structures on a LDB DN
1347 This routine is important because if the last record on the DN
1348 is removed, this routine will put a tombstone in the record.
1350 return dsdb_dns.replace_by_dn(self, dn, new_records)
1352 def garbage_collect_tombstones(self, dn, current_time,
1353 tombstone_lifetime=None):
1354 '''garbage_collect_tombstones(lp, samdb, [dn], current_time, tombstone_lifetime)
1355 -> (num_objects_expunged, num_links_expunged)'''
1357 if tombstone_lifetime is None:
1358 return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1359 current_time)
1360 else:
1361 return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1362 current_time,
1363 tombstone_lifetime)
1365 def create_own_rid_set(self):
1366 '''create a RID set for this DSA'''
1367 return dsdb._dsdb_create_own_rid_set(self)
1369 def allocate_rid(self):
1370 '''return a new RID from the RID Pool on this DSA'''
1371 return dsdb._dsdb_allocate_rid(self)
1373 def normalize_dn_in_domain(self, dn):
1374 '''return a new DN expanded by adding the domain DN
1376 If the dn is already a child of the domain DN, just
1377 return it as-is.
1379 :param dn: relative dn
1381 domain_dn = ldb.Dn(self, self.domain_dn())
1383 if isinstance(dn, ldb.Dn):
1384 dn = str(dn)
1386 full_dn = ldb.Dn(self, dn)
1387 if not full_dn.is_child_of(domain_dn):
1388 full_dn.add_base(domain_dn)
1389 return full_dn
1391 class dsdb_Dn(object):
1392 '''a class for binary DN'''
1394 def __init__(self, samdb, dnstring, syntax_oid=None):
1395 '''create a dsdb_Dn'''
1396 if syntax_oid is None:
1397 # auto-detect based on string
1398 if dnstring.startswith("B:"):
1399 syntax_oid = dsdb.DSDB_SYNTAX_BINARY_DN
1400 elif dnstring.startswith("S:"):
1401 syntax_oid = dsdb.DSDB_SYNTAX_STRING_DN
1402 else:
1403 syntax_oid = dsdb.DSDB_SYNTAX_OR_NAME
1404 if syntax_oid in [dsdb.DSDB_SYNTAX_BINARY_DN, dsdb.DSDB_SYNTAX_STRING_DN]:
1405 # it is a binary DN
1406 colons = dnstring.split(':')
1407 if len(colons) < 4:
1408 raise RuntimeError("Invalid DN %s" % dnstring)
1409 prefix_len = 4 + len(colons[1]) + int(colons[1])
1410 self.prefix = dnstring[0:prefix_len]
1411 self.binary = self.prefix[3 + len(colons[1]):-1]
1412 self.dnstring = dnstring[prefix_len:]
1413 else:
1414 self.dnstring = dnstring
1415 self.prefix = ''
1416 self.binary = ''
1417 self.dn = ldb.Dn(samdb, self.dnstring)
1419 def __str__(self):
1420 return self.prefix + str(self.dn.extended_str(mode=1))
1422 def __cmp__(self, other):
1423 ''' compare dsdb_Dn values similar to parsed_dn_compare()'''
1424 dn1 = self
1425 dn2 = other
1426 guid1 = dn1.dn.get_extended_component("GUID")
1427 guid2 = dn2.dn.get_extended_component("GUID")
1429 v = cmp(guid1, guid2)
1430 if v != 0:
1431 return v
1432 v = cmp(dn1.binary, dn2.binary)
1433 return v
1435 # In Python3, __cmp__ is replaced by these 6 methods
1436 def __eq__(self, other):
1437 return self.__cmp__(other) == 0
1439 def __ne__(self, other):
1440 return self.__cmp__(other) != 0
1442 def __lt__(self, other):
1443 return self.__cmp__(other) < 0
1445 def __le__(self, other):
1446 return self.__cmp__(other) <= 0
1448 def __gt__(self, other):
1449 return self.__cmp__(other) > 0
1451 def __ge__(self, other):
1452 return self.__cmp__(other) >= 0
1454 def get_binary_integer(self):
1455 '''return binary part of a dsdb_Dn as an integer, or None'''
1456 if self.prefix == '':
1457 return None
1458 return int(self.binary, 16)
1460 def get_bytes(self):
1461 '''return binary as a byte string'''
1462 return binascii.unhexlify(self.binary)