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."""
57 class _CleanUpOnError
:
58 def __init__(self
, samdb
, dn
):
65 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
66 if exc_type
is not None:
67 # We failed to modify the account. If we connected to the
68 # database over LDAP, we don't have transactions, and so when
69 # we call transaction_cancel(), the account will still exist in
70 # a half-created state. We'll delete the account to ensure that
72 self
.samdb
.delete(self
.dn
)
74 # Don't suppress any exceptions
77 def __init__(self
, url
=None, lp
=None, modules_dir
=None, session_info
=None,
78 credentials
=None, flags
=ldb
.FLG_DONT_CREATE_DB
,
79 options
=None, global_schema
=True,
80 auto_connect
=True, am_rodc
=None):
84 elif url
is None and lp
is not None:
89 super().__init
__(url
=url
, lp
=lp
, modules_dir
=modules_dir
,
90 session_info
=session_info
, credentials
=credentials
, flags
=flags
,
94 dsdb
._dsdb
_set
_global
_schema
(self
)
96 if am_rodc
is not None:
97 dsdb
._dsdb
_set
_am
_rodc
(self
, am_rodc
)
99 def connect(self
, url
=None, flags
=0, options
=None):
100 """connect to the database"""
101 if self
.lp
is not None and not os
.path
.exists(url
):
102 url
= self
.lp
.private_path(url
)
105 super().connect(url
=url
, flags
=flags
, options
=options
)
109 return f
"<SamDB {id(self):x} ({self.url})>"
111 return f
"<SamDB {id(self):x} (no connection)>"
116 """return True if we are an RODC"""
117 return dsdb
._am
_rodc
(self
)
120 """return True if we are an PDC emulator"""
121 return dsdb
._am
_pdc
(self
)
124 """return the domain DN"""
125 return str(self
.get_default_basedn())
128 """return the schema partition dn"""
129 return str(self
.get_schema_basedn())
131 def disable_account(self
, search_filter
):
132 """Disables an account
134 :param search_filter: LDAP filter to find the user (eg
138 flags
= samba
.dsdb
.UF_ACCOUNTDISABLE
139 self
.toggle_userAccountFlags(search_filter
, flags
, on
=True)
141 def enable_account(self
, search_filter
):
142 """Enables an account
144 :param search_filter: LDAP filter to find the user (eg
148 flags
= samba
.dsdb
.UF_ACCOUNTDISABLE | samba
.dsdb
.UF_PASSWD_NOTREQD
149 self
.toggle_userAccountFlags(search_filter
, flags
, on
=False)
151 def toggle_userAccountFlags(self
, search_filter
, flags
, flags_str
=None,
152 on
=True, strict
=False):
153 """Toggle_userAccountFlags
155 :param search_filter: LDAP filter to find the user (eg
157 :param flags: samba.dsdb.UF_* flags
158 :param on: on=True (default) => set, on=False => unset
159 :param strict: strict=False (default) ignore if no action is needed
160 strict=True raises an Exception if...
162 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
163 expression
=search_filter
, attrs
=["userAccountControl"])
165 raise Exception("Unable to find account where '%s'" % search_filter
)
166 assert(len(res
) == 1)
167 account_dn
= res
[0].dn
169 old_uac
= int(res
[0]["userAccountControl"][0])
171 if strict
and (old_uac
& flags
):
172 error
= "Account flag(s) '%s' already set" % flags_str
173 raise Exception(error
)
175 new_uac
= old_uac | flags
177 if strict
and not (old_uac
& flags
):
178 error
= "Account flag(s) '%s' already unset" % flags_str
179 raise Exception(error
)
181 new_uac
= old_uac
& ~flags
183 if old_uac
== new_uac
:
189 delete: userAccountControl
190 userAccountControl: %u
191 add: userAccountControl
192 userAccountControl: %u
193 """ % (account_dn
, old_uac
, new_uac
)
194 self
.modify_ldif(mod
)
196 def force_password_change_at_next_login(self
, search_filter
):
197 """Forces a password change at next login
199 :param search_filter: LDAP filter to find the user (eg
202 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
203 expression
=search_filter
, attrs
=[])
205 raise Exception('Unable to find user "%s"' % search_filter
)
206 assert(len(res
) == 1)
215 self
.modify_ldif(mod
)
217 def unlock_account(self
, search_filter
):
218 """Unlock a user account by resetting lockoutTime to 0.
219 This does also reset the badPwdCount to 0.
221 :param search_filter: LDAP filter to find the user (e.g.
222 sAMAccountName=username)
224 res
= self
.search(base
=self
.domain_dn(),
225 scope
=ldb
.SCOPE_SUBTREE
,
226 expression
=search_filter
,
229 raise SamDBNotFoundError('Unable to find user "%s"' % search_filter
)
231 raise SamDBError('User "%s" is not unique' % search_filter
)
240 self
.modify_ldif(mod
)
242 def newgroup(self
, groupname
, groupou
=None, grouptype
=None,
243 description
=None, mailaddress
=None, notes
=None, sd
=None,
244 gidnumber
=None, nisdomain
=None):
245 """Adds a new group with additional parameters
247 :param groupname: Name of the new group
248 :param grouptype: Type of the new group
249 :param description: Description of the new group
250 :param mailaddress: Email address of the new group
251 :param notes: Notes of the new group
252 :param gidnumber: GID Number of the new group
253 :param nisdomain: NIS Domain Name of the new group
254 :param sd: security descriptor of the object
258 group_dn
= "CN=%s,%s,%s" % (groupname
, groupou
, self
.domain_dn())
260 group_dn
= "CN=%s,%s" % (groupname
, self
.get_wellknown_dn(
261 self
.get_default_basedn(),
262 dsdb
.DS_GUID_USERS_CONTAINER
))
264 # The new user record. Note the reliance on the SAMLDB module which
265 # fills in the default information
266 ldbmessage
= {"dn": group_dn
,
267 "sAMAccountName": groupname
,
268 "objectClass": "group"}
270 if grouptype
is not None:
271 ldbmessage
["groupType"] = normalise_int32(grouptype
)
273 if description
is not None:
274 ldbmessage
["description"] = description
276 if mailaddress
is not None:
277 ldbmessage
["mail"] = mailaddress
279 if notes
is not None:
280 ldbmessage
["info"] = notes
282 if gidnumber
is not None:
283 ldbmessage
["gidNumber"] = normalise_int32(gidnumber
)
285 if nisdomain
is not None:
286 ldbmessage
["msSFU30Name"] = groupname
287 ldbmessage
["msSFU30NisDomain"] = nisdomain
290 ldbmessage
["nTSecurityDescriptor"] = ndr_pack(sd
)
294 def deletegroup(self
, groupname
):
297 :param groupname: Name of the target group
300 groupfilter
= "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb
.binary_encode(groupname
), "CN=Group,CN=Schema,CN=Configuration", self
.domain_dn())
301 self
.transaction_start()
303 targetgroup
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
304 expression
=groupfilter
, attrs
=[])
305 if len(targetgroup
) == 0:
306 raise Exception('Unable to find group "%s"' % groupname
)
307 assert(len(targetgroup
) == 1)
308 self
.delete(targetgroup
[0].dn
)
310 self
.transaction_cancel()
313 self
.transaction_commit()
315 def group_member_filter(self
, member
, member_types
):
318 all_member_types
= [ 'user',
325 if 'all' in member_types
:
326 member_types
= all_member_types
328 for member_type
in member_types
:
329 if member_type
not in all_member_types
:
330 raise Exception('Invalid group member type "%s". '
331 'Valid types are %s and all.' %
332 (member_type
, ", ".join(all_member_types
)))
334 if 'user' in member_types
:
335 filter += ('(&(sAMAccountName=%s)(samAccountType=%d))' %
336 (ldb
.binary_encode(member
), dsdb
.ATYPE_NORMAL_ACCOUNT
))
337 if 'group' in member_types
:
338 filter += ('(&(sAMAccountName=%s)'
339 '(objectClass=group)'
340 '(!(groupType:1.2.840.113556.1.4.803:=1)))' %
341 ldb
.binary_encode(member
))
342 if 'computer' in member_types
:
343 samaccountname
= member
344 if member
[-1] != '$':
345 samaccountname
= "%s$" % member
346 filter += ('(&(samAccountType=%d)'
347 '(!(objectCategory=msDS-ManagedServiceAccount))'
348 '(sAMAccountName=%s))' %
349 (dsdb
.ATYPE_WORKSTATION_TRUST
,
350 ldb
.binary_encode(samaccountname
)))
351 if 'serviceaccount' in member_types
:
352 samaccountname
= member
353 if member
[-1] != '$':
354 samaccountname
= "%s$" % member
355 filter += ('(&(samAccountType=%d)'
356 '(objectCategory=msDS-ManagedServiceAccount)'
357 '(sAMAccountName=%s))' %
358 (dsdb
.ATYPE_WORKSTATION_TRUST
,
359 ldb
.binary_encode(samaccountname
)))
360 if 'contact' in member_types
:
361 filter += ('(&(objectCategory=Person)(!(objectSid=*))(name=%s))' %
362 ldb
.binary_encode(member
))
364 filter = "(|%s)" % filter
368 def add_remove_group_members(self
, groupname
, members
,
369 add_members_operation
=True,
371 member_base_dn
=None):
372 """Adds or removes group members
374 :param groupname: Name of the target group
375 :param members: list of group members
376 :param add_members_operation: Defines if its an add or remove
379 if member_types
is None:
380 member_types
= ['user', 'group', 'computer']
382 groupfilter
= "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (
383 ldb
.binary_encode(groupname
), "CN=Group,CN=Schema,CN=Configuration", self
.domain_dn())
385 self
.transaction_start()
387 targetgroup
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
388 expression
=groupfilter
, attrs
=['member'])
389 if len(targetgroup
) == 0:
390 raise Exception('Unable to find group "%s"' % groupname
)
391 assert(len(targetgroup
) == 1)
395 addtargettogroup
= """
398 """ % (str(targetgroup
[0].dn
))
400 for member
in members
:
401 targetmember_dn
= None
402 if member_base_dn
is None:
403 member_base_dn
= self
.domain_dn()
406 membersid
= security
.dom_sid(member
)
407 targetmember_dn
= "<SID=%s>" % str(membersid
)
411 if targetmember_dn
is None:
413 member_dn
= ldb
.Dn(self
, member
)
414 if member_dn
.get_linearized() == member_dn
.extended_str(1):
415 full_member_dn
= self
.normalize_dn_in_domain(member_dn
)
417 full_member_dn
= member_dn
418 targetmember_dn
= full_member_dn
.extended_str(1)
419 except ValueError as e
:
422 if targetmember_dn
is None:
423 filter = self
.group_member_filter(member
, member_types
)
424 targetmember
= self
.search(base
=member_base_dn
,
425 scope
=ldb
.SCOPE_SUBTREE
,
429 if len(targetmember
) > 1:
430 targetmemberlist_str
= ""
431 for msg
in targetmember
:
432 targetmemberlist_str
+= "%s\n" % msg
.get("dn")
433 raise Exception('Found multiple results for "%s":\n%s' %
434 (member
, targetmemberlist_str
))
435 if len(targetmember
) != 1:
436 raise Exception('Unable to find "%s". Operation cancelled.' % member
)
437 targetmember_dn
= targetmember
[0].dn
.extended_str(1)
439 if add_members_operation
is True and (targetgroup
[0].get('member') is None or get_bytes(targetmember_dn
) not in [str(x
) for x
in targetgroup
[0]['member']]):
441 addtargettogroup
+= """add: member
443 """ % (str(targetmember_dn
))
445 elif add_members_operation
is False and (targetgroup
[0].get('member') is not None and get_bytes(targetmember_dn
) in targetgroup
[0]['member']):
447 addtargettogroup
+= """delete: member
449 """ % (str(targetmember_dn
))
452 self
.modify_ldif(addtargettogroup
)
455 self
.transaction_cancel()
458 self
.transaction_commit()
460 def prepare_attr_replace(self
, msg
, old
, attr_name
, value
):
461 """Changes the MessageElement with the given attr_name of the
462 given Message. If the value is "" set an empty value and the flag
463 FLAG_MOD_DELETE, otherwise set the new value and FLAG_MOD_REPLACE.
464 If the value is None or the Message contains the attr_name with this
465 value, nothing will changed."""
466 # skip unchanged attribute
469 if attr_name
in old
and str(value
) == str(old
[attr_name
]):
475 el
= ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
, attr_name
)
480 el
= ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, attr_name
)
483 def fullname_from_names(self
, given_name
=None, initials
=None, surname
=None,
484 old_attrs
=None, fallback_default
=""):
485 """Prepares new combined fullname, using the name parts.
486 Used for things like displayName or cn.
487 Use the original name values, if no new one is specified."""
488 if old_attrs
is None:
491 attrs
= {"givenName": given_name
,
492 "initials": initials
,
495 # if the attribute is not specified, try to use the old one
496 for attr_name
, attr_value
in attrs
.items():
497 if attr_value
is None and attr_name
in old_attrs
:
498 attrs
[attr_name
] = str(old_attrs
[attr_name
])
500 # add '.' to initials if initials are not None and not "" and if the initials
501 # don't have already a '.' at the end
502 if attrs
["initials"] and not attrs
["initials"].endswith('.'):
503 attrs
["initials"] += '.'
505 # remove empty values (None and '')
506 attrs_values
= list(filter(None, attrs
.values()))
508 # fullname is the combination of not-empty values as string, separated by ' '
509 fullname
= ' '.join(attrs_values
)
512 return fallback_default
516 def newuser(self
, username
, password
,
517 force_password_change_at_next_login_req
=False,
518 useusernameascn
=False, userou
=None, surname
=None, givenname
=None,
519 initials
=None, profilepath
=None, scriptpath
=None, homedrive
=None,
520 homedirectory
=None, jobtitle
=None, department
=None, company
=None,
521 description
=None, mailaddress
=None, internetaddress
=None,
522 telephonenumber
=None, physicaldeliveryoffice
=None, sd
=None,
523 setpassword
=True, uidnumber
=None, gidnumber
=None, gecos
=None,
524 loginshell
=None, uid
=None, nisdomain
=None, unixhome
=None,
525 smartcard_required
=False):
526 """Adds a new user with additional parameters
528 :param username: Name of the new user
529 :param password: Password for the new user
530 :param force_password_change_at_next_login_req: Force password change
531 :param useusernameascn: Use username as cn rather that firstname +
533 :param userou: Object container (without domainDN postfix) for new user
534 :param surname: Surname of the new user
535 :param givenname: First name of the new user
536 :param initials: Initials of the new user
537 :param profilepath: Profile path of the new user
538 :param scriptpath: Logon script path of the new user
539 :param homedrive: Home drive of the new user
540 :param homedirectory: Home directory of the new user
541 :param jobtitle: Job title of the new user
542 :param department: Department of the new user
543 :param company: Company of the new user
544 :param description: of the new user
545 :param mailaddress: Email address of the new user
546 :param internetaddress: Home page of the new user
547 :param telephonenumber: Phone number of the new user
548 :param physicaldeliveryoffice: Office location of the new user
549 :param sd: security descriptor of the object
550 :param setpassword: optionally disable password reset
551 :param uidnumber: RFC2307 Unix numeric UID of the new user
552 :param gidnumber: RFC2307 Unix primary GID of the new user
553 :param gecos: RFC2307 Unix GECOS field of the new user
554 :param loginshell: RFC2307 Unix login shell of the new user
555 :param uid: RFC2307 Unix username of the new user
556 :param nisdomain: RFC2307 Unix NIS domain of the new user
557 :param unixhome: RFC2307 Unix home directory of the new user
558 :param smartcard_required: set the UF_SMARTCARD_REQUIRED bit of the new user
561 displayname
= self
.fullname_from_names(given_name
=givenname
,
565 if useusernameascn
is None and displayname
!= "":
569 user_dn
= "CN=%s,%s,%s" % (cn
, userou
, self
.domain_dn())
571 user_dn
= "CN=%s,%s" % (cn
, self
.get_wellknown_dn(
572 self
.get_default_basedn(),
573 dsdb
.DS_GUID_USERS_CONTAINER
))
575 dnsdomain
= ldb
.Dn(self
, self
.domain_dn()).canonical_str().replace("/", "")
576 user_principal_name
= "%s@%s" % (username
, dnsdomain
)
577 # The new user record. Note the reliance on the SAMLDB module which
578 # fills in the default information
579 ldbmessage
= {"dn": user_dn
,
580 "sAMAccountName": username
,
581 "userPrincipalName": user_principal_name
,
582 "objectClass": "user"}
584 if smartcard_required
:
585 ldbmessage
["userAccountControl"] = str(dsdb
.UF_NORMAL_ACCOUNT |
586 dsdb
.UF_SMARTCARD_REQUIRED
)
589 if surname
is not None:
590 ldbmessage
["sn"] = surname
592 if givenname
is not None:
593 ldbmessage
["givenName"] = givenname
595 if displayname
!= "":
596 ldbmessage
["displayName"] = displayname
597 ldbmessage
["name"] = displayname
599 if initials
is not None:
600 ldbmessage
["initials"] = '%s.' % initials
602 if profilepath
is not None:
603 ldbmessage
["profilePath"] = profilepath
605 if scriptpath
is not None:
606 ldbmessage
["scriptPath"] = scriptpath
608 if homedrive
is not None:
609 ldbmessage
["homeDrive"] = homedrive
611 if homedirectory
is not None:
612 ldbmessage
["homeDirectory"] = homedirectory
614 if jobtitle
is not None:
615 ldbmessage
["title"] = jobtitle
617 if department
is not None:
618 ldbmessage
["department"] = department
620 if company
is not None:
621 ldbmessage
["company"] = company
623 if description
is not None:
624 ldbmessage
["description"] = description
626 if mailaddress
is not None:
627 ldbmessage
["mail"] = mailaddress
629 if internetaddress
is not None:
630 ldbmessage
["wWWHomePage"] = internetaddress
632 if telephonenumber
is not None:
633 ldbmessage
["telephoneNumber"] = telephonenumber
635 if physicaldeliveryoffice
is not None:
636 ldbmessage
["physicalDeliveryOfficeName"] = physicaldeliveryoffice
639 ldbmessage
["nTSecurityDescriptor"] = ndr_pack(sd
)
642 if any(map(lambda b
: b
is not None, (uid
, uidnumber
, gidnumber
, gecos
,
643 loginshell
, nisdomain
, unixhome
))):
644 ldbmessage2
= ldb
.Message()
645 ldbmessage2
.dn
= ldb
.Dn(self
, user_dn
)
647 ldbmessage2
["uid"] = ldb
.MessageElement(str(uid
), ldb
.FLAG_MOD_REPLACE
, 'uid')
648 if uidnumber
is not None:
649 ldbmessage2
["uidNumber"] = ldb
.MessageElement(str(uidnumber
), ldb
.FLAG_MOD_REPLACE
, 'uidNumber')
650 if gidnumber
is not None:
651 ldbmessage2
["gidNumber"] = ldb
.MessageElement(str(gidnumber
), ldb
.FLAG_MOD_REPLACE
, 'gidNumber')
652 if gecos
is not None:
653 ldbmessage2
["gecos"] = ldb
.MessageElement(str(gecos
), ldb
.FLAG_MOD_REPLACE
, 'gecos')
654 if loginshell
is not None:
655 ldbmessage2
["loginShell"] = ldb
.MessageElement(str(loginshell
), ldb
.FLAG_MOD_REPLACE
, 'loginShell')
656 if unixhome
is not None:
657 ldbmessage2
["unixHomeDirectory"] = ldb
.MessageElement(
658 str(unixhome
), ldb
.FLAG_MOD_REPLACE
, 'unixHomeDirectory')
659 if nisdomain
is not None:
660 ldbmessage2
["msSFU30NisDomain"] = ldb
.MessageElement(
661 str(nisdomain
), ldb
.FLAG_MOD_REPLACE
, 'msSFU30NisDomain')
662 ldbmessage2
["msSFU30Name"] = ldb
.MessageElement(
663 str(username
), ldb
.FLAG_MOD_REPLACE
, 'msSFU30Name')
664 ldbmessage2
["unixUserPassword"] = ldb
.MessageElement(
665 'ABCD!efgh12345$67890', ldb
.FLAG_MOD_REPLACE
,
668 self
.transaction_start()
672 with self
._CleanUpOnError
(self
, user_dn
):
674 self
.modify(ldbmessage2
)
676 # Sets the password for it
678 self
.setpassword(("(distinguishedName=%s)" %
679 ldb
.binary_encode(user_dn
)),
681 force_password_change_at_next_login_req
)
683 self
.transaction_cancel()
686 self
.transaction_commit()
689 fullcontactname
=None,
700 internetaddress
=None,
701 telephonenumber
=None,
703 physicaldeliveryoffice
=None):
704 """Adds a new contact with additional parameters
706 :param fullcontactname: Optional full name of the new contact
707 :param ou: Object container for new contact
708 :param surname: Surname of the new contact
709 :param givenname: First name of the new contact
710 :param initials: Initials of the new contact
711 :param displayname: displayName of the new contact
712 :param jobtitle: Job title of the new contact
713 :param department: Department of the new contact
714 :param company: Company of the new contact
715 :param description: Description of the new contact
716 :param mailaddress: Email address of the new contact
717 :param internetaddress: Home page of the new contact
718 :param telephonenumber: Phone number of the new contact
719 :param mobilenumber: Primary mobile number of the new contact
720 :param physicaldeliveryoffice: Office location of the new contact
723 # Prepare the contact name like the RSAT, using the name parts.
724 cn
= self
.fullname_from_names(given_name
=givenname
,
728 # Use the specified fullcontactname instead of the previously prepared
729 # contact name, if it is specified.
730 # This is similar to the "Full name" value of the RSAT.
731 if fullcontactname
is not None:
734 if fullcontactname
is None and cn
== "":
735 raise Exception('No name for contact specified')
737 contactcontainer_dn
= self
.domain_dn()
739 contactcontainer_dn
= self
.normalize_dn_in_domain(ou
)
741 contact_dn
= "CN=%s,%s" % (cn
, contactcontainer_dn
)
743 ldbmessage
= {"dn": contact_dn
,
744 "objectClass": "contact",
747 if surname
is not None:
748 ldbmessage
["sn"] = surname
750 if givenname
is not None:
751 ldbmessage
["givenName"] = givenname
753 if displayname
is not None:
754 ldbmessage
["displayName"] = displayname
756 if initials
is not None:
757 ldbmessage
["initials"] = '%s.' % initials
759 if jobtitle
is not None:
760 ldbmessage
["title"] = jobtitle
762 if department
is not None:
763 ldbmessage
["department"] = department
765 if company
is not None:
766 ldbmessage
["company"] = company
768 if description
is not None:
769 ldbmessage
["description"] = description
771 if mailaddress
is not None:
772 ldbmessage
["mail"] = mailaddress
774 if internetaddress
is not None:
775 ldbmessage
["wWWHomePage"] = internetaddress
777 if telephonenumber
is not None:
778 ldbmessage
["telephoneNumber"] = telephonenumber
780 if mobilenumber
is not None:
781 ldbmessage
["mobile"] = mobilenumber
783 if physicaldeliveryoffice
is not None:
784 ldbmessage
["physicalDeliveryOfficeName"] = physicaldeliveryoffice
790 def newcomputer(self
, computername
, computerou
=None, description
=None,
791 prepare_oldjoin
=False, ip_address_list
=None,
792 service_principal_name_list
=None):
793 """Adds a new user with additional parameters
795 :param computername: Name of the new computer
796 :param computerou: Object container for new computer
797 :param description: Description of the new computer
798 :param prepare_oldjoin: Preset computer password for oldjoin mechanism
799 :param ip_address_list: ip address list for DNS A or AAAA record
800 :param service_principal_name_list: string list of servicePincipalName
803 cn
= re
.sub(r
"\$$", "", computername
)
805 raise Exception('Illegal computername "%s"' % computername
)
806 samaccountname
= "%s$" % cn
808 computercontainer_dn
= self
.get_wellknown_dn(self
.get_default_basedn(),
809 dsdb
.DS_GUID_COMPUTERS_CONTAINER
)
811 computercontainer_dn
= self
.normalize_dn_in_domain(computerou
)
813 computer_dn
= "CN=%s,%s" % (cn
, computercontainer_dn
)
815 ldbmessage
= {"dn": computer_dn
,
816 "sAMAccountName": samaccountname
,
817 "objectClass": "computer",
820 if description
is not None:
821 ldbmessage
["description"] = description
823 if service_principal_name_list
:
824 ldbmessage
["servicePrincipalName"] = service_principal_name_list
826 accountcontrol
= str(dsdb
.UF_WORKSTATION_TRUST_ACCOUNT |
827 dsdb
.UF_ACCOUNTDISABLE
)
829 accountcontrol
= str(dsdb
.UF_WORKSTATION_TRUST_ACCOUNT
)
830 ldbmessage
["userAccountControl"] = accountcontrol
833 ldbmessage
['dNSHostName'] = '{}.{}'.format(
834 cn
, self
.domain_dns_name())
836 self
.transaction_start()
841 password
= cn
.lower()
842 with self
._CleanUpOnError
(self
, computer_dn
):
843 self
.setpassword(("(distinguishedName=%s)" %
844 ldb
.binary_encode(computer_dn
)),
847 self
.transaction_cancel()
850 self
.transaction_commit()
852 def deleteuser(self
, username
):
855 :param username: Name of the target user
858 filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb
.binary_encode(username
), "CN=Person,CN=Schema,CN=Configuration", self
.domain_dn())
859 self
.transaction_start()
861 target
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
862 expression
=filter, attrs
=[])
864 raise Exception('Unable to find user "%s"' % username
)
865 assert(len(target
) == 1)
866 self
.delete(target
[0].dn
)
868 self
.transaction_cancel()
871 self
.transaction_commit()
873 def setpassword(self
, search_filter
, password
,
874 force_change_at_next_login
=False, username
=None):
875 """Sets the password for a user
877 :param search_filter: LDAP filter to find the user (eg
879 :param password: Password for the user
880 :param force_change_at_next_login: Force password change
882 self
.transaction_start()
884 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
885 expression
=search_filter
, attrs
=[])
887 raise Exception('Unable to find user "%s"' % (username
or search_filter
))
889 raise Exception('Matched %u multiple users with filter "%s"' % (len(res
), search_filter
))
891 if not isinstance(password
, str):
892 pw
= password
.decode('utf-8')
895 pw
= ('"' + pw
+ '"').encode('utf-16-le')
901 """ % (user_dn
, base64
.b64encode(pw
).decode('utf-8'))
903 self
.modify_ldif(setpw
)
905 if force_change_at_next_login
:
906 self
.force_password_change_at_next_login(
907 "(distinguishedName=" + str(user_dn
) + ")")
909 # modify the userAccountControl to remove the disabled bit
910 self
.enable_account(search_filter
)
912 self
.transaction_cancel()
915 self
.transaction_commit()
917 def setexpiry(self
, search_filter
, expiry_seconds
, no_expiry_req
=False):
918 """Sets the account expiry for a user
920 :param search_filter: LDAP filter to find the user (eg
922 :param expiry_seconds: expiry time from now in seconds
923 :param no_expiry_req: if set, then don't expire password
925 self
.transaction_start()
927 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
928 expression
=search_filter
,
929 attrs
=["userAccountControl", "accountExpires"])
931 raise Exception('Unable to find user "%s"' % search_filter
)
932 assert(len(res
) == 1)
935 userAccountControl
= int(res
[0]["userAccountControl"][0])
937 userAccountControl
= userAccountControl |
0x10000
940 userAccountControl
= userAccountControl
& ~
0x10000
941 accountExpires
= samba
.unix2nttime(expiry_seconds
+ int(time
.time()))
946 replace: userAccountControl
947 userAccountControl: %u
948 replace: accountExpires
950 """ % (user_dn
, userAccountControl
, accountExpires
)
952 self
.modify_ldif(setexp
)
954 self
.transaction_cancel()
957 self
.transaction_commit()
959 def set_domain_sid(self
, sid
):
960 """Change the domain SID used by this LDB.
962 :param sid: The new domain sid to use.
964 dsdb
._samdb
_set
_domain
_sid
(self
, sid
)
966 def get_domain_sid(self
):
967 """Read the domain SID used by this LDB. """
968 return dsdb
._samdb
_get
_domain
_sid
(self
)
970 domain_sid
= property(get_domain_sid
, set_domain_sid
,
971 doc
="SID for the domain")
973 def get_connecting_user_sid(self
):
974 """Returns the SID of the connected user."""
975 msg
= self
.search(base
="", scope
=ldb
.SCOPE_BASE
, attrs
=["tokenGroups"])[0]
976 return str(ndr_unpack(security
.dom_sid
, msg
["tokenGroups"][0]))
978 connecting_user_sid
= property(get_connecting_user_sid
,
979 doc
="SID of the connecting user")
981 def set_invocation_id(self
, invocation_id
):
982 """Set the invocation id for this SamDB handle.
984 :param invocation_id: GUID of the invocation id.
986 dsdb
._dsdb
_set
_ntds
_invocation
_id
(self
, invocation_id
)
988 def get_invocation_id(self
):
989 """Get the invocation_id id"""
990 return dsdb
._samdb
_ntds
_invocation
_id
(self
)
992 invocation_id
= property(get_invocation_id
, set_invocation_id
,
993 doc
="Invocation ID GUID")
995 def get_oid_from_attid(self
, attid
):
996 return dsdb
._dsdb
_get
_oid
_from
_attid
(self
, attid
)
998 def get_attid_from_lDAPDisplayName(self
, ldap_display_name
,
1000 """return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI"""
1001 return dsdb
._dsdb
_get
_attid
_from
_lDAPDisplayName
(self
,
1002 ldap_display_name
, is_schema_nc
)
1004 def get_syntax_oid_from_lDAPDisplayName(self
, ldap_display_name
):
1005 """return the syntax OID for a LDAP attribute as a string"""
1006 return dsdb
._dsdb
_get
_syntax
_oid
_from
_lDAPDisplayName
(self
, ldap_display_name
)
1008 def get_systemFlags_from_lDAPDisplayName(self
, ldap_display_name
):
1009 """return the systemFlags for a LDAP attribute as a integer"""
1010 return dsdb
._dsdb
_get
_systemFlags
_from
_lDAPDisplayName
(self
, ldap_display_name
)
1012 def get_linkId_from_lDAPDisplayName(self
, ldap_display_name
):
1013 """return the linkID for a LDAP attribute as a integer"""
1014 return dsdb
._dsdb
_get
_linkId
_from
_lDAPDisplayName
(self
, ldap_display_name
)
1016 def get_lDAPDisplayName_by_attid(self
, attid
):
1017 """return the lDAPDisplayName from an integer DRS attribute ID"""
1018 return dsdb
._dsdb
_get
_lDAPDisplayName
_by
_attid
(self
, attid
)
1020 def get_backlink_from_lDAPDisplayName(self
, ldap_display_name
):
1021 """return the attribute name of the corresponding backlink from the name
1022 of a forward link attribute. If there is no backlink return None"""
1023 return dsdb
._dsdb
_get
_backlink
_from
_lDAPDisplayName
(self
, ldap_display_name
)
1025 def set_ntds_settings_dn(self
, ntds_settings_dn
):
1026 """Set the NTDS Settings DN, as would be returned on the dsServiceName
1029 This allows the DN to be set before the database fully exists
1031 :param ntds_settings_dn: The new DN to use
1033 dsdb
._samdb
_set
_ntds
_settings
_dn
(self
, ntds_settings_dn
)
1035 def get_ntds_GUID(self
):
1036 """Get the NTDS objectGUID"""
1037 return dsdb
._samdb
_ntds
_objectGUID
(self
)
1039 def get_timestr(self
):
1040 """Get the current time as generalized time string"""
1041 res
= self
.search(base
="",
1042 scope
=ldb
.SCOPE_BASE
,
1043 attrs
=["currentTime"])
1044 return str(res
[0]["currentTime"][0])
1047 """Get the current time as UNIX time"""
1048 return ldb
.string_to_time(self
.get_timestr())
1050 def get_nttime(self
):
1051 """Get the current time as NT time"""
1052 return samba
.unix2nttime(self
.get_time())
1054 def server_site_name(self
):
1055 """Get the server site name"""
1056 return dsdb
._samdb
_server
_site
_name
(self
)
1058 def host_dns_name(self
):
1059 """return the DNS name of this host"""
1060 res
= self
.search(base
='', scope
=ldb
.SCOPE_BASE
, attrs
=['dNSHostName'])
1061 return str(res
[0]['dNSHostName'][0])
1063 def domain_dns_name(self
):
1064 """return the DNS name of the domain root"""
1065 domain_dn
= self
.get_default_basedn()
1066 return domain_dn
.canonical_str().split('/')[0]
1068 def domain_netbios_name(self
):
1069 """return the NetBIOS name of the domain root"""
1070 domain_dn
= self
.get_default_basedn()
1071 dns_name
= self
.domain_dns_name()
1072 filter = "(&(objectClass=crossRef)(nETBIOSName=*)(ncName=%s)(dnsroot=%s))" % (domain_dn
, dns_name
)
1073 partitions_dn
= self
.get_partitions_dn()
1074 res
= self
.search(partitions_dn
,
1075 scope
=ldb
.SCOPE_ONELEVEL
,
1078 netbios_domain
= res
[0]["nETBIOSName"][0].decode()
1081 return netbios_domain
1083 def forest_dns_name(self
):
1084 """return the DNS name of the forest root"""
1085 forest_dn
= self
.get_root_basedn()
1086 return forest_dn
.canonical_str().split('/')[0]
1088 def load_partition_usn(self
, base_dn
):
1089 return dsdb
._dsdb
_load
_partition
_usn
(self
, base_dn
)
1091 def set_schema(self
, schema
, write_indices_and_attributes
=True):
1092 self
.set_schema_from_ldb(schema
.ldb
, write_indices_and_attributes
=write_indices_and_attributes
)
1094 def set_schema_from_ldb(self
, ldb_conn
, write_indices_and_attributes
=True):
1095 dsdb
._dsdb
_set
_schema
_from
_ldb
(self
, ldb_conn
, write_indices_and_attributes
)
1097 def set_schema_update_now(self
):
1101 add: schemaUpdateNow
1104 self
.modify_ldif(ldif
)
1106 def dsdb_DsReplicaAttribute(self
, ldb
, ldap_display_name
, ldif_elements
):
1107 """convert a list of attribute values to a DRSUAPI DsReplicaAttribute"""
1108 return dsdb
._dsdb
_DsReplicaAttribute
(ldb
, ldap_display_name
, ldif_elements
)
1110 def dsdb_normalise_attributes(self
, ldb
, ldap_display_name
, ldif_elements
):
1111 """normalise a list of attribute values"""
1112 return dsdb
._dsdb
_normalise
_attributes
(ldb
, ldap_display_name
, ldif_elements
)
1114 def get_attribute_from_attid(self
, attid
):
1115 """ Get from an attid the associated attribute
1117 :param attid: The attribute id for searched attribute
1118 :return: The name of the attribute associated with this id
1120 if len(self
.hash_oid_name
.keys()) == 0:
1121 self
._populate
_oid
_attid
()
1122 if self
.get_oid_from_attid(attid
) in self
.hash_oid_name
:
1123 return self
.hash_oid_name
[self
.get_oid_from_attid(attid
)]
1127 def _populate_oid_attid(self
):
1128 """Populate the hash hash_oid_name.
1130 This hash contains the oid of the attribute as a key and
1131 its display name as a value
1133 self
.hash_oid_name
= {}
1134 res
= self
.search(expression
="objectClass=attributeSchema",
1135 controls
=["search_options:1:2"],
1136 attrs
=["attributeID",
1140 strDisplay
= str(e
.get("lDAPDisplayName"))
1141 self
.hash_oid_name
[str(e
.get("attributeID"))] = strDisplay
1143 def get_attribute_replmetadata_version(self
, dn
, att
):
1144 """Get the version field trom the replPropertyMetaData for
1147 :param dn: The on which we want to get the version
1148 :param att: The name of the attribute
1149 :return: The value of the version field in the replPropertyMetaData
1150 for the given attribute. None if the attribute is not replicated
1153 res
= self
.search(expression
="distinguishedName=%s" % dn
,
1154 scope
=ldb
.SCOPE_SUBTREE
,
1155 controls
=["search_options:1:2"],
1156 attrs
=["replPropertyMetaData"])
1160 repl
= ndr_unpack(drsblobs
.replPropertyMetaDataBlob
,
1161 res
[0]["replPropertyMetaData"][0])
1163 if len(self
.hash_oid_name
.keys()) == 0:
1164 self
._populate
_oid
_attid
()
1166 # Search for Description
1167 att_oid
= self
.get_oid_from_attid(o
.attid
)
1168 if att_oid
in self
.hash_oid_name
and\
1169 att
.lower() == self
.hash_oid_name
[att_oid
].lower():
1173 def set_attribute_replmetadata_version(self
, dn
, att
, value
,
1174 addifnotexist
=False):
1175 res
= self
.search(expression
="distinguishedName=%s" % dn
,
1176 scope
=ldb
.SCOPE_SUBTREE
,
1177 controls
=["search_options:1:2"],
1178 attrs
=["replPropertyMetaData"])
1182 repl
= ndr_unpack(drsblobs
.replPropertyMetaDataBlob
,
1183 res
[0]["replPropertyMetaData"][0])
1185 now
= samba
.unix2nttime(int(time
.time()))
1187 if len(self
.hash_oid_name
.keys()) == 0:
1188 self
._populate
_oid
_attid
()
1190 # Search for Description
1191 att_oid
= self
.get_oid_from_attid(o
.attid
)
1192 if att_oid
in self
.hash_oid_name
and\
1193 att
.lower() == self
.hash_oid_name
[att_oid
].lower():
1195 seq
= self
.sequence_number(ldb
.SEQ_NEXT
)
1197 o
.originating_change_time
= now
1198 o
.originating_invocation_id
= misc
.GUID(self
.get_invocation_id())
1199 o
.originating_usn
= seq
1202 if not found
and addifnotexist
and len(ctr
.array
) > 0:
1203 o2
= drsblobs
.replPropertyMetaData1()
1205 att_oid
= self
.get_oid_from_attid(o2
.attid
)
1206 seq
= self
.sequence_number(ldb
.SEQ_NEXT
)
1208 o2
.originating_change_time
= now
1209 o2
.originating_invocation_id
= misc
.GUID(self
.get_invocation_id())
1210 o2
.originating_usn
= seq
1215 ctr
.count
= ctr
.count
+ 1
1219 replBlob
= ndr_pack(repl
)
1222 msg
["replPropertyMetaData"] = \
1223 ldb
.MessageElement(replBlob
,
1224 ldb
.FLAG_MOD_REPLACE
,
1225 "replPropertyMetaData")
1226 self
.modify(msg
, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
1228 def write_prefixes_from_schema(self
):
1229 dsdb
._dsdb
_write
_prefixes
_from
_schema
_to
_ldb
(self
)
1231 def get_partitions_dn(self
):
1232 return dsdb
._dsdb
_get
_partitions
_dn
(self
)
1234 def get_nc_root(self
, dn
):
1235 return dsdb
._dsdb
_get
_nc
_root
(self
, dn
)
1237 def get_wellknown_dn(self
, nc_root
, wkguid
):
1238 return dsdb
._dsdb
_get
_wellknown
_dn
(self
, nc_root
, wkguid
)
1240 def set_minPwdAge(self
, value
):
1241 if not isinstance(value
, bytes
):
1242 value
= str(value
).encode('utf8')
1244 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
1245 m
["minPwdAge"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "minPwdAge")
1248 def get_minPwdAge(self
):
1249 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["minPwdAge"])
1252 elif "minPwdAge" not in res
[0]:
1255 return int(res
[0]["minPwdAge"][0])
1257 def set_maxPwdAge(self
, value
):
1258 if not isinstance(value
, bytes
):
1259 value
= str(value
).encode('utf8')
1261 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
1262 m
["maxPwdAge"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "maxPwdAge")
1265 def get_maxPwdAge(self
):
1266 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["maxPwdAge"])
1269 elif "maxPwdAge" not in res
[0]:
1272 return int(res
[0]["maxPwdAge"][0])
1274 def set_minPwdLength(self
, value
):
1275 if not isinstance(value
, bytes
):
1276 value
= str(value
).encode('utf8')
1278 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
1279 m
["minPwdLength"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "minPwdLength")
1282 def get_minPwdLength(self
):
1283 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["minPwdLength"])
1286 elif "minPwdLength" not in res
[0]:
1289 return int(res
[0]["minPwdLength"][0])
1291 def set_pwdProperties(self
, value
):
1292 if not isinstance(value
, bytes
):
1293 value
= str(value
).encode('utf8')
1295 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
1296 m
["pwdProperties"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "pwdProperties")
1299 def get_pwdProperties(self
):
1300 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["pwdProperties"])
1303 elif "pwdProperties" not in res
[0]:
1306 return int(res
[0]["pwdProperties"][0])
1308 def set_dsheuristics(self
, dsheuristics
):
1310 m
.dn
= ldb
.Dn(self
, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
1311 % self
.get_config_basedn().get_linearized())
1312 if dsheuristics
is not None:
1313 m
["dSHeuristics"] = \
1314 ldb
.MessageElement(dsheuristics
,
1315 ldb
.FLAG_MOD_REPLACE
,
1318 m
["dSHeuristics"] = \
1319 ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
,
1323 def get_dsheuristics(self
):
1324 res
= self
.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
1325 % self
.get_config_basedn().get_linearized(),
1326 scope
=ldb
.SCOPE_BASE
, attrs
=["dSHeuristics"])
1329 elif "dSHeuristics" in res
[0]:
1330 dsheuristics
= res
[0]["dSHeuristics"][0]
1336 def create_ou(self
, ou_dn
, description
=None, name
=None, sd
=None):
1337 """Creates an organizationalUnit object
1338 :param ou_dn: dn of the new object
1339 :param description: description attribute
1340 :param name: name attribute
1341 :param sd: security descriptor of the object, can be
1342 an SDDL string or security.descriptor type
1345 "objectClass": "organizationalUnit"}
1348 m
["description"] = description
1353 m
["nTSecurityDescriptor"] = ndr_pack(sd
)
1356 def sequence_number(self
, seq_type
):
1357 """Returns the value of the sequence number according to the requested type
1358 :param seq_type: type of sequence number
1360 self
.transaction_start()
1362 seq
= super().sequence_number(seq_type
)
1364 self
.transaction_cancel()
1367 self
.transaction_commit()
1370 def get_dsServiceName(self
):
1371 """get the NTDS DN from the rootDSE"""
1372 res
= self
.search(base
="", scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
1373 return str(res
[0]["dsServiceName"][0])
1375 def get_serverName(self
):
1376 """get the server DN from the rootDSE"""
1377 res
= self
.search(base
="", scope
=ldb
.SCOPE_BASE
, attrs
=["serverName"])
1378 return str(res
[0]["serverName"][0])
1380 def dns_lookup(self
, dns_name
, dns_partition
=None):
1381 """Do a DNS lookup in the database, returns the NDR database structures"""
1382 if dns_partition
is None:
1383 return dsdb_dns
.lookup(self
, dns_name
)
1385 return dsdb_dns
.lookup(self
, dns_name
,
1386 dns_partition
=dns_partition
)
1388 def dns_extract(self
, el
):
1389 """Return the NDR database structures from a dnsRecord element"""
1390 return dsdb_dns
.extract(self
, el
)
1392 def dns_replace(self
, dns_name
, new_records
):
1393 """Do a DNS modification on the database, sets the NDR database
1394 structures on a DNS name
1396 return dsdb_dns
.replace(self
, dns_name
, new_records
)
1398 def dns_replace_by_dn(self
, dn
, new_records
):
1399 """Do a DNS modification on the database, sets the NDR database
1400 structures on a LDB DN
1402 This routine is important because if the last record on the DN
1403 is removed, this routine will put a tombstone in the record.
1405 return dsdb_dns
.replace_by_dn(self
, dn
, new_records
)
1407 def garbage_collect_tombstones(self
, dn
, current_time
,
1408 tombstone_lifetime
=None):
1409 """garbage_collect_tombstones(lp, samdb, [dn], current_time, tombstone_lifetime)
1410 -> (num_objects_expunged, num_links_expunged)"""
1412 if not is_ad_dc_built():
1413 raise SamDBError('Cannot garbage collect tombstones: '
1414 'AD DC was not built')
1416 if tombstone_lifetime
is None:
1417 return dsdb
._dsdb
_garbage
_collect
_tombstones
(self
, dn
,
1420 return dsdb
._dsdb
_garbage
_collect
_tombstones
(self
, dn
,
1424 def create_own_rid_set(self
):
1425 """create a RID set for this DSA"""
1426 return dsdb
._dsdb
_create
_own
_rid
_set
(self
)
1428 def allocate_rid(self
):
1429 """return a new RID from the RID Pool on this DSA"""
1430 return dsdb
._dsdb
_allocate
_rid
(self
)
1432 def next_free_rid(self
):
1433 """return the next free RID from the RID Pool on this DSA.
1435 :note: This function is not intended for general use, and care must be
1436 taken if it is used to generate objectSIDs. The returned RID is not
1437 formally reserved for use, creating the possibility of duplicate
1440 rid
, _
= self
.free_rid_bounds()
1443 def free_rid_bounds(self
):
1444 """return the low and high bounds (inclusive) of RIDs that are
1445 available for use in this DSA's current RID pool.
1447 :note: This function is not intended for general use, and care must be
1448 taken if it is used to generate objectSIDs. The returned range of
1449 RIDs is not formally reserved for use, creating the possibility of
1450 duplicate objectSIDs.
1452 # Get DN of this server's RID Set
1453 server_name_dn
= ldb
.Dn(self
, self
.get_serverName())
1454 res
= self
.search(base
=server_name_dn
,
1455 scope
=ldb
.SCOPE_BASE
,
1456 attrs
=["serverReference"])
1458 server_ref
= res
[0]["serverReference"]
1461 ldb
.ERR_NO_SUCH_ATTRIBUTE
,
1463 "Cannot find attribute serverReference of %s "
1464 "to calculate reference dn" % server_name_dn
) from None
1465 server_ref_dn
= ldb
.Dn(self
, server_ref
[0].decode("utf-8"))
1467 res
= self
.search(base
=server_ref_dn
,
1468 scope
=ldb
.SCOPE_BASE
,
1469 attrs
=["rIDSetReferences"])
1471 rid_set_refs
= res
[0]["rIDSetReferences"]
1474 ldb
.ERR_NO_SUCH_ATTRIBUTE
,
1476 "Cannot find attribute rIDSetReferences of %s "
1477 "to calculate reference dn" % server_ref_dn
) from None
1478 rid_set_dn
= ldb
.Dn(self
, rid_set_refs
[0].decode("utf-8"))
1480 # Get the alloc pools and next RID of this RID Set
1481 res
= self
.search(base
=rid_set_dn
,
1482 scope
=ldb
.SCOPE_BASE
,
1483 attrs
=["rIDAllocationPool",
1484 "rIDPreviousAllocationPool",
1487 uint32_max
= 2**32 - 1
1488 uint64_max
= 2**64 - 1
1491 alloc_pool
= int(res
[0]["rIDAllocationPool"][0])
1493 alloc_pool
= uint64_max
1494 if alloc_pool
== uint64_max
:
1495 raise ldb
.LdbError(ldb
.ERR_OPERATIONS_ERROR
,
1496 "Bad RID Set %s" % rid_set_dn
)
1499 prev_pool
= int(res
[0]["rIDPreviousAllocationPool"][0])
1501 prev_pool
= uint64_max
1503 next_rid
= int(res
[0]["rIDNextRID"][0])
1505 next_rid
= uint32_max
1507 # If we never used a pool, set up our first pool
1508 if prev_pool
== uint64_max
or next_rid
== uint32_max
:
1509 prev_pool
= alloc_pool
1510 next_rid
= prev_pool
& uint32_max
1514 # Now check if our current pool is still usable
1515 prev_pool_lo
= prev_pool
& uint32_max
1516 prev_pool_hi
= prev_pool
>> 32
1517 if next_rid
> prev_pool_hi
:
1518 # We need a new pool, check if we already have a new one
1519 # Otherwise we return an error code.
1520 if alloc_pool
== prev_pool
:
1521 raise ldb
.LdbError(ldb
.ERR_OPERATIONS_ERROR
,
1522 "RID pools out of RIDs")
1524 # Now use the new pool
1525 prev_pool
= alloc_pool
1526 prev_pool_lo
= prev_pool
& uint32_max
1527 prev_pool_hi
= prev_pool
>> 32
1528 next_rid
= prev_pool_lo
1530 if next_rid
< prev_pool_lo
or next_rid
> prev_pool_hi
:
1531 raise ldb
.LdbError(ldb
.ERR_OPERATIONS_ERROR
,
1532 "Bad RID chosen %d from range %d-%d" %
1533 (next_rid
, prev_pool_lo
, prev_pool_hi
))
1535 return next_rid
, prev_pool_hi
1537 def normalize_dn_in_domain(self
, dn
):
1538 """return a new DN expanded by adding the domain DN
1540 If the dn is already a child of the domain DN, just
1543 :param dn: relative dn
1545 domain_dn
= ldb
.Dn(self
, self
.domain_dn())
1547 if isinstance(dn
, ldb
.Dn
):
1550 full_dn
= ldb
.Dn(self
, dn
)
1551 if not full_dn
.is_child_of(domain_dn
):
1552 full_dn
.add_base(domain_dn
)
1555 def new_gkdi_root_key(self
, *args
, **kwargs
):
1557 dn
= dsdb
._dsdb
_create
_gkdi
_root
_key
(self
, *args
, **kwargs
)
1561 class dsdb_Dn(object):
1562 """a class for binary DN"""
1564 def __init__(self
, samdb
, dnstring
, syntax_oid
=None):
1565 """create a dsdb_Dn"""
1566 if syntax_oid
is None:
1567 # auto-detect based on string
1568 if dnstring
.startswith("B:"):
1569 syntax_oid
= dsdb
.DSDB_SYNTAX_BINARY_DN
1570 elif dnstring
.startswith("S:"):
1571 syntax_oid
= dsdb
.DSDB_SYNTAX_STRING_DN
1573 syntax_oid
= dsdb
.DSDB_SYNTAX_OR_NAME
1574 if syntax_oid
in [dsdb
.DSDB_SYNTAX_BINARY_DN
, dsdb
.DSDB_SYNTAX_STRING_DN
]:
1576 colons
= dnstring
.split(':')
1578 raise RuntimeError("Invalid DN %s" % dnstring
)
1579 prefix_len
= 4 + len(colons
[1]) + int(colons
[1])
1580 self
.prefix
= dnstring
[0:prefix_len
]
1581 self
.binary
= self
.prefix
[3 + len(colons
[1]):-1]
1582 self
.dnstring
= dnstring
[prefix_len
:]
1584 self
.dnstring
= dnstring
1587 self
.dn
= ldb
.Dn(samdb
, self
.dnstring
)
1590 return self
.prefix
+ str(self
.dn
.extended_str(mode
=1))
1592 def __cmp__(self
, other
):
1593 """ compare dsdb_Dn values similar to parsed_dn_compare()"""
1596 guid1
= dn1
.dn
.get_extended_component("GUID")
1597 guid2
= dn2
.dn
.get_extended_component("GUID")
1599 v
= cmp(guid1
, guid2
)
1602 v
= cmp(dn1
.binary
, dn2
.binary
)
1605 # In Python3, __cmp__ is replaced by these 6 methods
1606 def __eq__(self
, other
):
1607 return self
.__cmp
__(other
) == 0
1609 def __ne__(self
, other
):
1610 return self
.__cmp
__(other
) != 0
1612 def __lt__(self
, other
):
1613 return self
.__cmp
__(other
) < 0
1615 def __le__(self
, other
):
1616 return self
.__cmp
__(other
) <= 0
1618 def __gt__(self
, other
):
1619 return self
.__cmp
__(other
) > 0
1621 def __ge__(self
, other
):
1622 return self
.__cmp
__(other
) >= 0
1624 def get_binary_integer(self
):
1625 """return binary part of a dsdb_Dn as an integer, or None"""
1626 if self
.prefix
== '':
1628 return int(self
.binary
, 16)
1630 def get_bytes(self
):
1631 """return binary as a byte string"""
1632 return binascii
.unhexlify(self
.binary
)