s4:kdc: adjust formatting of samba_kdc_update_pac() documentation
[Samba.git] / python / samba / samdb.py
bloba7fbbe359af0c5402f298ac3a0370921647a8c4a
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 class _CleanUpOnError:
59 def __init__(self, samdb, dn):
60 self.samdb = samdb
61 self.dn = dn
63 def __enter__(self):
64 pass
66 def __exit__(self, exc_type, exc_val, exc_tb):
67 if exc_type is not None:
68 # We failed to modify the account. If we connected to the
69 # database over LDAP, we don't have transactions, and so when
70 # we call transaction_cancel(), the account will still exist in
71 # a half-created state. We'll delete the account to ensure that
72 # doesn't happen.
73 self.samdb.delete(self.dn)
75 # Don't suppress any exceptions
76 return False
78 def __init__(self, url=None, lp=None, modules_dir=None, session_info=None,
79 credentials=None, flags=ldb.FLG_DONT_CREATE_DB,
80 options=None, global_schema=True,
81 auto_connect=True, am_rodc=None):
82 self.lp = lp
83 if not auto_connect:
84 url = None
85 elif url is None and lp is not None:
86 url = lp.samdb_url()
88 self.url = url
90 super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir,
91 session_info=session_info, credentials=credentials, flags=flags,
92 options=options)
94 if global_schema:
95 dsdb._dsdb_set_global_schema(self)
97 if am_rodc is not None:
98 dsdb._dsdb_set_am_rodc(self, am_rodc)
100 def connect(self, url=None, flags=0, options=None):
101 '''connect to the database'''
102 if self.lp is not None and not os.path.exists(url):
103 url = self.lp.private_path(url)
104 self.url = url
106 super(SamDB, self).connect(url=url, flags=flags,
107 options=options)
109 def am_rodc(self):
110 '''return True if we are an RODC'''
111 return dsdb._am_rodc(self)
113 def am_pdc(self):
114 '''return True if we are an PDC emulator'''
115 return dsdb._am_pdc(self)
117 def domain_dn(self):
118 '''return the domain DN'''
119 return str(self.get_default_basedn())
121 def schema_dn(self):
122 '''return the schema partition dn'''
123 return str(self.get_schema_basedn())
125 def disable_account(self, search_filter):
126 """Disables an account
128 :param search_filter: LDAP filter to find the user (eg
129 samccountname=name)
132 flags = samba.dsdb.UF_ACCOUNTDISABLE
133 self.toggle_userAccountFlags(search_filter, flags, on=True)
135 def enable_account(self, search_filter):
136 """Enables an account
138 :param search_filter: LDAP filter to find the user (eg
139 samccountname=name)
142 flags = samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_PASSWD_NOTREQD
143 self.toggle_userAccountFlags(search_filter, flags, on=False)
145 def toggle_userAccountFlags(self, search_filter, flags, flags_str=None,
146 on=True, strict=False):
147 """Toggle_userAccountFlags
149 :param search_filter: LDAP filter to find the user (eg
150 samccountname=name)
151 :param flags: samba.dsdb.UF_* flags
152 :param on: on=True (default) => set, on=False => unset
153 :param strict: strict=False (default) ignore if no action is needed
154 strict=True raises an Exception if...
156 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
157 expression=search_filter, attrs=["userAccountControl"])
158 if len(res) == 0:
159 raise Exception("Unable to find account where '%s'" % search_filter)
160 assert(len(res) == 1)
161 account_dn = res[0].dn
163 old_uac = int(res[0]["userAccountControl"][0])
164 if on:
165 if strict and (old_uac & flags):
166 error = "Account flag(s) '%s' already set" % flags_str
167 raise Exception(error)
169 new_uac = old_uac | flags
170 else:
171 if strict and not (old_uac & flags):
172 error = "Account flag(s) '%s' already unset" % flags_str
173 raise Exception(error)
175 new_uac = old_uac & ~flags
177 if old_uac == new_uac:
178 return
180 mod = """
181 dn: %s
182 changetype: modify
183 delete: userAccountControl
184 userAccountControl: %u
185 add: userAccountControl
186 userAccountControl: %u
187 """ % (account_dn, old_uac, new_uac)
188 self.modify_ldif(mod)
190 def force_password_change_at_next_login(self, search_filter):
191 """Forces a password change at next login
193 :param search_filter: LDAP filter to find the user (eg
194 samccountname=name)
196 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
197 expression=search_filter, attrs=[])
198 if len(res) == 0:
199 raise Exception('Unable to find user "%s"' % search_filter)
200 assert(len(res) == 1)
201 user_dn = res[0].dn
203 mod = """
204 dn: %s
205 changetype: modify
206 replace: pwdLastSet
207 pwdLastSet: 0
208 """ % (user_dn)
209 self.modify_ldif(mod)
211 def unlock_account(self, search_filter):
212 """Unlock a user account by resetting lockoutTime to 0.
213 This does also reset the badPwdCount to 0.
215 :param search_filter: LDAP filter to find the user (e.g.
216 sAMAccountName=username)
218 res = self.search(base=self.domain_dn(),
219 scope=ldb.SCOPE_SUBTREE,
220 expression=search_filter,
221 attrs=[])
222 if len(res) == 0:
223 raise SamDBNotFoundError('Unable to find user "%s"' % search_filter)
224 if len(res) != 1:
225 raise SamDBError('User "%s" is not unique' % search_filter)
226 user_dn = res[0].dn
228 mod = """
229 dn: %s
230 changetype: modify
231 replace: lockoutTime
232 lockoutTime: 0
233 """ % (user_dn)
234 self.modify_ldif(mod)
236 def newgroup(self, groupname, groupou=None, grouptype=None,
237 description=None, mailaddress=None, notes=None, sd=None,
238 gidnumber=None, nisdomain=None):
239 """Adds a new group with additional parameters
241 :param groupname: Name of the new group
242 :param grouptype: Type of the new group
243 :param description: Description of the new group
244 :param mailaddress: Email address of the new group
245 :param notes: Notes of the new group
246 :param gidnumber: GID Number of the new group
247 :param nisdomain: NIS Domain Name of the new group
248 :param sd: security descriptor of the object
251 if groupou:
252 group_dn = "CN=%s,%s,%s" % (groupname, groupou, self.domain_dn())
253 else:
254 group_dn = "CN=%s,%s" % (groupname, self.get_wellknown_dn(
255 self.get_default_basedn(),
256 dsdb.DS_GUID_USERS_CONTAINER))
258 # The new user record. Note the reliance on the SAMLDB module which
259 # fills in the default information
260 ldbmessage = {"dn": group_dn,
261 "sAMAccountName": groupname,
262 "objectClass": "group"}
264 if grouptype is not None:
265 ldbmessage["groupType"] = normalise_int32(grouptype)
267 if description is not None:
268 ldbmessage["description"] = description
270 if mailaddress is not None:
271 ldbmessage["mail"] = mailaddress
273 if notes is not None:
274 ldbmessage["info"] = notes
276 if gidnumber is not None:
277 ldbmessage["gidNumber"] = normalise_int32(gidnumber)
279 if nisdomain is not None:
280 ldbmessage["msSFU30Name"] = groupname
281 ldbmessage["msSFU30NisDomain"] = nisdomain
283 if sd is not None:
284 ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
286 self.add(ldbmessage)
288 def deletegroup(self, groupname):
289 """Deletes a group
291 :param groupname: Name of the target group
294 groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
295 self.transaction_start()
296 try:
297 targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
298 expression=groupfilter, attrs=[])
299 if len(targetgroup) == 0:
300 raise Exception('Unable to find group "%s"' % groupname)
301 assert(len(targetgroup) == 1)
302 self.delete(targetgroup[0].dn)
303 except:
304 self.transaction_cancel()
305 raise
306 else:
307 self.transaction_commit()
309 def group_member_filter(self, member, member_types):
310 filter = ""
312 all_member_types = [ 'user',
313 'group',
314 'computer',
315 'serviceaccount',
316 'contact',
319 if 'all' in member_types:
320 member_types = all_member_types
322 for member_type in member_types:
323 if member_type not in all_member_types:
324 raise Exception('Invalid group member type "%s". '
325 'Valid types are %s and all.' %
326 (member_type, ", ".join(all_member_types)))
328 if 'user' in member_types:
329 filter += ('(&(sAMAccountName=%s)(samAccountType=%d))' %
330 (ldb.binary_encode(member), dsdb.ATYPE_NORMAL_ACCOUNT))
331 if 'group' in member_types:
332 filter += ('(&(sAMAccountName=%s)'
333 '(objectClass=group)'
334 '(!(groupType:1.2.840.113556.1.4.803:=1)))' %
335 ldb.binary_encode(member))
336 if 'computer' in member_types:
337 samaccountname = member
338 if member[-1] != '$':
339 samaccountname = "%s$" % member
340 filter += ('(&(samAccountType=%d)'
341 '(!(objectCategory=msDS-ManagedServiceAccount))'
342 '(sAMAccountName=%s))' %
343 (dsdb.ATYPE_WORKSTATION_TRUST,
344 ldb.binary_encode(samaccountname)))
345 if 'serviceaccount' in member_types:
346 samaccountname = member
347 if member[-1] != '$':
348 samaccountname = "%s$" % member
349 filter += ('(&(samAccountType=%d)'
350 '(objectCategory=msDS-ManagedServiceAccount)'
351 '(sAMAccountName=%s))' %
352 (dsdb.ATYPE_WORKSTATION_TRUST,
353 ldb.binary_encode(samaccountname)))
354 if 'contact' in member_types:
355 filter += ('(&(objectCategory=Person)(!(objectSid=*))(name=%s))' %
356 ldb.binary_encode(member))
358 filter = "(|%s)" % filter
360 return filter
362 def add_remove_group_members(self, groupname, members,
363 add_members_operation=True,
364 member_types=None,
365 member_base_dn=None):
366 """Adds or removes group members
368 :param groupname: Name of the target group
369 :param members: list of group members
370 :param add_members_operation: Defines if its an add or remove
371 operation
373 if member_types is None:
374 member_types = ['user', 'group', 'computer']
376 groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (
377 ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
379 self.transaction_start()
380 try:
381 targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
382 expression=groupfilter, attrs=['member'])
383 if len(targetgroup) == 0:
384 raise Exception('Unable to find group "%s"' % groupname)
385 assert(len(targetgroup) == 1)
387 modified = False
389 addtargettogroup = """
390 dn: %s
391 changetype: modify
392 """ % (str(targetgroup[0].dn))
394 for member in members:
395 targetmember_dn = None
396 if member_base_dn is None:
397 member_base_dn = self.domain_dn()
399 try:
400 membersid = security.dom_sid(member)
401 targetmember_dn = "<SID=%s>" % str(membersid)
402 except ValueError:
403 pass
405 if targetmember_dn is None:
406 try:
407 member_dn = ldb.Dn(self, member)
408 if member_dn.get_linearized() == member_dn.extended_str(1):
409 full_member_dn = self.normalize_dn_in_domain(member_dn)
410 else:
411 full_member_dn = member_dn
412 targetmember_dn = full_member_dn.extended_str(1)
413 except ValueError as e:
414 pass
416 if targetmember_dn is None:
417 filter = self.group_member_filter(member, member_types)
418 targetmember = self.search(base=member_base_dn,
419 scope=ldb.SCOPE_SUBTREE,
420 expression=filter,
421 attrs=[])
423 if len(targetmember) > 1:
424 targetmemberlist_str = ""
425 for msg in targetmember:
426 targetmemberlist_str += "%s\n" % msg.get("dn")
427 raise Exception('Found multiple results for "%s":\n%s' %
428 (member, targetmemberlist_str))
429 if len(targetmember) != 1:
430 raise Exception('Unable to find "%s". Operation cancelled.' % member)
431 targetmember_dn = targetmember[0].dn.extended_str(1)
433 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']]):
434 modified = True
435 addtargettogroup += """add: member
436 member: %s
437 """ % (str(targetmember_dn))
439 elif add_members_operation is False and (targetgroup[0].get('member') is not None and get_bytes(targetmember_dn) in targetgroup[0]['member']):
440 modified = True
441 addtargettogroup += """delete: member
442 member: %s
443 """ % (str(targetmember_dn))
445 if modified is True:
446 self.modify_ldif(addtargettogroup)
448 except:
449 self.transaction_cancel()
450 raise
451 else:
452 self.transaction_commit()
454 def prepare_attr_replace(self, msg, old, attr_name, value):
455 """Changes the MessageElement with the given attr_name of the
456 given Message. If the value is "" set an empty value and the flag
457 FLAG_MOD_DELETE, otherwise set the new value and FLAG_MOD_REPLACE.
458 If the value is None or the Message contains the attr_name with this
459 value, nothing will changed."""
460 # skip unchanged attribute
461 if value is None:
462 return
463 if attr_name in old and str(value) == str(old[attr_name]):
464 return
466 # remove attribute
467 if len(value) == 0:
468 if attr_name in old:
469 el = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr_name)
470 msg.add(el)
471 return
473 # change attribute
474 el = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, attr_name)
475 msg.add(el)
477 def fullname_from_names(self, given_name=None, initials=None, surname=None,
478 old_attrs=None, fallback_default=""):
479 """Prepares new combined fullname, using the name parts.
480 Used for things like displayName or cn.
481 Use the original name values, if no new one is specified."""
482 if old_attrs is None:
483 old_attrs = {}
485 attrs = {"givenName": given_name,
486 "initials": initials,
487 "sn": surname}
489 # if the attribute is not specified, try to use the old one
490 for attr_name, attr_value in attrs.items():
491 if attr_value is None and attr_name in old_attrs:
492 attrs[attr_name] = str(old_attrs[attr_name])
494 # add '.' to initials if initials are not None and not "" and if the initials
495 # don't have already a '.' at the end
496 if attrs["initials"] and not attrs["initials"].endswith('.'):
497 attrs["initials"] += '.'
499 # remove empty values (None and '')
500 attrs_values = list(filter(None, attrs.values()))
502 # fullname is the combination of not-empty values as string, separated by ' '
503 fullname = ' '.join(attrs_values)
505 if fullname == '':
506 return fallback_default
508 return fullname
510 def newuser(self, username, password,
511 force_password_change_at_next_login_req=False,
512 useusernameascn=False, userou=None, surname=None, givenname=None,
513 initials=None, profilepath=None, scriptpath=None, homedrive=None,
514 homedirectory=None, jobtitle=None, department=None, company=None,
515 description=None, mailaddress=None, internetaddress=None,
516 telephonenumber=None, physicaldeliveryoffice=None, sd=None,
517 setpassword=True, uidnumber=None, gidnumber=None, gecos=None,
518 loginshell=None, uid=None, nisdomain=None, unixhome=None,
519 smartcard_required=False):
520 """Adds a new user with additional parameters
522 :param username: Name of the new user
523 :param password: Password for the new user
524 :param force_password_change_at_next_login_req: Force password change
525 :param useusernameascn: Use username as cn rather that firstname +
526 initials + lastname
527 :param userou: Object container (without domainDN postfix) for new user
528 :param surname: Surname of the new user
529 :param givenname: First name of the new user
530 :param initials: Initials of the new user
531 :param profilepath: Profile path of the new user
532 :param scriptpath: Logon script path of the new user
533 :param homedrive: Home drive of the new user
534 :param homedirectory: Home directory of the new user
535 :param jobtitle: Job title of the new user
536 :param department: Department of the new user
537 :param company: Company of the new user
538 :param description: of the new user
539 :param mailaddress: Email address of the new user
540 :param internetaddress: Home page of the new user
541 :param telephonenumber: Phone number of the new user
542 :param physicaldeliveryoffice: Office location of the new user
543 :param sd: security descriptor of the object
544 :param setpassword: optionally disable password reset
545 :param uidnumber: RFC2307 Unix numeric UID of the new user
546 :param gidnumber: RFC2307 Unix primary GID of the new user
547 :param gecos: RFC2307 Unix GECOS field of the new user
548 :param loginshell: RFC2307 Unix login shell of the new user
549 :param uid: RFC2307 Unix username of the new user
550 :param nisdomain: RFC2307 Unix NIS domain of the new user
551 :param unixhome: RFC2307 Unix home directory of the new user
552 :param smartcard_required: set the UF_SMARTCARD_REQUIRED bit of the new user
555 displayname = self.fullname_from_names(given_name=givenname,
556 initials=initials,
557 surname=surname)
558 cn = username
559 if useusernameascn is None and displayname != "":
560 cn = displayname
562 if userou:
563 user_dn = "CN=%s,%s,%s" % (cn, userou, self.domain_dn())
564 else:
565 user_dn = "CN=%s,%s" % (cn, self.get_wellknown_dn(
566 self.get_default_basedn(),
567 dsdb.DS_GUID_USERS_CONTAINER))
569 dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "")
570 user_principal_name = "%s@%s" % (username, dnsdomain)
571 # The new user record. Note the reliance on the SAMLDB module which
572 # fills in the default information
573 ldbmessage = {"dn": user_dn,
574 "sAMAccountName": username,
575 "userPrincipalName": user_principal_name,
576 "objectClass": "user"}
578 if smartcard_required:
579 ldbmessage["userAccountControl"] = str(dsdb.UF_NORMAL_ACCOUNT |
580 dsdb.UF_SMARTCARD_REQUIRED)
581 setpassword = False
583 if surname is not None:
584 ldbmessage["sn"] = surname
586 if givenname is not None:
587 ldbmessage["givenName"] = givenname
589 if displayname != "":
590 ldbmessage["displayName"] = displayname
591 ldbmessage["name"] = displayname
593 if initials is not None:
594 ldbmessage["initials"] = '%s.' % initials
596 if profilepath is not None:
597 ldbmessage["profilePath"] = profilepath
599 if scriptpath is not None:
600 ldbmessage["scriptPath"] = scriptpath
602 if homedrive is not None:
603 ldbmessage["homeDrive"] = homedrive
605 if homedirectory is not None:
606 ldbmessage["homeDirectory"] = homedirectory
608 if jobtitle is not None:
609 ldbmessage["title"] = jobtitle
611 if department is not None:
612 ldbmessage["department"] = department
614 if company is not None:
615 ldbmessage["company"] = company
617 if description is not None:
618 ldbmessage["description"] = description
620 if mailaddress is not None:
621 ldbmessage["mail"] = mailaddress
623 if internetaddress is not None:
624 ldbmessage["wWWHomePage"] = internetaddress
626 if telephonenumber is not None:
627 ldbmessage["telephoneNumber"] = telephonenumber
629 if physicaldeliveryoffice is not None:
630 ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
632 if sd is not None:
633 ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
635 ldbmessage2 = None
636 if any(map(lambda b: b is not None, (uid, uidnumber, gidnumber, gecos,
637 loginshell, nisdomain, unixhome))):
638 ldbmessage2 = ldb.Message()
639 ldbmessage2.dn = ldb.Dn(self, user_dn)
640 if uid is not None:
641 ldbmessage2["uid"] = ldb.MessageElement(str(uid), ldb.FLAG_MOD_REPLACE, 'uid')
642 if uidnumber is not None:
643 ldbmessage2["uidNumber"] = ldb.MessageElement(str(uidnumber), ldb.FLAG_MOD_REPLACE, 'uidNumber')
644 if gidnumber is not None:
645 ldbmessage2["gidNumber"] = ldb.MessageElement(str(gidnumber), ldb.FLAG_MOD_REPLACE, 'gidNumber')
646 if gecos is not None:
647 ldbmessage2["gecos"] = ldb.MessageElement(str(gecos), ldb.FLAG_MOD_REPLACE, 'gecos')
648 if loginshell is not None:
649 ldbmessage2["loginShell"] = ldb.MessageElement(str(loginshell), ldb.FLAG_MOD_REPLACE, 'loginShell')
650 if unixhome is not None:
651 ldbmessage2["unixHomeDirectory"] = ldb.MessageElement(
652 str(unixhome), ldb.FLAG_MOD_REPLACE, 'unixHomeDirectory')
653 if nisdomain is not None:
654 ldbmessage2["msSFU30NisDomain"] = ldb.MessageElement(
655 str(nisdomain), ldb.FLAG_MOD_REPLACE, 'msSFU30NisDomain')
656 ldbmessage2["msSFU30Name"] = ldb.MessageElement(
657 str(username), ldb.FLAG_MOD_REPLACE, 'msSFU30Name')
658 ldbmessage2["unixUserPassword"] = ldb.MessageElement(
659 'ABCD!efgh12345$67890', ldb.FLAG_MOD_REPLACE,
660 'unixUserPassword')
662 self.transaction_start()
663 try:
664 self.add(ldbmessage)
666 with self._CleanUpOnError(self, user_dn):
667 if ldbmessage2:
668 self.modify(ldbmessage2)
670 # Sets the password for it
671 if setpassword:
672 self.setpassword(("(distinguishedName=%s)" %
673 ldb.binary_encode(user_dn)),
674 password,
675 force_password_change_at_next_login_req)
676 except:
677 self.transaction_cancel()
678 raise
679 else:
680 self.transaction_commit()
682 def newcontact(self,
683 fullcontactname=None,
684 ou=None,
685 surname=None,
686 givenname=None,
687 initials=None,
688 displayname=None,
689 jobtitle=None,
690 department=None,
691 company=None,
692 description=None,
693 mailaddress=None,
694 internetaddress=None,
695 telephonenumber=None,
696 mobilenumber=None,
697 physicaldeliveryoffice=None):
698 """Adds a new contact with additional parameters
700 :param fullcontactname: Optional full name of the new contact
701 :param ou: Object container for new contact
702 :param surname: Surname of the new contact
703 :param givenname: First name of the new contact
704 :param initials: Initials of the new contact
705 :param displayname: displayName of the new contact
706 :param jobtitle: Job title of the new contact
707 :param department: Department of the new contact
708 :param company: Company of the new contact
709 :param description: Description of the new contact
710 :param mailaddress: Email address of the new contact
711 :param internetaddress: Home page of the new contact
712 :param telephonenumber: Phone number of the new contact
713 :param mobilenumber: Primary mobile number of the new contact
714 :param physicaldeliveryoffice: Office location of the new contact
717 # Prepare the contact name like the RSAT, using the name parts.
718 cn = self.fullname_from_names(given_name=givenname,
719 initials=initials,
720 surname=surname)
722 # Use the specified fullcontactname instead of the previously prepared
723 # contact name, if it is specified.
724 # This is similar to the "Full name" value of the RSAT.
725 if fullcontactname is not None:
726 cn = fullcontactname
728 if fullcontactname is None and cn == "":
729 raise Exception('No name for contact specified')
731 contactcontainer_dn = self.domain_dn()
732 if ou:
733 contactcontainer_dn = self.normalize_dn_in_domain(ou)
735 contact_dn = "CN=%s,%s" % (cn, contactcontainer_dn)
737 ldbmessage = {"dn": contact_dn,
738 "objectClass": "contact",
741 if surname is not None:
742 ldbmessage["sn"] = surname
744 if givenname is not None:
745 ldbmessage["givenName"] = givenname
747 if displayname is not None:
748 ldbmessage["displayName"] = displayname
750 if initials is not None:
751 ldbmessage["initials"] = '%s.' % initials
753 if jobtitle is not None:
754 ldbmessage["title"] = jobtitle
756 if department is not None:
757 ldbmessage["department"] = department
759 if company is not None:
760 ldbmessage["company"] = company
762 if description is not None:
763 ldbmessage["description"] = description
765 if mailaddress is not None:
766 ldbmessage["mail"] = mailaddress
768 if internetaddress is not None:
769 ldbmessage["wWWHomePage"] = internetaddress
771 if telephonenumber is not None:
772 ldbmessage["telephoneNumber"] = telephonenumber
774 if mobilenumber is not None:
775 ldbmessage["mobile"] = mobilenumber
777 if physicaldeliveryoffice is not None:
778 ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
780 self.add(ldbmessage)
782 return cn
784 def newcomputer(self, computername, computerou=None, description=None,
785 prepare_oldjoin=False, ip_address_list=None,
786 service_principal_name_list=None):
787 """Adds a new user with additional parameters
789 :param computername: Name of the new computer
790 :param computerou: Object container for new computer
791 :param description: Description of the new computer
792 :param prepare_oldjoin: Preset computer password for oldjoin mechanism
793 :param ip_address_list: ip address list for DNS A or AAAA record
794 :param service_principal_name_list: string list of servicePincipalName
797 cn = re.sub(r"\$$", "", computername)
798 if cn.count('$'):
799 raise Exception('Illegal computername "%s"' % computername)
800 samaccountname = "%s$" % cn
802 computercontainer_dn = self.get_wellknown_dn(self.get_default_basedn(),
803 dsdb.DS_GUID_COMPUTERS_CONTAINER)
804 if computerou:
805 computercontainer_dn = self.normalize_dn_in_domain(computerou)
807 computer_dn = "CN=%s,%s" % (cn, computercontainer_dn)
809 ldbmessage = {"dn": computer_dn,
810 "sAMAccountName": samaccountname,
811 "objectClass": "computer",
814 if description is not None:
815 ldbmessage["description"] = description
817 if service_principal_name_list:
818 ldbmessage["servicePrincipalName"] = service_principal_name_list
820 accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
821 dsdb.UF_ACCOUNTDISABLE)
822 if prepare_oldjoin:
823 accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
824 ldbmessage["userAccountControl"] = accountcontrol
826 if ip_address_list:
827 ldbmessage['dNSHostName'] = '{}.{}'.format(
828 cn, self.domain_dns_name())
830 self.transaction_start()
831 try:
832 self.add(ldbmessage)
834 if prepare_oldjoin:
835 password = cn.lower()
836 with self._CleanUpOnError(self, computer_dn):
837 self.setpassword(("(distinguishedName=%s)" %
838 ldb.binary_encode(computer_dn)),
839 password, False)
840 except:
841 self.transaction_cancel()
842 raise
843 else:
844 self.transaction_commit()
846 def deleteuser(self, username):
847 """Deletes a user
849 :param username: Name of the target user
852 filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn())
853 self.transaction_start()
854 try:
855 target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
856 expression=filter, attrs=[])
857 if len(target) == 0:
858 raise Exception('Unable to find user "%s"' % username)
859 assert(len(target) == 1)
860 self.delete(target[0].dn)
861 except:
862 self.transaction_cancel()
863 raise
864 else:
865 self.transaction_commit()
867 def setpassword(self, search_filter, password,
868 force_change_at_next_login=False, username=None):
869 """Sets the password for a user
871 :param search_filter: LDAP filter to find the user (eg
872 samccountname=name)
873 :param password: Password for the user
874 :param force_change_at_next_login: Force password change
876 self.transaction_start()
877 try:
878 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
879 expression=search_filter, attrs=[])
880 if len(res) == 0:
881 raise Exception('Unable to find user "%s"' % (username or search_filter))
882 if len(res) > 1:
883 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter))
884 user_dn = res[0].dn
885 if not isinstance(password, str):
886 pw = password.decode('utf-8')
887 else:
888 pw = password
889 pw = ('"' + pw + '"').encode('utf-16-le')
890 setpw = """
891 dn: %s
892 changetype: modify
893 replace: unicodePwd
894 unicodePwd:: %s
895 """ % (user_dn, base64.b64encode(pw).decode('utf-8'))
897 self.modify_ldif(setpw)
899 if force_change_at_next_login:
900 self.force_password_change_at_next_login(
901 "(distinguishedName=" + str(user_dn) + ")")
903 # modify the userAccountControl to remove the disabled bit
904 self.enable_account(search_filter)
905 except:
906 self.transaction_cancel()
907 raise
908 else:
909 self.transaction_commit()
911 def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False):
912 """Sets the account expiry for a user
914 :param search_filter: LDAP filter to find the user (eg
915 samaccountname=name)
916 :param expiry_seconds: expiry time from now in seconds
917 :param no_expiry_req: if set, then don't expire password
919 self.transaction_start()
920 try:
921 res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
922 expression=search_filter,
923 attrs=["userAccountControl", "accountExpires"])
924 if len(res) == 0:
925 raise Exception('Unable to find user "%s"' % search_filter)
926 assert(len(res) == 1)
927 user_dn = res[0].dn
929 userAccountControl = int(res[0]["userAccountControl"][0])
930 if no_expiry_req:
931 userAccountControl = userAccountControl | 0x10000
932 accountExpires = 0
933 else:
934 userAccountControl = userAccountControl & ~0x10000
935 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
937 setexp = """
938 dn: %s
939 changetype: modify
940 replace: userAccountControl
941 userAccountControl: %u
942 replace: accountExpires
943 accountExpires: %u
944 """ % (user_dn, userAccountControl, accountExpires)
946 self.modify_ldif(setexp)
947 except:
948 self.transaction_cancel()
949 raise
950 else:
951 self.transaction_commit()
953 def set_domain_sid(self, sid):
954 """Change the domain SID used by this LDB.
956 :param sid: The new domain sid to use.
958 dsdb._samdb_set_domain_sid(self, sid)
960 def get_domain_sid(self):
961 """Read the domain SID used by this LDB. """
962 return dsdb._samdb_get_domain_sid(self)
964 domain_sid = property(get_domain_sid, set_domain_sid,
965 doc="SID for the domain")
967 def set_invocation_id(self, invocation_id):
968 """Set the invocation id for this SamDB handle.
970 :param invocation_id: GUID of the invocation id.
972 dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
974 def get_invocation_id(self):
975 """Get the invocation_id id"""
976 return dsdb._samdb_ntds_invocation_id(self)
978 invocation_id = property(get_invocation_id, set_invocation_id,
979 doc="Invocation ID GUID")
981 def get_oid_from_attid(self, attid):
982 return dsdb._dsdb_get_oid_from_attid(self, attid)
984 def get_attid_from_lDAPDisplayName(self, ldap_display_name,
985 is_schema_nc=False):
986 '''return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI'''
987 return dsdb._dsdb_get_attid_from_lDAPDisplayName(self,
988 ldap_display_name, is_schema_nc)
990 def get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name):
991 '''return the syntax OID for a LDAP attribute as a string'''
992 return dsdb._dsdb_get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name)
994 def get_systemFlags_from_lDAPDisplayName(self, ldap_display_name):
995 '''return the systemFlags for a LDAP attribute as a integer'''
996 return dsdb._dsdb_get_systemFlags_from_lDAPDisplayName(self, ldap_display_name)
998 def get_linkId_from_lDAPDisplayName(self, ldap_display_name):
999 '''return the linkID for a LDAP attribute as a integer'''
1000 return dsdb._dsdb_get_linkId_from_lDAPDisplayName(self, ldap_display_name)
1002 def get_lDAPDisplayName_by_attid(self, attid):
1003 '''return the lDAPDisplayName from an integer DRS attribute ID'''
1004 return dsdb._dsdb_get_lDAPDisplayName_by_attid(self, attid)
1006 def get_backlink_from_lDAPDisplayName(self, ldap_display_name):
1007 '''return the attribute name of the corresponding backlink from the name
1008 of a forward link attribute. If there is no backlink return None'''
1009 return dsdb._dsdb_get_backlink_from_lDAPDisplayName(self, ldap_display_name)
1011 def set_ntds_settings_dn(self, ntds_settings_dn):
1012 """Set the NTDS Settings DN, as would be returned on the dsServiceName
1013 rootDSE attribute.
1015 This allows the DN to be set before the database fully exists
1017 :param ntds_settings_dn: The new DN to use
1019 dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
1021 def get_ntds_GUID(self):
1022 """Get the NTDS objectGUID"""
1023 return dsdb._samdb_ntds_objectGUID(self)
1025 def get_timestr(self):
1026 """Get the current time as generalized time string"""
1027 res = self.search(base="",
1028 scope=ldb.SCOPE_BASE,
1029 attrs=["currentTime"])
1030 return str(res[0]["currentTime"][0])
1032 def get_time(self):
1033 """Get the current time as UNIX time"""
1034 return ldb.string_to_time(self.get_timestr())
1036 def get_nttime(self):
1037 """Get the current time as NT time"""
1038 return samba.unix2nttime(self.get_time())
1040 def server_site_name(self):
1041 """Get the server site name"""
1042 return dsdb._samdb_server_site_name(self)
1044 def host_dns_name(self):
1045 """return the DNS name of this host"""
1046 res = self.search(base='', scope=ldb.SCOPE_BASE, attrs=['dNSHostName'])
1047 return str(res[0]['dNSHostName'][0])
1049 def domain_dns_name(self):
1050 """return the DNS name of the domain root"""
1051 domain_dn = self.get_default_basedn()
1052 return domain_dn.canonical_str().split('/')[0]
1054 def domain_netbios_name(self):
1055 """return the NetBIOS name of the domain root"""
1056 domain_dn = self.get_default_basedn()
1057 dns_name = self.domain_dns_name()
1058 filter = "(&(objectClass=crossRef)(nETBIOSName=*)(ncName=%s)(dnsroot=%s))" % (domain_dn, dns_name)
1059 partitions_dn = self.get_partitions_dn()
1060 res = self.search(partitions_dn,
1061 scope=ldb.SCOPE_ONELEVEL,
1062 expression=filter)
1063 try:
1064 netbios_domain = res[0]["nETBIOSName"][0].decode()
1065 except IndexError:
1066 return None
1067 return netbios_domain
1069 def forest_dns_name(self):
1070 """return the DNS name of the forest root"""
1071 forest_dn = self.get_root_basedn()
1072 return forest_dn.canonical_str().split('/')[0]
1074 def load_partition_usn(self, base_dn):
1075 return dsdb._dsdb_load_partition_usn(self, base_dn)
1077 def set_schema(self, schema, write_indices_and_attributes=True):
1078 self.set_schema_from_ldb(schema.ldb, write_indices_and_attributes=write_indices_and_attributes)
1080 def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True):
1081 dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes)
1083 def set_schema_update_now(self):
1084 ldif = """
1086 changetype: modify
1087 add: schemaUpdateNow
1088 schemaUpdateNow: 1
1090 self.modify_ldif(ldif)
1092 def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
1093 '''convert a list of attribute values to a DRSUAPI DsReplicaAttribute'''
1094 return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
1096 def dsdb_normalise_attributes(self, ldb, ldap_display_name, ldif_elements):
1097 '''normalise a list of attribute values'''
1098 return dsdb._dsdb_normalise_attributes(ldb, ldap_display_name, ldif_elements)
1100 def get_attribute_from_attid(self, attid):
1101 """ Get from an attid the associated attribute
1103 :param attid: The attribute id for searched attribute
1104 :return: The name of the attribute associated with this id
1106 if len(self.hash_oid_name.keys()) == 0:
1107 self._populate_oid_attid()
1108 if self.get_oid_from_attid(attid) in self.hash_oid_name:
1109 return self.hash_oid_name[self.get_oid_from_attid(attid)]
1110 else:
1111 return None
1113 def _populate_oid_attid(self):
1114 """Populate the hash hash_oid_name.
1116 This hash contains the oid of the attribute as a key and
1117 its display name as a value
1119 self.hash_oid_name = {}
1120 res = self.search(expression="objectClass=attributeSchema",
1121 controls=["search_options:1:2"],
1122 attrs=["attributeID",
1123 "lDAPDisplayName"])
1124 if len(res) > 0:
1125 for e in res:
1126 strDisplay = str(e.get("lDAPDisplayName"))
1127 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
1129 def get_attribute_replmetadata_version(self, dn, att):
1130 """Get the version field trom the replPropertyMetaData for
1131 the given field
1133 :param dn: The on which we want to get the version
1134 :param att: The name of the attribute
1135 :return: The value of the version field in the replPropertyMetaData
1136 for the given attribute. None if the attribute is not replicated
1139 res = self.search(expression="distinguishedName=%s" % dn,
1140 scope=ldb.SCOPE_SUBTREE,
1141 controls=["search_options:1:2"],
1142 attrs=["replPropertyMetaData"])
1143 if len(res) == 0:
1144 return None
1146 repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
1147 res[0]["replPropertyMetaData"][0])
1148 ctr = repl.ctr
1149 if len(self.hash_oid_name.keys()) == 0:
1150 self._populate_oid_attid()
1151 for o in ctr.array:
1152 # Search for Description
1153 att_oid = self.get_oid_from_attid(o.attid)
1154 if att_oid in self.hash_oid_name and\
1155 att.lower() == self.hash_oid_name[att_oid].lower():
1156 return o.version
1157 return None
1159 def set_attribute_replmetadata_version(self, dn, att, value,
1160 addifnotexist=False):
1161 res = self.search(expression="distinguishedName=%s" % dn,
1162 scope=ldb.SCOPE_SUBTREE,
1163 controls=["search_options:1:2"],
1164 attrs=["replPropertyMetaData"])
1165 if len(res) == 0:
1166 return None
1168 repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
1169 res[0]["replPropertyMetaData"][0])
1170 ctr = repl.ctr
1171 now = samba.unix2nttime(int(time.time()))
1172 found = False
1173 if len(self.hash_oid_name.keys()) == 0:
1174 self._populate_oid_attid()
1175 for o in ctr.array:
1176 # Search for Description
1177 att_oid = self.get_oid_from_attid(o.attid)
1178 if att_oid in self.hash_oid_name and\
1179 att.lower() == self.hash_oid_name[att_oid].lower():
1180 found = True
1181 seq = self.sequence_number(ldb.SEQ_NEXT)
1182 o.version = value
1183 o.originating_change_time = now
1184 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
1185 o.originating_usn = seq
1186 o.local_usn = seq
1188 if not found and addifnotexist and len(ctr.array) > 0:
1189 o2 = drsblobs.replPropertyMetaData1()
1190 o2.attid = 589914
1191 att_oid = self.get_oid_from_attid(o2.attid)
1192 seq = self.sequence_number(ldb.SEQ_NEXT)
1193 o2.version = value
1194 o2.originating_change_time = now
1195 o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
1196 o2.originating_usn = seq
1197 o2.local_usn = seq
1198 found = True
1199 tab = ctr.array
1200 tab.append(o2)
1201 ctr.count = ctr.count + 1
1202 ctr.array = tab
1204 if found:
1205 replBlob = ndr_pack(repl)
1206 msg = ldb.Message()
1207 msg.dn = res[0].dn
1208 msg["replPropertyMetaData"] = \
1209 ldb.MessageElement(replBlob,
1210 ldb.FLAG_MOD_REPLACE,
1211 "replPropertyMetaData")
1212 self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
1214 def write_prefixes_from_schema(self):
1215 dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
1217 def get_partitions_dn(self):
1218 return dsdb._dsdb_get_partitions_dn(self)
1220 def get_nc_root(self, dn):
1221 return dsdb._dsdb_get_nc_root(self, dn)
1223 def get_wellknown_dn(self, nc_root, wkguid):
1224 h_nc = self.hash_well_known.get(str(nc_root))
1225 dn = None
1226 if h_nc is not None:
1227 dn = h_nc.get(wkguid)
1228 if dn is None:
1229 dn = dsdb._dsdb_get_wellknown_dn(self, nc_root, wkguid)
1230 if dn is None:
1231 return dn
1232 if h_nc is None:
1233 self.hash_well_known[str(nc_root)] = {}
1234 h_nc = self.hash_well_known[str(nc_root)]
1235 h_nc[wkguid] = dn
1236 return dn
1238 def set_minPwdAge(self, value):
1239 if not isinstance(value, bytes):
1240 value = str(value).encode('utf8')
1241 m = ldb.Message()
1242 m.dn = ldb.Dn(self, self.domain_dn())
1243 m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
1244 self.modify(m)
1246 def get_minPwdAge(self):
1247 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
1248 if len(res) == 0:
1249 return None
1250 elif "minPwdAge" not in res[0]:
1251 return None
1252 else:
1253 return int(res[0]["minPwdAge"][0])
1255 def set_maxPwdAge(self, value):
1256 if not isinstance(value, bytes):
1257 value = str(value).encode('utf8')
1258 m = ldb.Message()
1259 m.dn = ldb.Dn(self, self.domain_dn())
1260 m["maxPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "maxPwdAge")
1261 self.modify(m)
1263 def get_maxPwdAge(self):
1264 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["maxPwdAge"])
1265 if len(res) == 0:
1266 return None
1267 elif "maxPwdAge" not in res[0]:
1268 return None
1269 else:
1270 return int(res[0]["maxPwdAge"][0])
1272 def set_minPwdLength(self, value):
1273 if not isinstance(value, bytes):
1274 value = str(value).encode('utf8')
1275 m = ldb.Message()
1276 m.dn = ldb.Dn(self, self.domain_dn())
1277 m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength")
1278 self.modify(m)
1280 def get_minPwdLength(self):
1281 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"])
1282 if len(res) == 0:
1283 return None
1284 elif "minPwdLength" not in res[0]:
1285 return None
1286 else:
1287 return int(res[0]["minPwdLength"][0])
1289 def set_pwdProperties(self, value):
1290 if not isinstance(value, bytes):
1291 value = str(value).encode('utf8')
1292 m = ldb.Message()
1293 m.dn = ldb.Dn(self, self.domain_dn())
1294 m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties")
1295 self.modify(m)
1297 def get_pwdProperties(self):
1298 res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"])
1299 if len(res) == 0:
1300 return None
1301 elif "pwdProperties" not in res[0]:
1302 return None
1303 else:
1304 return int(res[0]["pwdProperties"][0])
1306 def set_dsheuristics(self, dsheuristics):
1307 m = ldb.Message()
1308 m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
1309 % self.get_config_basedn().get_linearized())
1310 if dsheuristics is not None:
1311 m["dSHeuristics"] = \
1312 ldb.MessageElement(dsheuristics,
1313 ldb.FLAG_MOD_REPLACE,
1314 "dSHeuristics")
1315 else:
1316 m["dSHeuristics"] = \
1317 ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
1318 "dSHeuristics")
1319 self.modify(m)
1321 def get_dsheuristics(self):
1322 res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
1323 % self.get_config_basedn().get_linearized(),
1324 scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
1325 if len(res) == 0:
1326 dsheuristics = None
1327 elif "dSHeuristics" in res[0]:
1328 dsheuristics = res[0]["dSHeuristics"][0]
1329 else:
1330 dsheuristics = None
1332 return dsheuristics
1334 def create_ou(self, ou_dn, description=None, name=None, sd=None):
1335 """Creates an organizationalUnit object
1336 :param ou_dn: dn of the new object
1337 :param description: description attribute
1338 :param name: name attribute
1339 :param sd: security descriptor of the object, can be
1340 an SDDL string or security.descriptor type
1342 m = {"dn": ou_dn,
1343 "objectClass": "organizationalUnit"}
1345 if description:
1346 m["description"] = description
1347 if name:
1348 m["name"] = name
1350 if sd:
1351 m["nTSecurityDescriptor"] = ndr_pack(sd)
1352 self.add(m)
1354 def sequence_number(self, seq_type):
1355 """Returns the value of the sequence number according to the requested type
1356 :param seq_type: type of sequence number
1358 self.transaction_start()
1359 try:
1360 seq = super(SamDB, self).sequence_number(seq_type)
1361 except:
1362 self.transaction_cancel()
1363 raise
1364 else:
1365 self.transaction_commit()
1366 return seq
1368 def get_dsServiceName(self):
1369 '''get the NTDS DN from the rootDSE'''
1370 res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
1371 return str(res[0]["dsServiceName"][0])
1373 def get_serverName(self):
1374 '''get the server DN from the rootDSE'''
1375 res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["serverName"])
1376 return str(res[0]["serverName"][0])
1378 def dns_lookup(self, dns_name, dns_partition=None):
1379 '''Do a DNS lookup in the database, returns the NDR database structures'''
1380 if dns_partition is None:
1381 return dsdb_dns.lookup(self, dns_name)
1382 else:
1383 return dsdb_dns.lookup(self, dns_name,
1384 dns_partition=dns_partition)
1386 def dns_extract(self, el):
1387 '''Return the NDR database structures from a dnsRecord element'''
1388 return dsdb_dns.extract(self, el)
1390 def dns_replace(self, dns_name, new_records):
1391 '''Do a DNS modification on the database, sets the NDR database
1392 structures on a DNS name
1394 return dsdb_dns.replace(self, dns_name, new_records)
1396 def dns_replace_by_dn(self, dn, new_records):
1397 '''Do a DNS modification on the database, sets the NDR database
1398 structures on a LDB DN
1400 This routine is important because if the last record on the DN
1401 is removed, this routine will put a tombstone in the record.
1403 return dsdb_dns.replace_by_dn(self, dn, new_records)
1405 def garbage_collect_tombstones(self, dn, current_time,
1406 tombstone_lifetime=None):
1407 '''garbage_collect_tombstones(lp, samdb, [dn], current_time, tombstone_lifetime)
1408 -> (num_objects_expunged, num_links_expunged)'''
1410 if not is_ad_dc_built():
1411 raise SamDBError('Cannot garbage collect tombstones: ' \
1412 'AD DC was not built')
1414 if tombstone_lifetime is None:
1415 return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1416 current_time)
1417 else:
1418 return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1419 current_time,
1420 tombstone_lifetime)
1422 def create_own_rid_set(self):
1423 '''create a RID set for this DSA'''
1424 return dsdb._dsdb_create_own_rid_set(self)
1426 def allocate_rid(self):
1427 '''return a new RID from the RID Pool on this DSA'''
1428 return dsdb._dsdb_allocate_rid(self)
1430 def next_free_rid(self):
1431 '''return the next free RID from the RID Pool on this DSA.
1433 :note: This function is not intended for general use, and care must be
1434 taken if it is used to generate objectSIDs. The returned RID is not
1435 formally reserved for use, creating the possibility of duplicate
1436 objectSIDs.
1438 rid, _ = self.free_rid_bounds()
1439 return rid
1441 def free_rid_bounds(self):
1442 '''return the low and high bounds (inclusive) of RIDs that are
1443 available for use in this DSA's current RID pool.
1445 :note: This function is not intended for general use, and care must be
1446 taken if it is used to generate objectSIDs. The returned range of
1447 RIDs is not formally reserved for use, creating the possibility of
1448 duplicate objectSIDs.
1450 # Get DN of this server's RID Set
1451 server_name_dn = ldb.Dn(self, self.get_serverName())
1452 res = self.search(base=server_name_dn,
1453 scope=ldb.SCOPE_BASE,
1454 attrs=["serverReference"])
1455 try:
1456 server_ref = res[0]["serverReference"]
1457 except KeyError:
1458 raise ldb.LdbError(
1459 ldb.ERR_NO_SUCH_ATTRIBUTE,
1460 "No RID Set DN - "
1461 "Cannot find attribute serverReference of %s "
1462 "to calculate reference dn" % server_name_dn) from None
1463 server_ref_dn = ldb.Dn(self, server_ref[0].decode("utf-8"))
1465 res = self.search(base=server_ref_dn,
1466 scope=ldb.SCOPE_BASE,
1467 attrs=["rIDSetReferences"])
1468 try:
1469 rid_set_refs = res[0]["rIDSetReferences"]
1470 except KeyError:
1471 raise ldb.LdbError(
1472 ldb.ERR_NO_SUCH_ATTRIBUTE,
1473 "No RID Set DN - "
1474 "Cannot find attribute rIDSetReferences of %s "
1475 "to calculate reference dn" % server_ref_dn) from None
1476 rid_set_dn = ldb.Dn(self, rid_set_refs[0].decode("utf-8"))
1478 # Get the alloc pools and next RID of this RID Set
1479 res = self.search(base=rid_set_dn,
1480 scope=ldb.SCOPE_BASE,
1481 attrs=["rIDAllocationPool",
1482 "rIDPreviousAllocationPool",
1483 "rIDNextRID"])
1485 uint32_max = 2**32 - 1
1486 uint64_max = 2**64 - 1
1488 try:
1489 alloc_pool = int(res[0]["rIDAllocationPool"][0])
1490 except KeyError:
1491 alloc_pool = uint64_max
1492 if alloc_pool == uint64_max:
1493 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1494 "Bad RID Set %s" % rid_set_dn)
1496 try:
1497 prev_pool = int(res[0]["rIDPreviousAllocationPool"][0])
1498 except KeyError:
1499 prev_pool = uint64_max
1500 try:
1501 next_rid = int(res[0]["rIDNextRID"][0])
1502 except KeyError:
1503 next_rid = uint32_max
1505 # If we never used a pool, set up our first pool
1506 if prev_pool == uint64_max or next_rid == uint32_max:
1507 prev_pool = alloc_pool
1508 next_rid = prev_pool & uint32_max
1509 else:
1510 next_rid += 1
1512 # Now check if our current pool is still usable
1513 prev_pool_lo = prev_pool & uint32_max
1514 prev_pool_hi = prev_pool >> 32
1515 if next_rid > prev_pool_hi:
1516 # We need a new pool, check if we already have a new one
1517 # Otherwise we return an error code.
1518 if alloc_pool == prev_pool:
1519 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1520 "RID pools out of RIDs")
1522 # Now use the new pool
1523 prev_pool = alloc_pool
1524 prev_pool_lo = prev_pool & uint32_max
1525 prev_pool_hi = prev_pool >> 32
1526 next_rid = prev_pool_lo
1528 if next_rid < prev_pool_lo or next_rid > prev_pool_hi:
1529 raise ldb.LdbError(ldb.ERR_OPERATIONS_ERROR,
1530 "Bad RID chosen %d from range %d-%d" %
1531 (next_rid, prev_pool_lo, prev_pool_hi))
1533 return next_rid, prev_pool_hi
1535 def normalize_dn_in_domain(self, dn):
1536 '''return a new DN expanded by adding the domain DN
1538 If the dn is already a child of the domain DN, just
1539 return it as-is.
1541 :param dn: relative dn
1543 domain_dn = ldb.Dn(self, self.domain_dn())
1545 if isinstance(dn, ldb.Dn):
1546 dn = str(dn)
1548 full_dn = ldb.Dn(self, dn)
1549 if not full_dn.is_child_of(domain_dn):
1550 full_dn.add_base(domain_dn)
1551 return full_dn
1553 class dsdb_Dn(object):
1554 '''a class for binary DN'''
1556 def __init__(self, samdb, dnstring, syntax_oid=None):
1557 '''create a dsdb_Dn'''
1558 if syntax_oid is None:
1559 # auto-detect based on string
1560 if dnstring.startswith("B:"):
1561 syntax_oid = dsdb.DSDB_SYNTAX_BINARY_DN
1562 elif dnstring.startswith("S:"):
1563 syntax_oid = dsdb.DSDB_SYNTAX_STRING_DN
1564 else:
1565 syntax_oid = dsdb.DSDB_SYNTAX_OR_NAME
1566 if syntax_oid in [dsdb.DSDB_SYNTAX_BINARY_DN, dsdb.DSDB_SYNTAX_STRING_DN]:
1567 # it is a binary DN
1568 colons = dnstring.split(':')
1569 if len(colons) < 4:
1570 raise RuntimeError("Invalid DN %s" % dnstring)
1571 prefix_len = 4 + len(colons[1]) + int(colons[1])
1572 self.prefix = dnstring[0:prefix_len]
1573 self.binary = self.prefix[3 + len(colons[1]):-1]
1574 self.dnstring = dnstring[prefix_len:]
1575 else:
1576 self.dnstring = dnstring
1577 self.prefix = ''
1578 self.binary = ''
1579 self.dn = ldb.Dn(samdb, self.dnstring)
1581 def __str__(self):
1582 return self.prefix + str(self.dn.extended_str(mode=1))
1584 def __cmp__(self, other):
1585 ''' compare dsdb_Dn values similar to parsed_dn_compare()'''
1586 dn1 = self
1587 dn2 = other
1588 guid1 = dn1.dn.get_extended_component("GUID")
1589 guid2 = dn2.dn.get_extended_component("GUID")
1591 v = cmp(guid1, guid2)
1592 if v != 0:
1593 return v
1594 v = cmp(dn1.binary, dn2.binary)
1595 return v
1597 # In Python3, __cmp__ is replaced by these 6 methods
1598 def __eq__(self, other):
1599 return self.__cmp__(other) == 0
1601 def __ne__(self, other):
1602 return self.__cmp__(other) != 0
1604 def __lt__(self, other):
1605 return self.__cmp__(other) < 0
1607 def __le__(self, other):
1608 return self.__cmp__(other) <= 0
1610 def __gt__(self, other):
1611 return self.__cmp__(other) > 0
1613 def __ge__(self, other):
1614 return self.__cmp__(other) >= 0
1616 def get_binary_integer(self):
1617 '''return binary part of a dsdb_Dn as an integer, or None'''
1618 if self.prefix == '':
1619 return None
1620 return int(self.binary, 16)
1622 def get_bytes(self):
1623 '''return binary as a byte string'''
1624 return binascii.unhexlify(self.binary)