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