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."""
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
40 __docformat__
= "restructuredText"
43 def get_default_backend_store():
46 class SamDBError(Exception):
49 class SamDBNotFoundError(SamDBError
):
52 class SamDB(samba
.Ldb
):
53 """The SAM database."""
58 class _CleanUpOnError
:
59 def __init__(self
, samdb
, dn
):
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
73 self
.samdb
.delete(self
.dn
)
75 # Don't suppress any exceptions
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):
85 elif url
is None and lp
is not None:
90 super(SamDB
, self
).__init
__(url
=url
, lp
=lp
, modules_dir
=modules_dir
,
91 session_info
=session_info
, credentials
=credentials
, flags
=flags
,
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
)
106 super(SamDB
, self
).connect(url
=url
, flags
=flags
,
110 '''return True if we are an RODC'''
111 return dsdb
._am
_rodc
(self
)
114 '''return True if we are an PDC emulator'''
115 return dsdb
._am
_pdc
(self
)
118 '''return the domain DN'''
119 return str(self
.get_default_basedn())
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
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
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
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"])
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])
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
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
:
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
196 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
197 expression
=search_filter
, attrs
=[])
199 raise Exception('Unable to find user "%s"' % search_filter
)
200 assert(len(res
) == 1)
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
,
223 raise SamDBNotFoundError('Unable to find user "%s"' % search_filter
)
225 raise SamDBError('User "%s" is not unique' % search_filter
)
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
252 group_dn
= "CN=%s,%s,%s" % (groupname
, groupou
, self
.domain_dn())
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
284 ldbmessage
["nTSecurityDescriptor"] = ndr_pack(sd
)
288 def deletegroup(self
, groupname
):
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()
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
)
304 self
.transaction_cancel()
307 self
.transaction_commit()
309 def group_member_filter(self
, member
, member_types
):
312 all_member_types
= [ 'user',
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
362 def add_remove_group_members(self
, groupname
, members
,
363 add_members_operation
=True,
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
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()
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)
389 addtargettogroup
= """
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()
400 membersid
= security
.dom_sid(member
)
401 targetmember_dn
= "<SID=%s>" % str(membersid
)
405 if targetmember_dn
is None:
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
)
411 full_member_dn
= member_dn
412 targetmember_dn
= full_member_dn
.extended_str(1)
413 except ValueError as e
:
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
,
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']]):
435 addtargettogroup
+= """add: member
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']):
441 addtargettogroup
+= """delete: member
443 """ % (str(targetmember_dn
))
446 self
.modify_ldif(addtargettogroup
)
449 self
.transaction_cancel()
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
463 if attr_name
in old
and str(value
) == str(old
[attr_name
]):
469 el
= ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, attr_name
)
474 el
= ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, attr_name
)
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:
485 attrs
= {"givenName": given_name
,
486 "initials": initials
,
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
)
506 return fallback_default
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 +
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
,
559 if useusernameascn
is None and displayname
!= "":
563 user_dn
= "CN=%s,%s,%s" % (cn
, userou
, self
.domain_dn())
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
)
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
633 ldbmessage
["nTSecurityDescriptor"] = ndr_pack(sd
)
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
)
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
,
662 self
.transaction_start()
666 with self
._CleanUpOnError
(self
, user_dn
):
668 self
.modify(ldbmessage2
)
670 # Sets the password for it
672 self
.setpassword(("(distinguishedName=%s)" %
673 ldb
.binary_encode(user_dn
)),
675 force_password_change_at_next_login_req
)
677 self
.transaction_cancel()
680 self
.transaction_commit()
683 fullcontactname
=None,
694 internetaddress
=None,
695 telephonenumber
=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
,
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:
728 if fullcontactname
is None and cn
== "":
729 raise Exception('No name for contact specified')
731 contactcontainer_dn
= self
.domain_dn()
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
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
)
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
)
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
)
823 accountcontrol
= str(dsdb
.UF_WORKSTATION_TRUST_ACCOUNT
)
824 ldbmessage
["userAccountControl"] = accountcontrol
827 ldbmessage
['dNSHostName'] = '{}.{}'.format(
828 cn
, self
.domain_dns_name())
830 self
.transaction_start()
835 password
= cn
.lower()
836 with self
._CleanUpOnError
(self
, computer_dn
):
837 self
.setpassword(("(distinguishedName=%s)" %
838 ldb
.binary_encode(computer_dn
)),
841 self
.transaction_cancel()
844 self
.transaction_commit()
846 def deleteuser(self
, username
):
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()
855 target
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
856 expression
=filter, attrs
=[])
858 raise Exception('Unable to find user "%s"' % username
)
859 assert(len(target
) == 1)
860 self
.delete(target
[0].dn
)
862 self
.transaction_cancel()
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
873 :param password: Password for the user
874 :param force_change_at_next_login: Force password change
876 self
.transaction_start()
878 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
879 expression
=search_filter
, attrs
=[])
881 raise Exception('Unable to find user "%s"' % (username
or search_filter
))
883 raise Exception('Matched %u multiple users with filter "%s"' % (len(res
), search_filter
))
885 if not isinstance(password
, str):
886 pw
= password
.decode('utf-8')
889 pw
= ('"' + pw
+ '"').encode('utf-16-le')
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
)
906 self
.transaction_cancel()
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
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()
921 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
922 expression
=search_filter
,
923 attrs
=["userAccountControl", "accountExpires"])
925 raise Exception('Unable to find user "%s"' % search_filter
)
926 assert(len(res
) == 1)
929 userAccountControl
= int(res
[0]["userAccountControl"][0])
931 userAccountControl
= userAccountControl |
0x10000
934 userAccountControl
= userAccountControl
& ~
0x10000
935 accountExpires
= samba
.unix2nttime(expiry_seconds
+ int(time
.time()))
940 replace: userAccountControl
941 userAccountControl: %u
942 replace: accountExpires
944 """ % (user_dn
, userAccountControl
, accountExpires
)
946 self
.modify_ldif(setexp
)
948 self
.transaction_cancel()
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
,
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
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])
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
,
1064 netbios_domain
= res
[0]["nETBIOSName"][0].decode()
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
):
1087 add: schemaUpdateNow
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
)]
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",
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
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"])
1146 repl
= ndr_unpack(drsblobs
.replPropertyMetaDataBlob
,
1147 res
[0]["replPropertyMetaData"][0])
1149 if len(self
.hash_oid_name
.keys()) == 0:
1150 self
._populate
_oid
_attid
()
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():
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"])
1168 repl
= ndr_unpack(drsblobs
.replPropertyMetaDataBlob
,
1169 res
[0]["replPropertyMetaData"][0])
1171 now
= samba
.unix2nttime(int(time
.time()))
1173 if len(self
.hash_oid_name
.keys()) == 0:
1174 self
._populate
_oid
_attid
()
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():
1181 seq
= self
.sequence_number(ldb
.SEQ_NEXT
)
1183 o
.originating_change_time
= now
1184 o
.originating_invocation_id
= misc
.GUID(self
.get_invocation_id())
1185 o
.originating_usn
= seq
1188 if not found
and addifnotexist
and len(ctr
.array
) > 0:
1189 o2
= drsblobs
.replPropertyMetaData1()
1191 att_oid
= self
.get_oid_from_attid(o2
.attid
)
1192 seq
= self
.sequence_number(ldb
.SEQ_NEXT
)
1194 o2
.originating_change_time
= now
1195 o2
.originating_invocation_id
= misc
.GUID(self
.get_invocation_id())
1196 o2
.originating_usn
= seq
1201 ctr
.count
= ctr
.count
+ 1
1205 replBlob
= ndr_pack(repl
)
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
))
1226 if h_nc
is not None:
1227 dn
= h_nc
.get(wkguid
)
1229 dn
= dsdb
._dsdb
_get
_wellknown
_dn
(self
, nc_root
, wkguid
)
1233 self
.hash_well_known
[str(nc_root
)] = {}
1234 h_nc
= self
.hash_well_known
[str(nc_root
)]
1238 def set_minPwdAge(self
, value
):
1239 if not isinstance(value
, bytes
):
1240 value
= str(value
).encode('utf8')
1242 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
1243 m
["minPwdAge"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "minPwdAge")
1246 def get_minPwdAge(self
):
1247 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["minPwdAge"])
1250 elif "minPwdAge" not in res
[0]:
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')
1259 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
1260 m
["maxPwdAge"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "maxPwdAge")
1263 def get_maxPwdAge(self
):
1264 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["maxPwdAge"])
1267 elif "maxPwdAge" not in res
[0]:
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')
1276 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
1277 m
["minPwdLength"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "minPwdLength")
1280 def get_minPwdLength(self
):
1281 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["minPwdLength"])
1284 elif "minPwdLength" not in res
[0]:
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')
1293 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
1294 m
["pwdProperties"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "pwdProperties")
1297 def get_pwdProperties(self
):
1298 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["pwdProperties"])
1301 elif "pwdProperties" not in res
[0]:
1304 return int(res
[0]["pwdProperties"][0])
1306 def set_dsheuristics(self
, dsheuristics
):
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
,
1316 m
["dSHeuristics"] = \
1317 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
,
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"])
1327 elif "dSHeuristics" in res
[0]:
1328 dsheuristics
= res
[0]["dSHeuristics"][0]
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
1343 "objectClass": "organizationalUnit"}
1346 m
["description"] = description
1351 m
["nTSecurityDescriptor"] = ndr_pack(sd
)
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()
1360 seq
= super(SamDB
, self
).sequence_number(seq_type
)
1362 self
.transaction_cancel()
1365 self
.transaction_commit()
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
)
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
,
1418 return dsdb
._dsdb
_garbage
_collect
_tombstones
(self
, dn
,
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
1438 rid
, _
= self
.free_rid_bounds()
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"])
1456 server_ref
= res
[0]["serverReference"]
1459 ldb
.ERR_NO_SUCH_ATTRIBUTE
,
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"])
1469 rid_set_refs
= res
[0]["rIDSetReferences"]
1472 ldb
.ERR_NO_SUCH_ATTRIBUTE
,
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",
1485 uint32_max
= 2**32 - 1
1486 uint64_max
= 2**64 - 1
1489 alloc_pool
= int(res
[0]["rIDAllocationPool"][0])
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
)
1497 prev_pool
= int(res
[0]["rIDPreviousAllocationPool"][0])
1499 prev_pool
= uint64_max
1501 next_rid
= int(res
[0]["rIDNextRID"][0])
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
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
1541 :param dn: relative dn
1543 domain_dn
= ldb
.Dn(self
, self
.domain_dn())
1545 if isinstance(dn
, ldb
.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
)
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
1565 syntax_oid
= dsdb
.DSDB_SYNTAX_OR_NAME
1566 if syntax_oid
in [dsdb
.DSDB_SYNTAX_BINARY_DN
, dsdb
.DSDB_SYNTAX_STRING_DN
]:
1568 colons
= dnstring
.split(':')
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
:]
1576 self
.dnstring
= dnstring
1579 self
.dn
= ldb
.Dn(samdb
, self
.dnstring
)
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()'''
1588 guid1
= dn1
.dn
.get_extended_component("GUID")
1589 guid2
= dn2
.dn
.get_extended_component("GUID")
1591 v
= cmp(guid1
, guid2
)
1594 v
= cmp(dn1
.binary
, dn2
.binary
)
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
== '':
1620 return int(self
.binary
, 16)
1622 def get_bytes(self
):
1623 '''return binary as a byte string'''
1624 return binascii
.unhexlify(self
.binary
)