ctdb-tests: add a comment to the generated public_addresses file used by eventscript...
[Samba.git] / python / samba / samdb.py
blob5e4f07bb6797a1f52ad286e6c1305c98c7f0f150
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 = {}
56 hash_well_known = {}
58 def __init__(self, url=None, lp=None, modules_dir=None, session_info=None,
59 credentials=None, flags=ldb.FLG_DONT_CREATE_DB,
60 options=None, global_schema=True,
61 auto_connect=True, am_rodc=None):
62 self.lp = lp
63 if not auto_connect:
64 url = None
65 elif url is None and lp is not None:
66 url = lp.samdb_url()
68 self.url = url
70 super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir,
71 session_info=session_info, credentials=credentials, flags=flags,
72 options=options)
74 if global_schema:
75 dsdb._dsdb_set_global_schema(self)
77 if am_rodc is not None:
78 dsdb._dsdb_set_am_rodc(self, am_rodc)
80 def connect(self, url=None, flags=0, options=None):
81 '''connect to the database'''
82 if self.lp is not None and not os.path.exists(url):
83 url = self.lp.private_path(url)
84 self.url = url
86 super(SamDB, self).connect(url=url, flags=flags,
87 options=options)
89 def am_rodc(self):
90 '''return True if we are an RODC'''
91 return dsdb._am_rodc(self)
93 def am_pdc(self):
94 '''return True if we are an PDC emulator'''
95 return dsdb._am_pdc(self)
97 def domain_dn(self):
98 '''return the domain DN'''
99 return str(self.get_default_basedn())
101 def schema_dn(self):
102 '''return the schema partition dn'''
103 return str(self.get_schema_basedn())
105 def disable_account(self, search_filter):
106 """Disables an account
108 :param search_filter: LDAP filter to find the user (eg
109 samccountname=name)
112 flags = samba.dsdb.UF_ACCOUNTDISABLE
113 self.toggle_userAccountFlags(search_filter, flags, on=True)
115 def enable_account(self, search_filter):
116 """Enables an account
118 :param search_filter: LDAP filter to find the user (eg
119 samccountname=name)
122 flags = samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_PASSWD_NOTREQD
123 self.toggle_userAccountFlags(search_filter, flags, on=False)
125 def toggle_userAccountFlags(self, search_filter, flags, flags_str=None,
126 on=True, strict=False):
127 """Toggle_userAccountFlags
129 :param search_filter: LDAP filter to find the user (eg
130 samccountname=name)
131 :param flags: samba.dsdb.UF_* flags
132 :param on: on=True (default) => set, on=False => unset
133 :param strict: strict=False (default) ignore if no action is needed
134 strict=True raises an Exception if...
136 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
137 expression=search_filter, attrs=["userAccountControl"])
138 if len(res) == 0:
139 raise Exception("Unable to find account where '%s'" % search_filter)
140 assert(len(res) == 1)
141 account_dn = res[0].dn
143 old_uac = int(res[0]["userAccountControl"][0])
144 if on:
145 if strict and (old_uac & flags):
146 error = "Account flag(s) '%s' already set" % flags_str
147 raise Exception(error)
149 new_uac = old_uac | flags
150 else:
151 if strict and not (old_uac & flags):
152 error = "Account flag(s) '%s' already unset" % flags_str
153 raise Exception(error)
155 new_uac = old_uac & ~flags
157 if old_uac == new_uac:
158 return
160 mod = """
161 dn: %s
162 changetype: modify
163 delete: userAccountControl
164 userAccountControl: %u
165 add: userAccountControl
166 userAccountControl: %u
167 """ % (account_dn, old_uac, new_uac)
168 self.modify_ldif(mod)
170 def force_password_change_at_next_login(self, search_filter):
171 """Forces a password change at next login
173 :param search_filter: LDAP filter to find the user (eg
174 samccountname=name)
176 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
177 expression=search_filter, attrs=[])
178 if len(res) == 0:
179 raise Exception('Unable to find user "%s"' % search_filter)
180 assert(len(res) == 1)
181 user_dn = res[0].dn
183 mod = """
184 dn: %s
185 changetype: modify
186 replace: pwdLastSet
187 pwdLastSet: 0
188 """ % (user_dn)
189 self.modify_ldif(mod)
191 def unlock_account(self, search_filter):
192 """Unlock a user account by resetting lockoutTime to 0.
193 This does also reset the badPwdCount to 0.
195 :param search_filter: LDAP filter to find the user (e.g.
196 sAMAccountName=username)
198 res = self.search(base=self.domain_dn(),
199 scope=ldb.SCOPE_SUBTREE,
200 expression=search_filter,
201 attrs=[])
202 if len(res) == 0:
203 raise SamDBNotFoundError('Unable to find user "%s"' % search_filter)
204 if len(res) != 1:
205 raise SamDBError('User "%s" is not unique' % search_filter)
206 user_dn = res[0].dn
208 mod = """
209 dn: %s
210 changetype: modify
211 replace: lockoutTime
212 lockoutTime: 0
213 """ % (user_dn)
214 self.modify_ldif(mod)
216 def newgroup(self, groupname, groupou=None, grouptype=None,
217 description=None, mailaddress=None, notes=None, sd=None,
218 gidnumber=None, nisdomain=None):
219 """Adds a new group with additional parameters
221 :param groupname: Name of the new group
222 :param grouptype: Type of the new group
223 :param description: Description of the new group
224 :param mailaddress: Email address of the new group
225 :param notes: Notes of the new group
226 :param gidnumber: GID Number of the new group
227 :param nisdomain: NIS Domain Name of the new group
228 :param sd: security descriptor of the object
231 if groupou:
232 group_dn = "CN=%s,%s,%s" % (groupname, groupou, self.domain_dn())
233 else:
234 group_dn = "CN=%s,%s" % (groupname, self.get_wellknown_dn(
235 self.get_default_basedn(),
236 dsdb.DS_GUID_USERS_CONTAINER))
238 # The new user record. Note the reliance on the SAMLDB module which
239 # fills in the default information
240 ldbmessage = {"dn": group_dn,
241 "sAMAccountName": groupname,
242 "objectClass": "group"}
244 if grouptype is not None:
245 ldbmessage["groupType"] = normalise_int32(grouptype)
247 if description is not None:
248 ldbmessage["description"] = description
250 if mailaddress is not None:
251 ldbmessage["mail"] = mailaddress
253 if notes is not None:
254 ldbmessage["info"] = notes
256 if gidnumber is not None:
257 ldbmessage["gidNumber"] = normalise_int32(gidnumber)
259 if nisdomain is not None:
260 ldbmessage["msSFU30Name"] = groupname
261 ldbmessage["msSFU30NisDomain"] = nisdomain
263 if sd is not None:
264 ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
266 self.add(ldbmessage)
268 def deletegroup(self, groupname):
269 """Deletes a group
271 :param groupname: Name of the target group
274 groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
275 self.transaction_start()
276 try:
277 targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
278 expression=groupfilter, attrs=[])
279 if len(targetgroup) == 0:
280 raise Exception('Unable to find group "%s"' % groupname)
281 assert(len(targetgroup) == 1)
282 self.delete(targetgroup[0].dn)
283 except:
284 self.transaction_cancel()
285 raise
286 else:
287 self.transaction_commit()
289 def group_member_filter(self, member, member_types):
290 filter = ""
292 all_member_types = [ 'user',
293 'group',
294 'computer',
295 'serviceaccount',
296 'contact',
299 if 'all' in member_types:
300 member_types = all_member_types
302 for member_type in member_types:
303 if member_type not in all_member_types:
304 raise Exception('Invalid group member type "%s". '
305 'Valid types are %s and all.' %
306 (member_type, ", ".join(all_member_types)))
308 if 'user' in member_types:
309 filter += ('(&(sAMAccountName=%s)(samAccountType=%d))' %
310 (ldb.binary_encode(member), dsdb.ATYPE_NORMAL_ACCOUNT))
311 if 'group' in member_types:
312 filter += ('(&(sAMAccountName=%s)'
313 '(objectClass=group)'
314 '(!(groupType:1.2.840.113556.1.4.803:=1)))' %
315 ldb.binary_encode(member))
316 if 'computer' in member_types:
317 samaccountname = member
318 if member[-1] != '$':
319 samaccountname = "%s$" % member
320 filter += ('(&(samAccountType=%d)'
321 '(!(objectCategory=msDS-ManagedServiceAccount))'
322 '(sAMAccountName=%s))' %
323 (dsdb.ATYPE_WORKSTATION_TRUST,
324 ldb.binary_encode(samaccountname)))
325 if 'serviceaccount' in member_types:
326 samaccountname = member
327 if member[-1] != '$':
328 samaccountname = "%s$" % member
329 filter += ('(&(samAccountType=%d)'
330 '(objectCategory=msDS-ManagedServiceAccount)'
331 '(sAMAccountName=%s))' %
332 (dsdb.ATYPE_WORKSTATION_TRUST,
333 ldb.binary_encode(samaccountname)))
334 if 'contact' in member_types:
335 filter += ('(&(objectCategory=Person)(!(objectSid=*))(name=%s))' %
336 ldb.binary_encode(member))
338 filter = "(|%s)" % filter
340 return filter
342 def add_remove_group_members(self, groupname, members,
343 add_members_operation=True,
344 member_types=[ 'user', 'group', 'computer' ],
345 member_base_dn=None):
346 """Adds or removes group members
348 :param groupname: Name of the target group
349 :param members: list of group members
350 :param add_members_operation: Defines if its an add or remove
351 operation
354 groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (
355 ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
357 self.transaction_start()
358 try:
359 targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
360 expression=groupfilter, attrs=['member'])
361 if len(targetgroup) == 0:
362 raise Exception('Unable to find group "%s"' % groupname)
363 assert(len(targetgroup) == 1)
365 modified = False
367 addtargettogroup = """
368 dn: %s
369 changetype: modify
370 """ % (str(targetgroup[0].dn))
372 for member in members:
373 targetmember_dn = None
374 if member_base_dn is None:
375 member_base_dn = self.domain_dn()
377 try:
378 membersid = security.dom_sid(member)
379 targetmember_dn = "<SID=%s>" % str(membersid)
380 except TypeError as e:
381 pass
383 if targetmember_dn is None:
384 try:
385 member_dn = ldb.Dn(self, member)
386 if member_dn.get_linearized() == member_dn.extended_str(1):
387 full_member_dn = self.normalize_dn_in_domain(member_dn)
388 else:
389 full_member_dn = member_dn
390 targetmember_dn = full_member_dn.extended_str(1)
391 except ValueError as e:
392 pass
394 if targetmember_dn is None:
395 filter = self.group_member_filter(member, member_types)
396 targetmember = self.search(base=member_base_dn,
397 scope=ldb.SCOPE_SUBTREE,
398 expression=filter,
399 attrs=[])
401 if len(targetmember) > 1:
402 targetmemberlist_str = ""
403 for msg in targetmember:
404 targetmemberlist_str += "%s\n" % msg.get("dn")
405 raise Exception('Found multiple results for "%s":\n%s' %
406 (member, targetmemberlist_str))
407 if len(targetmember) != 1:
408 raise Exception('Unable to find "%s". Operation cancelled.' % member)
409 targetmember_dn = targetmember[0].dn.extended_str(1)
411 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']]):
412 modified = True
413 addtargettogroup += """add: member
414 member: %s
415 """ % (str(targetmember_dn))
417 elif add_members_operation is False and (targetgroup[0].get('member') is not None and get_bytes(targetmember_dn) in targetgroup[0]['member']):
418 modified = True
419 addtargettogroup += """delete: member
420 member: %s
421 """ % (str(targetmember_dn))
423 if modified is True:
424 self.modify_ldif(addtargettogroup)
426 except:
427 self.transaction_cancel()
428 raise
429 else:
430 self.transaction_commit()
432 def prepare_attr_replace(self, msg, old, attr_name, value):
433 """Changes the MessageElement with the given attr_name of the
434 given Message. If the value is "" set an empty value and the flag
435 FLAG_MOD_DELETE, otherwise set the new value and FLAG_MOD_REPLACE.
436 If the value is None or the Message contains the attr_name with this
437 value, nothing will changed."""
438 # skip unchanged attribute
439 if value is None:
440 return
441 if attr_name in old and str(value) == str(old[attr_name]):
442 return
444 # remove attribute
445 if len(value) == 0:
446 if attr_name in old:
447 el = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr_name)
448 msg.add(el)
449 return
451 # change attribute
452 el = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, attr_name)
453 msg.add(el)
455 def fullname_from_names(self, given_name=None, initials=None, surname=None,
456 old_attrs={}, fallback_default=""):
457 """Prepares new combined fullname, using the name parts.
458 Used for things like displayName or cn.
459 Use the original name values, if no new one is specified."""
461 attrs = {"givenName": given_name,
462 "initials": initials,
463 "sn": surname}
465 # if the attribute is not specified, try to use the old one
466 for attr_name, attr_value in attrs.items():
467 if attr_value == None and attr_name in old_attrs:
468 attrs[attr_name] = str(old_attrs[attr_name])
470 # add '.' to initials if initals are not None and not "" and if the initials
471 # don't have already a '.' at the end
472 if attrs["initials"] and not attrs["initials"].endswith('.'):
473 attrs["initials"] += '.'
475 # remove empty values (None and '')
476 attrs_values = list(filter(None, attrs.values()))
478 # fullname is the combination of not-empty values as string, separated by ' '
479 fullname = ' '.join(attrs_values)
481 if fullname == '':
482 return fallback_default
484 return fullname
486 def newuser(self, username, password,
487 force_password_change_at_next_login_req=False,
488 useusernameascn=False, userou=None, surname=None, givenname=None,
489 initials=None, profilepath=None, scriptpath=None, homedrive=None,
490 homedirectory=None, jobtitle=None, department=None, company=None,
491 description=None, mailaddress=None, internetaddress=None,
492 telephonenumber=None, physicaldeliveryoffice=None, sd=None,
493 setpassword=True, uidnumber=None, gidnumber=None, gecos=None,
494 loginshell=None, uid=None, nisdomain=None, unixhome=None,
495 smartcard_required=False):
496 """Adds a new user with additional parameters
498 :param username: Name of the new user
499 :param password: Password for the new user
500 :param force_password_change_at_next_login_req: Force password change
501 :param useusernameascn: Use username as cn rather that firstname +
502 initials + lastname
503 :param userou: Object container (without domainDN postfix) for new user
504 :param surname: Surname of the new user
505 :param givenname: First name of the new user
506 :param initials: Initials of the new user
507 :param profilepath: Profile path of the new user
508 :param scriptpath: Logon script path of the new user
509 :param homedrive: Home drive of the new user
510 :param homedirectory: Home directory of the new user
511 :param jobtitle: Job title of the new user
512 :param department: Department of the new user
513 :param company: Company of the new user
514 :param description: of the new user
515 :param mailaddress: Email address of the new user
516 :param internetaddress: Home page of the new user
517 :param telephonenumber: Phone number of the new user
518 :param physicaldeliveryoffice: Office location of the new user
519 :param sd: security descriptor of the object
520 :param setpassword: optionally disable password reset
521 :param uidnumber: RFC2307 Unix numeric UID of the new user
522 :param gidnumber: RFC2307 Unix primary GID of the new user
523 :param gecos: RFC2307 Unix GECOS field of the new user
524 :param loginshell: RFC2307 Unix login shell of the new user
525 :param uid: RFC2307 Unix username of the new user
526 :param nisdomain: RFC2307 Unix NIS domain of the new user
527 :param unixhome: RFC2307 Unix home directory of the new user
528 :param smartcard_required: set the UF_SMARTCARD_REQUIRED bit of the new user
531 displayname = self.fullname_from_names(given_name=givenname,
532 initials=initials,
533 surname=surname)
534 cn = username
535 if useusernameascn is None and displayname != "":
536 cn = displayname
538 if userou:
539 user_dn = "CN=%s,%s,%s" % (cn, userou, self.domain_dn())
540 else:
541 user_dn = "CN=%s,%s" % (cn, self.get_wellknown_dn(
542 self.get_default_basedn(),
543 dsdb.DS_GUID_USERS_CONTAINER))
545 dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "")
546 user_principal_name = "%s@%s" % (username, dnsdomain)
547 # The new user record. Note the reliance on the SAMLDB module which
548 # fills in the default information
549 ldbmessage = {"dn": user_dn,
550 "sAMAccountName": username,
551 "userPrincipalName": user_principal_name,
552 "objectClass": "user"}
554 if smartcard_required:
555 ldbmessage["userAccountControl"] = str(dsdb.UF_NORMAL_ACCOUNT |
556 dsdb.UF_SMARTCARD_REQUIRED)
557 setpassword = False
559 if surname is not None:
560 ldbmessage["sn"] = surname
562 if givenname is not None:
563 ldbmessage["givenName"] = givenname
565 if displayname != "":
566 ldbmessage["displayName"] = displayname
567 ldbmessage["name"] = displayname
569 if initials is not None:
570 ldbmessage["initials"] = '%s.' % initials
572 if profilepath is not None:
573 ldbmessage["profilePath"] = profilepath
575 if scriptpath is not None:
576 ldbmessage["scriptPath"] = scriptpath
578 if homedrive is not None:
579 ldbmessage["homeDrive"] = homedrive
581 if homedirectory is not None:
582 ldbmessage["homeDirectory"] = homedirectory
584 if jobtitle is not None:
585 ldbmessage["title"] = jobtitle
587 if department is not None:
588 ldbmessage["department"] = department
590 if company is not None:
591 ldbmessage["company"] = company
593 if description is not None:
594 ldbmessage["description"] = description
596 if mailaddress is not None:
597 ldbmessage["mail"] = mailaddress
599 if internetaddress is not None:
600 ldbmessage["wWWHomePage"] = internetaddress
602 if telephonenumber is not None:
603 ldbmessage["telephoneNumber"] = telephonenumber
605 if physicaldeliveryoffice is not None:
606 ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
608 if sd is not None:
609 ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
611 ldbmessage2 = None
612 if any(map(lambda b: b is not None, (uid, uidnumber, gidnumber, gecos,
613 loginshell, nisdomain, unixhome))):
614 ldbmessage2 = ldb.Message()
615 ldbmessage2.dn = ldb.Dn(self, user_dn)
616 if uid is not None:
617 ldbmessage2["uid"] = ldb.MessageElement(str(uid), ldb.FLAG_MOD_REPLACE, 'uid')
618 if uidnumber is not None:
619 ldbmessage2["uidNumber"] = ldb.MessageElement(str(uidnumber), ldb.FLAG_MOD_REPLACE, 'uidNumber')
620 if gidnumber is not None:
621 ldbmessage2["gidNumber"] = ldb.MessageElement(str(gidnumber), ldb.FLAG_MOD_REPLACE, 'gidNumber')
622 if gecos is not None:
623 ldbmessage2["gecos"] = ldb.MessageElement(str(gecos), ldb.FLAG_MOD_REPLACE, 'gecos')
624 if loginshell is not None:
625 ldbmessage2["loginShell"] = ldb.MessageElement(str(loginshell), ldb.FLAG_MOD_REPLACE, 'loginShell')
626 if unixhome is not None:
627 ldbmessage2["unixHomeDirectory"] = ldb.MessageElement(
628 str(unixhome), ldb.FLAG_MOD_REPLACE, 'unixHomeDirectory')
629 if nisdomain is not None:
630 ldbmessage2["msSFU30NisDomain"] = ldb.MessageElement(
631 str(nisdomain), ldb.FLAG_MOD_REPLACE, 'msSFU30NisDomain')
632 ldbmessage2["msSFU30Name"] = ldb.MessageElement(
633 str(username), ldb.FLAG_MOD_REPLACE, 'msSFU30Name')
634 ldbmessage2["unixUserPassword"] = ldb.MessageElement(
635 'ABCD!efgh12345$67890', ldb.FLAG_MOD_REPLACE,
636 'unixUserPassword')
638 self.transaction_start()
639 try:
640 self.add(ldbmessage)
641 if ldbmessage2:
642 self.modify(ldbmessage2)
644 # Sets the password for it
645 if setpassword:
646 self.setpassword(("(distinguishedName=%s)" %
647 ldb.binary_encode(user_dn)),
648 password,
649 force_password_change_at_next_login_req)
650 except:
651 self.transaction_cancel()
652 raise
653 else:
654 self.transaction_commit()
656 def newcontact(self,
657 fullcontactname=None,
658 ou=None,
659 surname=None,
660 givenname=None,
661 initials=None,
662 displayname=None,
663 jobtitle=None,
664 department=None,
665 company=None,
666 description=None,
667 mailaddress=None,
668 internetaddress=None,
669 telephonenumber=None,
670 mobilenumber=None,
671 physicaldeliveryoffice=None):
672 """Adds a new contact with additional parameters
674 :param fullcontactname: Optional full name of the new contact
675 :param ou: Object container for new contact
676 :param surname: Surname of the new contact
677 :param givenname: First name of the new contact
678 :param initials: Initials of the new contact
679 :param displayname: displayName of the new contact
680 :param jobtitle: Job title of the new contact
681 :param department: Department of the new contact
682 :param company: Company of the new contact
683 :param description: Description of the new contact
684 :param mailaddress: Email address of the new contact
685 :param internetaddress: Home page of the new contact
686 :param telephonenumber: Phone number of the new contact
687 :param mobilenumber: Primary mobile number of the new contact
688 :param physicaldeliveryoffice: Office location of the new contact
691 # Prepare the contact name like the RSAT, using the name parts.
692 cn = self.fullname_from_names(given_name=givenname,
693 initials=initials,
694 surname=surname)
696 # Use the specified fullcontactname instead of the previously prepared
697 # contact name, if it is specified.
698 # This is similar to the "Full name" value of the RSAT.
699 if fullcontactname is not None:
700 cn = fullcontactname
702 if fullcontactname is None and cn == "":
703 raise Exception('No name for contact specified')
705 contactcontainer_dn = self.domain_dn()
706 if ou:
707 contactcontainer_dn = self.normalize_dn_in_domain(ou)
709 contact_dn = "CN=%s,%s" % (cn, contactcontainer_dn)
711 ldbmessage = {"dn": contact_dn,
712 "objectClass": "contact",
715 if surname is not None:
716 ldbmessage["sn"] = surname
718 if givenname is not None:
719 ldbmessage["givenName"] = givenname
721 if displayname is not None:
722 ldbmessage["displayName"] = displayname
724 if initials is not None:
725 ldbmessage["initials"] = '%s.' % initials
727 if jobtitle is not None:
728 ldbmessage["title"] = jobtitle
730 if department is not None:
731 ldbmessage["department"] = department
733 if company is not None:
734 ldbmessage["company"] = company
736 if description is not None:
737 ldbmessage["description"] = description
739 if mailaddress is not None:
740 ldbmessage["mail"] = mailaddress
742 if internetaddress is not None:
743 ldbmessage["wWWHomePage"] = internetaddress
745 if telephonenumber is not None:
746 ldbmessage["telephoneNumber"] = telephonenumber
748 if mobilenumber is not None:
749 ldbmessage["mobile"] = mobilenumber
751 if physicaldeliveryoffice is not None:
752 ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
754 self.add(ldbmessage)
756 return cn
758 def newcomputer(self, computername, computerou=None, description=None,
759 prepare_oldjoin=False, ip_address_list=None,
760 service_principal_name_list=None):
761 """Adds a new user with additional parameters
763 :param computername: Name of the new computer
764 :param computerou: Object container for new computer
765 :param description: Description of the new computer
766 :param prepare_oldjoin: Preset computer password for oldjoin mechanism
767 :param ip_address_list: ip address list for DNS A or AAAA record
768 :param service_principal_name_list: string list of servicePincipalName
771 cn = re.sub(r"\$$", "", computername)
772 if cn.count('$'):
773 raise Exception('Illegal computername "%s"' % computername)
774 samaccountname = "%s$" % cn
776 computercontainer_dn = self.get_wellknown_dn(self.get_default_basedn(),
777 dsdb.DS_GUID_COMPUTERS_CONTAINER)
778 if computerou:
779 computercontainer_dn = self.normalize_dn_in_domain(computerou)
781 computer_dn = "CN=%s,%s" % (cn, computercontainer_dn)
783 ldbmessage = {"dn": computer_dn,
784 "sAMAccountName": samaccountname,
785 "objectClass": "computer",
788 if description is not None:
789 ldbmessage["description"] = description
791 if service_principal_name_list:
792 ldbmessage["servicePrincipalName"] = service_principal_name_list
794 accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
795 dsdb.UF_ACCOUNTDISABLE)
796 if prepare_oldjoin:
797 accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
798 ldbmessage["userAccountControl"] = accountcontrol
800 if ip_address_list:
801 ldbmessage['dNSHostName'] = '{}.{}'.format(
802 cn, self.domain_dns_name())
804 self.transaction_start()
805 try:
806 self.add(ldbmessage)
808 if prepare_oldjoin:
809 password = cn.lower()
810 self.setpassword(("(distinguishedName=%s)" %
811 ldb.binary_encode(computer_dn)),
812 password, False)
813 except:
814 self.transaction_cancel()
815 raise
816 else:
817 self.transaction_commit()
819 def deleteuser(self, username):
820 """Deletes a user
822 :param username: Name of the target user
825 filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn())
826 self.transaction_start()
827 try:
828 target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
829 expression=filter, attrs=[])
830 if len(target) == 0:
831 raise Exception('Unable to find user "%s"' % username)
832 assert(len(target) == 1)
833 self.delete(target[0].dn)
834 except:
835 self.transaction_cancel()
836 raise
837 else:
838 self.transaction_commit()
840 def setpassword(self, search_filter, password,
841 force_change_at_next_login=False, username=None):
842 """Sets the password for a user
844 :param search_filter: LDAP filter to find the user (eg
845 samccountname=name)
846 :param password: Password for the user
847 :param force_change_at_next_login: Force password change
849 self.transaction_start()
850 try:
851 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
852 expression=search_filter, attrs=[])
853 if len(res) == 0:
854 raise Exception('Unable to find user "%s"' % (username or search_filter))
855 if len(res) > 1:
856 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter))
857 user_dn = res[0].dn
858 if not isinstance(password, str):
859 pw = password.decode('utf-8')
860 else:
861 pw = password
862 pw = ('"' + pw + '"').encode('utf-16-le')
863 setpw = """
864 dn: %s
865 changetype: modify
866 replace: unicodePwd
867 unicodePwd:: %s
868 """ % (user_dn, base64.b64encode(pw).decode('utf-8'))
870 self.modify_ldif(setpw)
872 if force_change_at_next_login:
873 self.force_password_change_at_next_login(
874 "(distinguishedName=" + str(user_dn) + ")")
876 # modify the userAccountControl to remove the disabled bit
877 self.enable_account(search_filter)
878 except:
879 self.transaction_cancel()
880 raise
881 else:
882 self.transaction_commit()
884 def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False):
885 """Sets the account expiry for a user
887 :param search_filter: LDAP filter to find the user (eg
888 samaccountname=name)
889 :param expiry_seconds: expiry time from now in seconds
890 :param no_expiry_req: if set, then don't expire password
892 self.transaction_start()
893 try:
894 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
895 expression=search_filter,
896 attrs=["userAccountControl", "accountExpires"])
897 if len(res) == 0:
898 raise Exception('Unable to find user "%s"' % search_filter)
899 assert(len(res) == 1)
900 user_dn = res[0].dn
902 userAccountControl = int(res[0]["userAccountControl"][0])
903 accountExpires = int(res[0]["accountExpires"][0])
904 if no_expiry_req:
905 userAccountControl = userAccountControl | 0x10000
906 accountExpires = 0
907 else:
908 userAccountControl = userAccountControl & ~0x10000
909 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
911 setexp = """
912 dn: %s
913 changetype: modify
914 replace: userAccountControl
915 userAccountControl: %u
916 replace: accountExpires
917 accountExpires: %u
918 """ % (user_dn, userAccountControl, accountExpires)
920 self.modify_ldif(setexp)
921 except:
922 self.transaction_cancel()
923 raise
924 else:
925 self.transaction_commit()
927 def set_domain_sid(self, sid):
928 """Change the domain SID used by this LDB.
930 :param sid: The new domain sid to use.
932 dsdb._samdb_set_domain_sid(self, sid)
934 def get_domain_sid(self):
935 """Read the domain SID used by this LDB. """
936 return dsdb._samdb_get_domain_sid(self)
938 domain_sid = property(get_domain_sid, set_domain_sid,
939 doc="SID for the domain")
941 def set_invocation_id(self, invocation_id):
942 """Set the invocation id for this SamDB handle.
944 :param invocation_id: GUID of the invocation id.
946 dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
948 def get_invocation_id(self):
949 """Get the invocation_id id"""
950 return dsdb._samdb_ntds_invocation_id(self)
952 invocation_id = property(get_invocation_id, set_invocation_id,
953 doc="Invocation ID GUID")
955 def get_oid_from_attid(self, attid):
956 return dsdb._dsdb_get_oid_from_attid(self, attid)
958 def get_attid_from_lDAPDisplayName(self, ldap_display_name,
959 is_schema_nc=False):
960 '''return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI'''
961 return dsdb._dsdb_get_attid_from_lDAPDisplayName(self,
962 ldap_display_name, is_schema_nc)
964 def get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name):
965 '''return the syntax OID for a LDAP attribute as a string'''
966 return dsdb._dsdb_get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name)
968 def get_systemFlags_from_lDAPDisplayName(self, ldap_display_name):
969 '''return the systemFlags for a LDAP attribute as a integer'''
970 return dsdb._dsdb_get_systemFlags_from_lDAPDisplayName(self, ldap_display_name)
972 def get_linkId_from_lDAPDisplayName(self, ldap_display_name):
973 '''return the linkID for a LDAP attribute as a integer'''
974 return dsdb._dsdb_get_linkId_from_lDAPDisplayName(self, ldap_display_name)
976 def get_lDAPDisplayName_by_attid(self, attid):
977 '''return the lDAPDisplayName from an integer DRS attribute ID'''
978 return dsdb._dsdb_get_lDAPDisplayName_by_attid(self, attid)
980 def get_backlink_from_lDAPDisplayName(self, ldap_display_name):
981 '''return the attribute name of the corresponding backlink from the name
982 of a forward link attribute. If there is no backlink return None'''
983 return dsdb._dsdb_get_backlink_from_lDAPDisplayName(self, ldap_display_name)
985 def set_ntds_settings_dn(self, ntds_settings_dn):
986 """Set the NTDS Settings DN, as would be returned on the dsServiceName
987 rootDSE attribute.
989 This allows the DN to be set before the database fully exists
991 :param ntds_settings_dn: The new DN to use
993 dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
995 def get_ntds_GUID(self):
996 """Get the NTDS objectGUID"""
997 return dsdb._samdb_ntds_objectGUID(self)
999 def get_timestr(self):
1000 """Get the current time as generalized time string"""
1001 res = self.search(base="",
1002 scope=ldb.SCOPE_BASE,
1003 attrs=["currentTime"])
1004 return str(res[0]["currentTime"][0])
1006 def get_time(self):
1007 """Get the current time as UNIX time"""
1008 return ldb.string_to_time(self.get_timestr())
1010 def get_nttime(self):
1011 """Get the current time as NT time"""
1012 return samba.unix2nttime(self.get_time())
1014 def server_site_name(self):
1015 """Get the server site name"""
1016 return dsdb._samdb_server_site_name(self)
1018 def host_dns_name(self):
1019 """return the DNS name of this host"""
1020 res = self.search(base='', scope=ldb.SCOPE_BASE, attrs=['dNSHostName'])
1021 return str(res[0]['dNSHostName'][0])
1023 def domain_dns_name(self):
1024 """return the DNS name of the domain root"""
1025 domain_dn = self.get_default_basedn()
1026 return domain_dn.canonical_str().split('/')[0]
1028 def domain_netbios_name(self):
1029 """return the NetBIOS name of the domain root"""
1030 domain_dn = self.get_default_basedn()
1031 dns_name = self.domain_dns_name()
1032 filter = "(&(objectClass=crossRef)(nETBIOSName=*)(ncName=%s)(dnsroot=%s))" % (domain_dn, dns_name)
1033 partitions_dn = self.get_partitions_dn()
1034 res = self.search(partitions_dn,
1035 scope=ldb.SCOPE_ONELEVEL,
1036 expression=filter)
1037 try:
1038 netbios_domain = res[0]["nETBIOSName"][0].decode()
1039 except IndexError:
1040 return None
1041 return netbios_domain
1043 def forest_dns_name(self):
1044 """return the DNS name of the forest root"""
1045 forest_dn = self.get_root_basedn()
1046 return forest_dn.canonical_str().split('/')[0]
1048 def load_partition_usn(self, base_dn):
1049 return dsdb._dsdb_load_partition_usn(self, base_dn)
1051 def set_schema(self, schema, write_indices_and_attributes=True):
1052 self.set_schema_from_ldb(schema.ldb, write_indices_and_attributes=write_indices_and_attributes)
1054 def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True):
1055 dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes)
1057 def set_schema_update_now(self):
1058 ldif = """
1060 changetype: modify
1061 add: schemaUpdateNow
1062 schemaUpdateNow: 1
1064 self.modify_ldif(ldif)
1066 def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
1067 '''convert a list of attribute values to a DRSUAPI DsReplicaAttribute'''
1068 return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
1070 def dsdb_normalise_attributes(self, ldb, ldap_display_name, ldif_elements):
1071 '''normalise a list of attribute values'''
1072 return dsdb._dsdb_normalise_attributes(ldb, ldap_display_name, ldif_elements)
1074 def get_attribute_from_attid(self, attid):
1075 """ Get from an attid the associated attribute
1077 :param attid: The attribute id for searched attribute
1078 :return: The name of the attribute associated with this id
1080 if len(self.hash_oid_name.keys()) == 0:
1081 self._populate_oid_attid()
1082 if self.get_oid_from_attid(attid) in self.hash_oid_name:
1083 return self.hash_oid_name[self.get_oid_from_attid(attid)]
1084 else:
1085 return None
1087 def _populate_oid_attid(self):
1088 """Populate the hash hash_oid_name.
1090 This hash contains the oid of the attribute as a key and
1091 its display name as a value
1093 self.hash_oid_name = {}
1094 res = self.search(expression="objectClass=attributeSchema",
1095 controls=["search_options:1:2"],
1096 attrs=["attributeID",
1097 "lDAPDisplayName"])
1098 if len(res) > 0:
1099 for e in res:
1100 strDisplay = str(e.get("lDAPDisplayName"))
1101 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
1103 def get_attribute_replmetadata_version(self, dn, att):
1104 """Get the version field trom the replPropertyMetaData for
1105 the given field
1107 :param dn: The on which we want to get the version
1108 :param att: The name of the attribute
1109 :return: The value of the version field in the replPropertyMetaData
1110 for the given attribute. None if the attribute is not replicated
1113 res = self.search(expression="distinguishedName=%s" % dn,
1114 scope=ldb.SCOPE_SUBTREE,
1115 controls=["search_options:1:2"],
1116 attrs=["replPropertyMetaData"])
1117 if len(res) == 0:
1118 return None
1120 repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
1121 res[0]["replPropertyMetaData"][0])
1122 ctr = repl.ctr
1123 if len(self.hash_oid_name.keys()) == 0:
1124 self._populate_oid_attid()
1125 for o in ctr.array:
1126 # Search for Description
1127 att_oid = self.get_oid_from_attid(o.attid)
1128 if att_oid in self.hash_oid_name and\
1129 att.lower() == self.hash_oid_name[att_oid].lower():
1130 return o.version
1131 return None
1133 def set_attribute_replmetadata_version(self, dn, att, value,
1134 addifnotexist=False):
1135 res = self.search(expression="distinguishedName=%s" % dn,
1136 scope=ldb.SCOPE_SUBTREE,
1137 controls=["search_options:1:2"],
1138 attrs=["replPropertyMetaData"])
1139 if len(res) == 0:
1140 return None
1142 repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
1143 res[0]["replPropertyMetaData"][0])
1144 ctr = repl.ctr
1145 now = samba.unix2nttime(int(time.time()))
1146 found = False
1147 if len(self.hash_oid_name.keys()) == 0:
1148 self._populate_oid_attid()
1149 for o in ctr.array:
1150 # Search for Description
1151 att_oid = self.get_oid_from_attid(o.attid)
1152 if att_oid in self.hash_oid_name and\
1153 att.lower() == self.hash_oid_name[att_oid].lower():
1154 found = True
1155 seq = self.sequence_number(ldb.SEQ_NEXT)
1156 o.version = value
1157 o.originating_change_time = now
1158 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
1159 o.originating_usn = seq
1160 o.local_usn = seq
1162 if not found and addifnotexist and len(ctr.array) > 0:
1163 o2 = drsblobs.replPropertyMetaData1()
1164 o2.attid = 589914
1165 att_oid = self.get_oid_from_attid(o2.attid)
1166 seq = self.sequence_number(ldb.SEQ_NEXT)
1167 o2.version = value
1168 o2.originating_change_time = now
1169 o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
1170 o2.originating_usn = seq
1171 o2.local_usn = seq
1172 found = True
1173 tab = ctr.array
1174 tab.append(o2)
1175 ctr.count = ctr.count + 1
1176 ctr.array = tab
1178 if found:
1179 replBlob = ndr_pack(repl)
1180 msg = ldb.Message()
1181 msg.dn = res[0].dn
1182 msg["replPropertyMetaData"] = \
1183 ldb.MessageElement(replBlob,
1184 ldb.FLAG_MOD_REPLACE,
1185 "replPropertyMetaData")
1186 self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
1188 def write_prefixes_from_schema(self):
1189 dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
1191 def get_partitions_dn(self):
1192 return dsdb._dsdb_get_partitions_dn(self)
1194 def get_nc_root(self, dn):
1195 return dsdb._dsdb_get_nc_root(self, dn)
1197 def get_wellknown_dn(self, nc_root, wkguid):
1198 h_nc = self.hash_well_known.get(str(nc_root))
1199 dn = None
1200 if h_nc is not None:
1201 dn = h_nc.get(wkguid)
1202 if dn is None:
1203 dn = dsdb._dsdb_get_wellknown_dn(self, nc_root, wkguid)
1204 if dn is None:
1205 return dn
1206 if h_nc is None:
1207 self.hash_well_known[str(nc_root)] = {}
1208 h_nc = self.hash_well_known[str(nc_root)]
1209 h_nc[wkguid] = dn
1210 return dn
1212 def set_minPwdAge(self, value):
1213 if not isinstance(value, bytes):
1214 value = str(value).encode('utf8')
1215 m = ldb.Message()
1216 m.dn = ldb.Dn(self, self.domain_dn())
1217 m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
1218 self.modify(m)
1220 def get_minPwdAge(self):
1221 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
1222 if len(res) == 0:
1223 return None
1224 elif "minPwdAge" not in res[0]:
1225 return None
1226 else:
1227 return int(res[0]["minPwdAge"][0])
1229 def set_maxPwdAge(self, value):
1230 if not isinstance(value, bytes):
1231 value = str(value).encode('utf8')
1232 m = ldb.Message()
1233 m.dn = ldb.Dn(self, self.domain_dn())
1234 m["maxPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "maxPwdAge")
1235 self.modify(m)
1237 def get_maxPwdAge(self):
1238 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["maxPwdAge"])
1239 if len(res) == 0:
1240 return None
1241 elif "maxPwdAge" not in res[0]:
1242 return None
1243 else:
1244 return int(res[0]["maxPwdAge"][0])
1246 def set_minPwdLength(self, value):
1247 if not isinstance(value, bytes):
1248 value = str(value).encode('utf8')
1249 m = ldb.Message()
1250 m.dn = ldb.Dn(self, self.domain_dn())
1251 m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength")
1252 self.modify(m)
1254 def get_minPwdLength(self):
1255 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"])
1256 if len(res) == 0:
1257 return None
1258 elif "minPwdLength" not in res[0]:
1259 return None
1260 else:
1261 return int(res[0]["minPwdLength"][0])
1263 def set_pwdProperties(self, value):
1264 if not isinstance(value, bytes):
1265 value = str(value).encode('utf8')
1266 m = ldb.Message()
1267 m.dn = ldb.Dn(self, self.domain_dn())
1268 m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties")
1269 self.modify(m)
1271 def get_pwdProperties(self):
1272 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"])
1273 if len(res) == 0:
1274 return None
1275 elif "pwdProperties" not in res[0]:
1276 return None
1277 else:
1278 return int(res[0]["pwdProperties"][0])
1280 def set_dsheuristics(self, dsheuristics):
1281 m = ldb.Message()
1282 m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
1283 % self.get_config_basedn().get_linearized())
1284 if dsheuristics is not None:
1285 m["dSHeuristics"] = \
1286 ldb.MessageElement(dsheuristics,
1287 ldb.FLAG_MOD_REPLACE,
1288 "dSHeuristics")
1289 else:
1290 m["dSHeuristics"] = \
1291 ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
1292 "dSHeuristics")
1293 self.modify(m)
1295 def get_dsheuristics(self):
1296 res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
1297 % self.get_config_basedn().get_linearized(),
1298 scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
1299 if len(res) == 0:
1300 dsheuristics = None
1301 elif "dSHeuristics" in res[0]:
1302 dsheuristics = res[0]["dSHeuristics"][0]
1303 else:
1304 dsheuristics = None
1306 return dsheuristics
1308 def create_ou(self, ou_dn, description=None, name=None, sd=None):
1309 """Creates an organizationalUnit object
1310 :param ou_dn: dn of the new object
1311 :param description: description attribute
1312 :param name: name atttribute
1313 :param sd: security descriptor of the object, can be
1314 an SDDL string or security.descriptor type
1316 m = {"dn": ou_dn,
1317 "objectClass": "organizationalUnit"}
1319 if description:
1320 m["description"] = description
1321 if name:
1322 m["name"] = name
1324 if sd:
1325 m["nTSecurityDescriptor"] = ndr_pack(sd)
1326 self.add(m)
1328 def sequence_number(self, seq_type):
1329 """Returns the value of the sequence number according to the requested type
1330 :param seq_type: type of sequence number
1332 self.transaction_start()
1333 try:
1334 seq = super(SamDB, self).sequence_number(seq_type)
1335 except:
1336 self.transaction_cancel()
1337 raise
1338 else:
1339 self.transaction_commit()
1340 return seq
1342 def get_dsServiceName(self):
1343 '''get the NTDS DN from the rootDSE'''
1344 res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
1345 return str(res[0]["dsServiceName"][0])
1347 def get_serverName(self):
1348 '''get the server DN from the rootDSE'''
1349 res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["serverName"])
1350 return str(res[0]["serverName"][0])
1352 def dns_lookup(self, dns_name, dns_partition=None):
1353 '''Do a DNS lookup in the database, returns the NDR database structures'''
1354 if dns_partition is None:
1355 return dsdb_dns.lookup(self, dns_name)
1356 else:
1357 return dsdb_dns.lookup(self, dns_name,
1358 dns_partition=dns_partition)
1360 def dns_extract(self, el):
1361 '''Return the NDR database structures from a dnsRecord element'''
1362 return dsdb_dns.extract(self, el)
1364 def dns_replace(self, dns_name, new_records):
1365 '''Do a DNS modification on the database, sets the NDR database
1366 structures on a DNS name
1368 return dsdb_dns.replace(self, dns_name, new_records)
1370 def dns_replace_by_dn(self, dn, new_records):
1371 '''Do a DNS modification on the database, sets the NDR database
1372 structures on a LDB DN
1374 This routine is important because if the last record on the DN
1375 is removed, this routine will put a tombstone in the record.
1377 return dsdb_dns.replace_by_dn(self, dn, new_records)
1379 def garbage_collect_tombstones(self, dn, current_time,
1380 tombstone_lifetime=None):
1381 '''garbage_collect_tombstones(lp, samdb, [dn], current_time, tombstone_lifetime)
1382 -> (num_objects_expunged, num_links_expunged)'''
1384 if not is_ad_dc_built():
1385 raise SamDBError('Cannot garbage collect tombstones: ' \
1386 'AD DC was not built')
1388 if tombstone_lifetime is None:
1389 return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1390 current_time)
1391 else:
1392 return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1393 current_time,
1394 tombstone_lifetime)
1396 def create_own_rid_set(self):
1397 '''create a RID set for this DSA'''
1398 return dsdb._dsdb_create_own_rid_set(self)
1400 def allocate_rid(self):
1401 '''return a new RID from the RID Pool on this DSA'''
1402 return dsdb._dsdb_allocate_rid(self)
1404 def next_free_rid(self):
1405 '''return the next free RID from the RID Pool on this DSA.
1407 :note: This function is not intended for general use, and care must be
1408 taken if it is used to generate objectSIDs. The returned RID is not
1409 formally reserved for use, creating the possibility of duplicate
1410 objectSIDs.
1412 rid, _ = self.free_rid_bounds()
1413 return rid
1415 def free_rid_bounds(self):
1416 '''return the low and high bounds (inclusive) of RIDs that are
1417 available for use in this DSA's current RID pool.
1419 :note: This function is not intended for general use, and care must be
1420 taken if it is used to generate objectSIDs. The returned range of
1421 RIDs is not formally reserved for use, creating the possibility of
1422 duplicate objectSIDs.
1424 # Get DN of this server's RID Set
1425 server_name_dn = ldb.Dn(self, self.get_serverName())
1426 res = self.search(base=server_name_dn,
1427 scope=ldb.SCOPE_BASE,
1428 attrs=["serverReference"])
1429 try:
1430 server_ref = res[0]["serverReference"]
1431 except KeyError:
1432 raise ldb.LdbError(
1433 ldb.ERR_NO_SUCH_ATTRIBUTE,
1434 "No RID Set DN - "
1435 "Cannot find attribute serverReference of %s "
1436 "to calculate reference dn" % server_name_dn) from None
1437 server_ref_dn = ldb.Dn(self, server_ref[0].decode("utf-8"))
1439 res = self.search(base=server_ref_dn,
1440 scope=ldb.SCOPE_BASE,
1441 attrs=["rIDSetReferences"])
1442 try:
1443 rid_set_refs = res[0]["rIDSetReferences"]
1444 except KeyError:
1445 raise ldb.LdbError(
1446 ldb.ERR_NO_SUCH_ATTRIBUTE,
1447 "No RID Set DN - "
1448 "Cannot find attribute rIDSetReferences of %s "
1449 "to calculate reference dn" % server_ref_dn) from None
1450 rid_set_dn = ldb.Dn(self, rid_set_refs[0].decode("utf-8"))
1452 # Get the alloc pools and next RID of this RID Set
1453 res = self.search(base=rid_set_dn,
1454 scope=ldb.SCOPE_BASE,
1455 attrs=["rIDAllocationPool",
1456 "rIDPreviousAllocationPool",
1457 "rIDNextRID"])
1459 uint32_max = 2**32 - 1
1460 uint64_max = 2**64 - 1
1462 try:
1463 alloc_pool = int(res[0]["rIDAllocationPool"][0])
1464 except KeyError:
1465 alloc_pool = uint64_max
1466 if alloc_pool == uint64_max:
1467 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1468 "Bad RID Set %s" % rid_set_dn)
1470 try:
1471 prev_pool = int(res[0]["rIDPreviousAllocationPool"][0])
1472 except KeyError:
1473 prev_pool = uint64_max
1474 try:
1475 next_rid = int(res[0]["rIDNextRID"][0])
1476 except KeyError:
1477 next_rid = uint32_max
1479 # If we never used a pool, set up our first pool
1480 if prev_pool == uint64_max or next_rid == uint32_max:
1481 prev_pool = alloc_pool
1482 next_rid = prev_pool & uint32_max
1483 else:
1484 next_rid += 1
1486 # Now check if our current pool is still usable
1487 prev_pool_lo = prev_pool & uint32_max
1488 prev_pool_hi = prev_pool >> 32
1489 if next_rid > prev_pool_hi:
1490 # We need a new pool, check if we already have a new one
1491 # Otherwise we return an error code.
1492 if alloc_pool == prev_pool:
1493 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1494 "RID pools out of RIDs")
1496 # Now use the new pool
1497 prev_pool = alloc_pool
1498 prev_pool_lo = prev_pool & uint32_max
1499 prev_pool_hi = prev_pool >> 32
1500 next_rid = prev_pool_lo
1502 if next_rid < prev_pool_lo or next_rid > prev_pool_hi:
1503 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1504 "Bad RID chosen %d from range %d-%d" %
1505 (next_rid, prev_pool_lo, prev_pool_hi))
1507 return next_rid, prev_pool_hi
1509 def normalize_dn_in_domain(self, dn):
1510 '''return a new DN expanded by adding the domain DN
1512 If the dn is already a child of the domain DN, just
1513 return it as-is.
1515 :param dn: relative dn
1517 domain_dn = ldb.Dn(self, self.domain_dn())
1519 if isinstance(dn, ldb.Dn):
1520 dn = str(dn)
1522 full_dn = ldb.Dn(self, dn)
1523 if not full_dn.is_child_of(domain_dn):
1524 full_dn.add_base(domain_dn)
1525 return full_dn
1527 class dsdb_Dn(object):
1528 '''a class for binary DN'''
1530 def __init__(self, samdb, dnstring, syntax_oid=None):
1531 '''create a dsdb_Dn'''
1532 if syntax_oid is None:
1533 # auto-detect based on string
1534 if dnstring.startswith("B:"):
1535 syntax_oid = dsdb.DSDB_SYNTAX_BINARY_DN
1536 elif dnstring.startswith("S:"):
1537 syntax_oid = dsdb.DSDB_SYNTAX_STRING_DN
1538 else:
1539 syntax_oid = dsdb.DSDB_SYNTAX_OR_NAME
1540 if syntax_oid in [dsdb.DSDB_SYNTAX_BINARY_DN, dsdb.DSDB_SYNTAX_STRING_DN]:
1541 # it is a binary DN
1542 colons = dnstring.split(':')
1543 if len(colons) < 4:
1544 raise RuntimeError("Invalid DN %s" % dnstring)
1545 prefix_len = 4 + len(colons[1]) + int(colons[1])
1546 self.prefix = dnstring[0:prefix_len]
1547 self.binary = self.prefix[3 + len(colons[1]):-1]
1548 self.dnstring = dnstring[prefix_len:]
1549 else:
1550 self.dnstring = dnstring
1551 self.prefix = ''
1552 self.binary = ''
1553 self.dn = ldb.Dn(samdb, self.dnstring)
1555 def __str__(self):
1556 return self.prefix + str(self.dn.extended_str(mode=1))
1558 def __cmp__(self, other):
1559 ''' compare dsdb_Dn values similar to parsed_dn_compare()'''
1560 dn1 = self
1561 dn2 = other
1562 guid1 = dn1.dn.get_extended_component("GUID")
1563 guid2 = dn2.dn.get_extended_component("GUID")
1565 v = cmp(guid1, guid2)
1566 if v != 0:
1567 return v
1568 v = cmp(dn1.binary, dn2.binary)
1569 return v
1571 # In Python3, __cmp__ is replaced by these 6 methods
1572 def __eq__(self, other):
1573 return self.__cmp__(other) == 0
1575 def __ne__(self, other):
1576 return self.__cmp__(other) != 0
1578 def __lt__(self, other):
1579 return self.__cmp__(other) < 0
1581 def __le__(self, other):
1582 return self.__cmp__(other) <= 0
1584 def __gt__(self, other):
1585 return self.__cmp__(other) > 0
1587 def __ge__(self, other):
1588 return self.__cmp__(other) >= 0
1590 def get_binary_integer(self):
1591 '''return binary part of a dsdb_Dn as an integer, or None'''
1592 if self.prefix == '':
1593 return None
1594 return int(self.binary, 16)
1596 def get_bytes(self):
1597 '''return binary as a byte string'''
1598 return binascii.unhexlify(self.binary)