pyldb: avoid segfault when adding an element with no name
[nivanova/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.compat import text_type
36 from samba.compat import binary_type
37 from samba.compat import get_bytes
38 from samba.dcerpc import security
39
40 __docformat__ = "restructuredText"
41
42
43 def get_default_backend_store():
44     return "tdb"
45
46
47 class SamDB(samba.Ldb):
48     """The SAM database."""
49
50     hash_oid_name = {}
51     hash_well_known = {}
52
53     def __init__(self, url=None, lp=None, modules_dir=None, session_info=None,
54                  credentials=None, flags=ldb.FLG_DONT_CREATE_DB,
55                  options=None, global_schema=True,
56                  auto_connect=True, am_rodc=None):
57         self.lp = lp
58         if not auto_connect:
59             url = None
60         elif url is None and lp is not None:
61             url = lp.samdb_url()
62
63         self.url = url
64
65         super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir,
66                                     session_info=session_info, credentials=credentials, flags=flags,
67                                     options=options)
68
69         if global_schema:
70             dsdb._dsdb_set_global_schema(self)
71
72         if am_rodc is not None:
73             dsdb._dsdb_set_am_rodc(self, am_rodc)
74
75     def connect(self, url=None, flags=0, options=None):
76         '''connect to the database'''
77         if self.lp is not None and not os.path.exists(url):
78             url = self.lp.private_path(url)
79         self.url = url
80
81         super(SamDB, self).connect(url=url, flags=flags,
82                                    options=options)
83
84     def am_rodc(self):
85         '''return True if we are an RODC'''
86         return dsdb._am_rodc(self)
87
88     def am_pdc(self):
89         '''return True if we are an PDC emulator'''
90         return dsdb._am_pdc(self)
91
92     def domain_dn(self):
93         '''return the domain DN'''
94         return str(self.get_default_basedn())
95
96     def schema_dn(self):
97         '''return the schema partition dn'''
98         return str(self.get_schema_basedn())
99
100     def disable_account(self, search_filter):
101         """Disables an account
102
103         :param search_filter: LDAP filter to find the user (eg
104             samccountname=name)
105         """
106
107         flags = samba.dsdb.UF_ACCOUNTDISABLE
108         self.toggle_userAccountFlags(search_filter, flags, on=True)
109
110     def enable_account(self, search_filter):
111         """Enables an account
112
113         :param search_filter: LDAP filter to find the user (eg
114             samccountname=name)
115         """
116
117         flags = samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_PASSWD_NOTREQD
118         self.toggle_userAccountFlags(search_filter, flags, on=False)
119
120     def toggle_userAccountFlags(self, search_filter, flags, flags_str=None,
121                                 on=True, strict=False):
122         """Toggle_userAccountFlags
123
124         :param search_filter: LDAP filter to find the user (eg
125             samccountname=name)
126         :param flags: samba.dsdb.UF_* flags
127         :param on: on=True (default) => set, on=False => unset
128         :param strict: strict=False (default) ignore if no action is needed
129                  strict=True raises an Exception if...
130         """
131         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
132                           expression=search_filter, attrs=["userAccountControl"])
133         if len(res) == 0:
134                 raise Exception("Unable to find account where '%s'" % search_filter)
135         assert(len(res) == 1)
136         account_dn = res[0].dn
137
138         old_uac = int(res[0]["userAccountControl"][0])
139         if on:
140             if strict and (old_uac & flags):
141                 error = "Account flag(s) '%s' already set" % flags_str
142                 raise Exception(error)
143
144             new_uac = old_uac | flags
145         else:
146             if strict and not (old_uac & flags):
147                 error = "Account flag(s) '%s' already unset" % flags_str
148                 raise Exception(error)
149
150             new_uac = old_uac & ~flags
151
152         if old_uac == new_uac:
153             return
154
155         mod = """
156 dn: %s
157 changetype: modify
158 delete: userAccountControl
159 userAccountControl: %u
160 add: userAccountControl
161 userAccountControl: %u
162 """ % (account_dn, old_uac, new_uac)
163         self.modify_ldif(mod)
164
165     def force_password_change_at_next_login(self, search_filter):
166         """Forces a password change at next login
167
168         :param search_filter: LDAP filter to find the user (eg
169             samccountname=name)
170         """
171         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
172                           expression=search_filter, attrs=[])
173         if len(res) == 0:
174                 raise Exception('Unable to find user "%s"' % search_filter)
175         assert(len(res) == 1)
176         user_dn = res[0].dn
177
178         mod = """
179 dn: %s
180 changetype: modify
181 replace: pwdLastSet
182 pwdLastSet: 0
183 """ % (user_dn)
184         self.modify_ldif(mod)
185
186     def newgroup(self, groupname, groupou=None, grouptype=None,
187                  description=None, mailaddress=None, notes=None, sd=None,
188                  gidnumber=None, nisdomain=None):
189         """Adds a new group with additional parameters
190
191         :param groupname: Name of the new group
192         :param grouptype: Type of the new group
193         :param description: Description of the new group
194         :param mailaddress: Email address of the new group
195         :param notes: Notes of the new group
196         :param gidnumber: GID Number of the new group
197         :param nisdomain: NIS Domain Name of the new group
198         :param sd: security descriptor of the object
199         """
200
201         group_dn = "CN=%s,%s,%s" % (groupname, (groupou or "CN=Users"), self.domain_dn())
202
203         # The new user record. Note the reliance on the SAMLDB module which
204         # fills in the default information
205         ldbmessage = {"dn": group_dn,
206                       "sAMAccountName": groupname,
207                       "objectClass": "group"}
208
209         if grouptype is not None:
210             ldbmessage["groupType"] = normalise_int32(grouptype)
211
212         if description is not None:
213             ldbmessage["description"] = description
214
215         if mailaddress is not None:
216             ldbmessage["mail"] = mailaddress
217
218         if notes is not None:
219             ldbmessage["info"] = notes
220
221         if gidnumber is not None:
222             ldbmessage["gidNumber"] = normalise_int32(gidnumber)
223
224         if nisdomain is not None:
225             ldbmessage["msSFU30Name"] = groupname
226             ldbmessage["msSFU30NisDomain"] = nisdomain
227
228         if sd is not None:
229             ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
230
231         self.add(ldbmessage)
232
233     def deletegroup(self, groupname):
234         """Deletes a group
235
236         :param groupname: Name of the target group
237         """
238
239         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
240         self.transaction_start()
241         try:
242             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
243                                       expression=groupfilter, attrs=[])
244             if len(targetgroup) == 0:
245                 raise Exception('Unable to find group "%s"' % groupname)
246             assert(len(targetgroup) == 1)
247             self.delete(targetgroup[0].dn)
248         except:
249             self.transaction_cancel()
250             raise
251         else:
252             self.transaction_commit()
253
254     def add_remove_group_members(self, groupname, members,
255                                  add_members_operation=True):
256         """Adds or removes group members
257
258         :param groupname: Name of the target group
259         :param members: list of group members
260         :param add_members_operation: Defines if its an add or remove
261             operation
262         """
263
264         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (
265             ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
266
267         self.transaction_start()
268         try:
269             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
270                                       expression=groupfilter, attrs=['member'])
271             if len(targetgroup) == 0:
272                 raise Exception('Unable to find group "%s"' % groupname)
273             assert(len(targetgroup) == 1)
274
275             modified = False
276
277             addtargettogroup = """
278 dn: %s
279 changetype: modify
280 """ % (str(targetgroup[0].dn))
281
282             for member in members:
283                 filter = ('(&(sAMAccountName=%s)(|(objectclass=user)'
284                           '(objectclass=group)))' % ldb.binary_encode(member))
285                 foreign_msg = None
286                 try:
287                     membersid = security.dom_sid(member)
288                 except TypeError as e:
289                     membersid = None
290
291                 if membersid is not None:
292                     filter = '(objectSid=%s)' % str(membersid)
293                     dn_str = "<SID=%s>" % str(membersid)
294                     foreign_msg = ldb.Message()
295                     foreign_msg.dn = ldb.Dn(self, dn_str)
296
297                 targetmember = self.search(base=self.domain_dn(),
298                                            scope=ldb.SCOPE_SUBTREE,
299                                            expression="%s" % filter,
300                                            attrs=[])
301
302                 if len(targetmember) == 0 and foreign_msg is not None:
303                     targetmember = [foreign_msg]
304                 if len(targetmember) != 1:
305                     raise Exception('Unable to find "%s". Operation cancelled.' % member)
306                 targetmember_dn = targetmember[0].dn.extended_str(1)
307                 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']]):
308                     modified = True
309                     addtargettogroup += """add: member
310 member: %s
311 """ % (str(targetmember_dn))
312
313                 elif add_members_operation is False and (targetgroup[0].get('member') is not None and get_bytes(targetmember_dn) in targetgroup[0]['member']):
314                     modified = True
315                     addtargettogroup += """delete: member
316 member: %s
317 """ % (str(targetmember_dn))
318
319             if modified is True:
320                 self.modify_ldif(addtargettogroup)
321
322         except:
323             self.transaction_cancel()
324             raise
325         else:
326             self.transaction_commit()
327
328     def newuser(self, username, password,
329                 force_password_change_at_next_login_req=False,
330                 useusernameascn=False, userou=None, surname=None, givenname=None,
331                 initials=None, profilepath=None, scriptpath=None, homedrive=None,
332                 homedirectory=None, jobtitle=None, department=None, company=None,
333                 description=None, mailaddress=None, internetaddress=None,
334                 telephonenumber=None, physicaldeliveryoffice=None, sd=None,
335                 setpassword=True, uidnumber=None, gidnumber=None, gecos=None,
336                 loginshell=None, uid=None, nisdomain=None, unixhome=None,
337                 smartcard_required=False):
338         """Adds a new user with additional parameters
339
340         :param username: Name of the new user
341         :param password: Password for the new user
342         :param force_password_change_at_next_login_req: Force password change
343         :param useusernameascn: Use username as cn rather that firstname +
344             initials + lastname
345         :param userou: Object container (without domainDN postfix) for new user
346         :param surname: Surname of the new user
347         :param givenname: First name of the new user
348         :param initials: Initials of the new user
349         :param profilepath: Profile path of the new user
350         :param scriptpath: Logon script path of the new user
351         :param homedrive: Home drive of the new user
352         :param homedirectory: Home directory of the new user
353         :param jobtitle: Job title of the new user
354         :param department: Department of the new user
355         :param company: Company of the new user
356         :param description: of the new user
357         :param mailaddress: Email address of the new user
358         :param internetaddress: Home page of the new user
359         :param telephonenumber: Phone number of the new user
360         :param physicaldeliveryoffice: Office location of the new user
361         :param sd: security descriptor of the object
362         :param setpassword: optionally disable password reset
363         :param uidnumber: RFC2307 Unix numeric UID of the new user
364         :param gidnumber: RFC2307 Unix primary GID of the new user
365         :param gecos: RFC2307 Unix GECOS field of the new user
366         :param loginshell: RFC2307 Unix login shell of the new user
367         :param uid: RFC2307 Unix username of the new user
368         :param nisdomain: RFC2307 Unix NIS domain of the new user
369         :param unixhome: RFC2307 Unix home directory of the new user
370         :param smartcard_required: set the UF_SMARTCARD_REQUIRED bit of the new user
371         """
372
373         displayname = ""
374         if givenname is not None:
375             displayname += givenname
376
377         if initials is not None:
378             displayname += ' %s.' % initials
379
380         if surname is not None:
381             displayname += ' %s' % surname
382
383         cn = username
384         if useusernameascn is None and displayname != "":
385             cn = displayname
386
387         user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn())
388
389         dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "")
390         user_principal_name = "%s@%s" % (username, dnsdomain)
391         # The new user record. Note the reliance on the SAMLDB module which
392         # fills in the default information
393         ldbmessage = {"dn": user_dn,
394                       "sAMAccountName": username,
395                       "userPrincipalName": user_principal_name,
396                       "objectClass": "user"}
397
398         if smartcard_required:
399             ldbmessage["userAccountControl"] = str(dsdb.UF_NORMAL_ACCOUNT |
400                                                    dsdb.UF_SMARTCARD_REQUIRED)
401             setpassword = False
402
403         if surname is not None:
404             ldbmessage["sn"] = surname
405
406         if givenname is not None:
407             ldbmessage["givenName"] = givenname
408
409         if displayname != "":
410             ldbmessage["displayName"] = displayname
411             ldbmessage["name"] = displayname
412
413         if initials is not None:
414             ldbmessage["initials"] = '%s.' % initials
415
416         if profilepath is not None:
417             ldbmessage["profilePath"] = profilepath
418
419         if scriptpath is not None:
420             ldbmessage["scriptPath"] = scriptpath
421
422         if homedrive is not None:
423             ldbmessage["homeDrive"] = homedrive
424
425         if homedirectory is not None:
426             ldbmessage["homeDirectory"] = homedirectory
427
428         if jobtitle is not None:
429             ldbmessage["title"] = jobtitle
430
431         if department is not None:
432             ldbmessage["department"] = department
433
434         if company is not None:
435             ldbmessage["company"] = company
436
437         if description is not None:
438             ldbmessage["description"] = description
439
440         if mailaddress is not None:
441             ldbmessage["mail"] = mailaddress
442
443         if internetaddress is not None:
444             ldbmessage["wWWHomePage"] = internetaddress
445
446         if telephonenumber is not None:
447             ldbmessage["telephoneNumber"] = telephonenumber
448
449         if physicaldeliveryoffice is not None:
450             ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
451
452         if sd is not None:
453             ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
454
455         ldbmessage2 = None
456         if any(map(lambda b: b is not None, (uid, uidnumber, gidnumber, gecos,
457                                              loginshell, nisdomain, unixhome))):
458             ldbmessage2 = ldb.Message()
459             ldbmessage2.dn = ldb.Dn(self, user_dn)
460             if uid is not None:
461                 ldbmessage2["uid"] = ldb.MessageElement(str(uid), ldb.FLAG_MOD_REPLACE, 'uid')
462             if uidnumber is not None:
463                 ldbmessage2["uidNumber"] = ldb.MessageElement(str(uidnumber), ldb.FLAG_MOD_REPLACE, 'uidNumber')
464             if gidnumber is not None:
465                 ldbmessage2["gidNumber"] = ldb.MessageElement(str(gidnumber), ldb.FLAG_MOD_REPLACE, 'gidNumber')
466             if gecos is not None:
467                 ldbmessage2["gecos"] = ldb.MessageElement(str(gecos), ldb.FLAG_MOD_REPLACE, 'gecos')
468             if loginshell is not None:
469                 ldbmessage2["loginShell"] = ldb.MessageElement(str(loginshell), ldb.FLAG_MOD_REPLACE, 'loginShell')
470             if unixhome is not None:
471                 ldbmessage2["unixHomeDirectory"] = ldb.MessageElement(
472                     str(unixhome), ldb.FLAG_MOD_REPLACE, 'unixHomeDirectory')
473             if nisdomain is not None:
474                 ldbmessage2["msSFU30NisDomain"] = ldb.MessageElement(
475                     str(nisdomain), ldb.FLAG_MOD_REPLACE, 'msSFU30NisDomain')
476                 ldbmessage2["msSFU30Name"] = ldb.MessageElement(
477                     str(username), ldb.FLAG_MOD_REPLACE, 'msSFU30Name')
478                 ldbmessage2["unixUserPassword"] = ldb.MessageElement(
479                     'ABCD!efgh12345$67890', ldb.FLAG_MOD_REPLACE,
480                     'unixUserPassword')
481
482         self.transaction_start()
483         try:
484             self.add(ldbmessage)
485             if ldbmessage2:
486                 self.modify(ldbmessage2)
487
488             # Sets the password for it
489             if setpassword:
490                 self.setpassword(("(distinguishedName=%s)" %
491                                   ldb.binary_encode(user_dn)),
492                                  password,
493                                  force_password_change_at_next_login_req)
494         except:
495             self.transaction_cancel()
496             raise
497         else:
498             self.transaction_commit()
499
500     def newcomputer(self, computername, computerou=None, description=None,
501                     prepare_oldjoin=False, ip_address_list=None,
502                     service_principal_name_list=None):
503         """Adds a new user with additional parameters
504
505         :param computername: Name of the new computer
506         :param computerou: Object container for new computer
507         :param description: Description of the new computer
508         :param prepare_oldjoin: Preset computer password for oldjoin mechanism
509         :param ip_address_list: ip address list for DNS A or AAAA record
510         :param service_principal_name_list: string list of servicePincipalName
511         """
512
513         cn = re.sub(r"\$$", "", computername)
514         if cn.count('$'):
515             raise Exception('Illegal computername "%s"' % computername)
516         samaccountname = "%s$" % cn
517
518         computercontainer_dn = "CN=Computers,%s" % self.domain_dn()
519         if computerou:
520             computercontainer_dn = self.normalize_dn_in_domain(computerou)
521
522         computer_dn = "CN=%s,%s" % (cn, computercontainer_dn)
523
524         ldbmessage = {"dn": computer_dn,
525                       "sAMAccountName": samaccountname,
526                       "objectClass": "computer",
527                       }
528
529         if description is not None:
530             ldbmessage["description"] = description
531
532         if service_principal_name_list:
533             ldbmessage["servicePrincipalName"] = service_principal_name_list
534
535         accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
536                              dsdb.UF_ACCOUNTDISABLE)
537         if prepare_oldjoin:
538             accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
539         ldbmessage["userAccountControl"] = accountcontrol
540
541         if ip_address_list:
542             ldbmessage['dNSHostName'] = '{}.{}'.format(
543                 cn, self.domain_dns_name())
544
545         self.transaction_start()
546         try:
547             self.add(ldbmessage)
548
549             if prepare_oldjoin:
550                 password = cn.lower()
551                 self.setpassword(("(distinguishedName=%s)" %
552                                   ldb.binary_encode(computer_dn)),
553                                  password, False)
554         except:
555             self.transaction_cancel()
556             raise
557         else:
558             self.transaction_commit()
559
560     def deleteuser(self, username):
561         """Deletes a user
562
563         :param username: Name of the target user
564         """
565
566         filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn())
567         self.transaction_start()
568         try:
569             target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
570                                  expression=filter, attrs=[])
571             if len(target) == 0:
572                 raise Exception('Unable to find user "%s"' % username)
573             assert(len(target) == 1)
574             self.delete(target[0].dn)
575         except:
576             self.transaction_cancel()
577             raise
578         else:
579             self.transaction_commit()
580
581     def setpassword(self, search_filter, password,
582                     force_change_at_next_login=False, username=None):
583         """Sets the password for a user
584
585         :param search_filter: LDAP filter to find the user (eg
586             samccountname=name)
587         :param password: Password for the user
588         :param force_change_at_next_login: Force password change
589         """
590         self.transaction_start()
591         try:
592             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
593                               expression=search_filter, attrs=[])
594             if len(res) == 0:
595                 raise Exception('Unable to find user "%s"' % (username or search_filter))
596             if len(res) > 1:
597                 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter))
598             user_dn = res[0].dn
599             if not isinstance(password, text_type):
600                 pw = password.decode('utf-8')
601             else:
602                 pw = password
603             pw = ('"' + pw + '"').encode('utf-16-le')
604             setpw = """
605 dn: %s
606 changetype: modify
607 replace: unicodePwd
608 unicodePwd:: %s
609 """ % (user_dn, base64.b64encode(pw).decode('utf-8'))
610
611             self.modify_ldif(setpw)
612
613             if force_change_at_next_login:
614                 self.force_password_change_at_next_login(
615                     "(distinguishedName=" + str(user_dn) + ")")
616
617             #  modify the userAccountControl to remove the disabled bit
618             self.enable_account(search_filter)
619         except:
620             self.transaction_cancel()
621             raise
622         else:
623             self.transaction_commit()
624
625     def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False):
626         """Sets the account expiry for a user
627
628         :param search_filter: LDAP filter to find the user (eg
629             samaccountname=name)
630         :param expiry_seconds: expiry time from now in seconds
631         :param no_expiry_req: if set, then don't expire password
632         """
633         self.transaction_start()
634         try:
635             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
636                               expression=search_filter,
637                               attrs=["userAccountControl", "accountExpires"])
638             if len(res) == 0:
639                 raise Exception('Unable to find user "%s"' % search_filter)
640             assert(len(res) == 1)
641             user_dn = res[0].dn
642
643             userAccountControl = int(res[0]["userAccountControl"][0])
644             accountExpires     = int(res[0]["accountExpires"][0])
645             if no_expiry_req:
646                 userAccountControl = userAccountControl | 0x10000
647                 accountExpires = 0
648             else:
649                 userAccountControl = userAccountControl & ~0x10000
650                 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
651
652             setexp = """
653 dn: %s
654 changetype: modify
655 replace: userAccountControl
656 userAccountControl: %u
657 replace: accountExpires
658 accountExpires: %u
659 """ % (user_dn, userAccountControl, accountExpires)
660
661             self.modify_ldif(setexp)
662         except:
663             self.transaction_cancel()
664             raise
665         else:
666             self.transaction_commit()
667
668     def set_domain_sid(self, sid):
669         """Change the domain SID used by this LDB.
670
671         :param sid: The new domain sid to use.
672         """
673         dsdb._samdb_set_domain_sid(self, sid)
674
675     def get_domain_sid(self):
676         """Read the domain SID used by this LDB. """
677         return dsdb._samdb_get_domain_sid(self)
678
679     domain_sid = property(get_domain_sid, set_domain_sid,
680                           doc="SID for the domain")
681
682     def set_invocation_id(self, invocation_id):
683         """Set the invocation id for this SamDB handle.
684
685         :param invocation_id: GUID of the invocation id.
686         """
687         dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
688
689     def get_invocation_id(self):
690         """Get the invocation_id id"""
691         return dsdb._samdb_ntds_invocation_id(self)
692
693     invocation_id = property(get_invocation_id, set_invocation_id,
694                              doc="Invocation ID GUID")
695
696     def get_oid_from_attid(self, attid):
697         return dsdb._dsdb_get_oid_from_attid(self, attid)
698
699     def get_attid_from_lDAPDisplayName(self, ldap_display_name,
700                                        is_schema_nc=False):
701         '''return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI'''
702         return dsdb._dsdb_get_attid_from_lDAPDisplayName(self,
703                                                          ldap_display_name, is_schema_nc)
704
705     def get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name):
706         '''return the syntax OID for a LDAP attribute as a string'''
707         return dsdb._dsdb_get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name)
708
709     def get_systemFlags_from_lDAPDisplayName(self, ldap_display_name):
710         '''return the systemFlags for a LDAP attribute as a integer'''
711         return dsdb._dsdb_get_systemFlags_from_lDAPDisplayName(self, ldap_display_name)
712
713     def get_linkId_from_lDAPDisplayName(self, ldap_display_name):
714         '''return the linkID for a LDAP attribute as a integer'''
715         return dsdb._dsdb_get_linkId_from_lDAPDisplayName(self, ldap_display_name)
716
717     def get_lDAPDisplayName_by_attid(self, attid):
718         '''return the lDAPDisplayName from an integer DRS attribute ID'''
719         return dsdb._dsdb_get_lDAPDisplayName_by_attid(self, attid)
720
721     def get_backlink_from_lDAPDisplayName(self, ldap_display_name):
722         '''return the attribute name of the corresponding backlink from the name
723         of a forward link attribute. If there is no backlink return None'''
724         return dsdb._dsdb_get_backlink_from_lDAPDisplayName(self, ldap_display_name)
725
726     def set_ntds_settings_dn(self, ntds_settings_dn):
727         """Set the NTDS Settings DN, as would be returned on the dsServiceName
728         rootDSE attribute.
729
730         This allows the DN to be set before the database fully exists
731
732         :param ntds_settings_dn: The new DN to use
733         """
734         dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
735
736     def get_ntds_GUID(self):
737         """Get the NTDS objectGUID"""
738         return dsdb._samdb_ntds_objectGUID(self)
739
740     def server_site_name(self):
741         """Get the server site name"""
742         return dsdb._samdb_server_site_name(self)
743
744     def host_dns_name(self):
745         """return the DNS name of this host"""
746         res = self.search(base='', scope=ldb.SCOPE_BASE, attrs=['dNSHostName'])
747         return str(res[0]['dNSHostName'][0])
748
749     def domain_dns_name(self):
750         """return the DNS name of the domain root"""
751         domain_dn = self.get_default_basedn()
752         return domain_dn.canonical_str().split('/')[0]
753
754     def forest_dns_name(self):
755         """return the DNS name of the forest root"""
756         forest_dn = self.get_root_basedn()
757         return forest_dn.canonical_str().split('/')[0]
758
759     def load_partition_usn(self, base_dn):
760         return dsdb._dsdb_load_partition_usn(self, base_dn)
761
762     def set_schema(self, schema, write_indices_and_attributes=True):
763         self.set_schema_from_ldb(schema.ldb, write_indices_and_attributes=write_indices_and_attributes)
764
765     def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True):
766         dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes)
767
768     def set_schema_update_now(self):
769         ldif = """
770 dn:
771 changetype: modify
772 add: schemaUpdateNow
773 schemaUpdateNow: 1
774 """
775         self.modify_ldif(ldif)
776
777     def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
778         '''convert a list of attribute values to a DRSUAPI DsReplicaAttribute'''
779         return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
780
781     def dsdb_normalise_attributes(self, ldb, ldap_display_name, ldif_elements):
782         '''normalise a list of attribute values'''
783         return dsdb._dsdb_normalise_attributes(ldb, ldap_display_name, ldif_elements)
784
785     def get_attribute_from_attid(self, attid):
786         """ Get from an attid the associated attribute
787
788         :param attid: The attribute id for searched attribute
789         :return: The name of the attribute associated with this id
790         """
791         if len(self.hash_oid_name.keys()) == 0:
792             self._populate_oid_attid()
793         if self.get_oid_from_attid(attid) in self.hash_oid_name:
794             return self.hash_oid_name[self.get_oid_from_attid(attid)]
795         else:
796             return None
797
798     def _populate_oid_attid(self):
799         """Populate the hash hash_oid_name.
800
801         This hash contains the oid of the attribute as a key and
802         its display name as a value
803         """
804         self.hash_oid_name = {}
805         res = self.search(expression="objectClass=attributeSchema",
806                           controls=["search_options:1:2"],
807                           attrs=["attributeID",
808                                  "lDAPDisplayName"])
809         if len(res) > 0:
810             for e in res:
811                 strDisplay = str(e.get("lDAPDisplayName"))
812                 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
813
814     def get_attribute_replmetadata_version(self, dn, att):
815         """Get the version field trom the replPropertyMetaData for
816         the given field
817
818         :param dn: The on which we want to get the version
819         :param att: The name of the attribute
820         :return: The value of the version field in the replPropertyMetaData
821             for the given attribute. None if the attribute is not replicated
822         """
823
824         res = self.search(expression="distinguishedName=%s" % dn,
825                           scope=ldb.SCOPE_SUBTREE,
826                           controls=["search_options:1:2"],
827                           attrs=["replPropertyMetaData"])
828         if len(res) == 0:
829             return None
830
831         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
832                           res[0]["replPropertyMetaData"][0])
833         ctr = repl.ctr
834         if len(self.hash_oid_name.keys()) == 0:
835             self._populate_oid_attid()
836         for o in ctr.array:
837             # Search for Description
838             att_oid = self.get_oid_from_attid(o.attid)
839             if att_oid in self.hash_oid_name and\
840                att.lower() == self.hash_oid_name[att_oid].lower():
841                 return o.version
842         return None
843
844     def set_attribute_replmetadata_version(self, dn, att, value,
845                                            addifnotexist=False):
846         res = self.search(expression="distinguishedName=%s" % dn,
847                           scope=ldb.SCOPE_SUBTREE,
848                           controls=["search_options:1:2"],
849                           attrs=["replPropertyMetaData"])
850         if len(res) == 0:
851             return None
852
853         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
854                           res[0]["replPropertyMetaData"][0])
855         ctr = repl.ctr
856         now = samba.unix2nttime(int(time.time()))
857         found = False
858         if len(self.hash_oid_name.keys()) == 0:
859             self._populate_oid_attid()
860         for o in ctr.array:
861             # Search for Description
862             att_oid = self.get_oid_from_attid(o.attid)
863             if att_oid in self.hash_oid_name and\
864                att.lower() == self.hash_oid_name[att_oid].lower():
865                 found = True
866                 seq = self.sequence_number(ldb.SEQ_NEXT)
867                 o.version = value
868                 o.originating_change_time = now
869                 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
870                 o.originating_usn = seq
871                 o.local_usn = seq
872
873         if not found and addifnotexist and len(ctr.array) > 0:
874             o2 = drsblobs.replPropertyMetaData1()
875             o2.attid = 589914
876             att_oid = self.get_oid_from_attid(o2.attid)
877             seq = self.sequence_number(ldb.SEQ_NEXT)
878             o2.version = value
879             o2.originating_change_time = now
880             o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
881             o2.originating_usn = seq
882             o2.local_usn = seq
883             found = True
884             tab = ctr.array
885             tab.append(o2)
886             ctr.count = ctr.count + 1
887             ctr.array = tab
888
889         if found:
890             replBlob = ndr_pack(repl)
891             msg = ldb.Message()
892             msg.dn = res[0].dn
893             msg["replPropertyMetaData"] = \
894                 ldb.MessageElement(replBlob,
895                                    ldb.FLAG_MOD_REPLACE,
896                                    "replPropertyMetaData")
897             self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
898
899     def write_prefixes_from_schema(self):
900         dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
901
902     def get_partitions_dn(self):
903         return dsdb._dsdb_get_partitions_dn(self)
904
905     def get_nc_root(self, dn):
906         return dsdb._dsdb_get_nc_root(self, dn)
907
908     def get_wellknown_dn(self, nc_root, wkguid):
909         h_nc = self.hash_well_known.get(str(nc_root))
910         dn = None
911         if h_nc is not None:
912             dn = h_nc.get(wkguid)
913         if dn is None:
914             dn = dsdb._dsdb_get_wellknown_dn(self, nc_root, wkguid)
915             if dn is None:
916                 return dn
917             if h_nc is None:
918                 self.hash_well_known[str(nc_root)] = {}
919                 h_nc = self.hash_well_known[str(nc_root)]
920             h_nc[wkguid] = dn
921         return dn
922
923     def set_minPwdAge(self, value):
924         if not isinstance(value, binary_type):
925             value = str(value).encode('utf8')
926         m = ldb.Message()
927         m.dn = ldb.Dn(self, self.domain_dn())
928         m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
929         self.modify(m)
930
931     def get_minPwdAge(self):
932         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
933         if len(res) == 0:
934             return None
935         elif "minPwdAge" not in res[0]:
936             return None
937         else:
938             return int(res[0]["minPwdAge"][0])
939
940     def set_maxPwdAge(self, value):
941         if not isinstance(value, binary_type):
942             value = str(value).encode('utf8')
943         m = ldb.Message()
944         m.dn = ldb.Dn(self, self.domain_dn())
945         m["maxPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "maxPwdAge")
946         self.modify(m)
947
948     def get_maxPwdAge(self):
949         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["maxPwdAge"])
950         if len(res) == 0:
951             return None
952         elif "maxPwdAge" not in res[0]:
953             return None
954         else:
955             return int(res[0]["maxPwdAge"][0])
956
957     def set_minPwdLength(self, value):
958         if not isinstance(value, binary_type):
959             value = str(value).encode('utf8')
960         m = ldb.Message()
961         m.dn = ldb.Dn(self, self.domain_dn())
962         m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength")
963         self.modify(m)
964
965     def get_minPwdLength(self):
966         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"])
967         if len(res) == 0:
968             return None
969         elif "minPwdLength" not in res[0]:
970             return None
971         else:
972             return int(res[0]["minPwdLength"][0])
973
974     def set_pwdProperties(self, value):
975         if not isinstance(value, binary_type):
976             value = str(value).encode('utf8')
977         m = ldb.Message()
978         m.dn = ldb.Dn(self, self.domain_dn())
979         m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties")
980         self.modify(m)
981
982     def get_pwdProperties(self):
983         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"])
984         if len(res) == 0:
985             return None
986         elif "pwdProperties" not in res[0]:
987             return None
988         else:
989             return int(res[0]["pwdProperties"][0])
990
991     def set_dsheuristics(self, dsheuristics):
992         m = ldb.Message()
993         m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
994                       % self.get_config_basedn().get_linearized())
995         if dsheuristics is not None:
996             m["dSHeuristics"] = \
997                 ldb.MessageElement(dsheuristics,
998                                    ldb.FLAG_MOD_REPLACE,
999                                    "dSHeuristics")
1000         else:
1001             m["dSHeuristics"] = \
1002                 ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
1003                                    "dSHeuristics")
1004         self.modify(m)
1005
1006     def get_dsheuristics(self):
1007         res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
1008                           % self.get_config_basedn().get_linearized(),
1009                           scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
1010         if len(res) == 0:
1011             dsheuristics = None
1012         elif "dSHeuristics" in res[0]:
1013             dsheuristics = res[0]["dSHeuristics"][0]
1014         else:
1015             dsheuristics = None
1016
1017         return dsheuristics
1018
1019     def create_ou(self, ou_dn, description=None, name=None, sd=None):
1020         """Creates an organizationalUnit object
1021         :param ou_dn: dn of the new object
1022         :param description: description attribute
1023         :param name: name atttribute
1024         :param sd: security descriptor of the object, can be
1025         an SDDL string or security.descriptor type
1026         """
1027         m = {"dn": ou_dn,
1028              "objectClass": "organizationalUnit"}
1029
1030         if description:
1031             m["description"] = description
1032         if name:
1033             m["name"] = name
1034
1035         if sd:
1036             m["nTSecurityDescriptor"] = ndr_pack(sd)
1037         self.add(m)
1038
1039     def sequence_number(self, seq_type):
1040         """Returns the value of the sequence number according to the requested type
1041         :param seq_type: type of sequence number
1042          """
1043         self.transaction_start()
1044         try:
1045             seq = super(SamDB, self).sequence_number(seq_type)
1046         except:
1047             self.transaction_cancel()
1048             raise
1049         else:
1050             self.transaction_commit()
1051         return seq
1052
1053     def get_dsServiceName(self):
1054         '''get the NTDS DN from the rootDSE'''
1055         res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
1056         return str(res[0]["dsServiceName"][0])
1057
1058     def get_serverName(self):
1059         '''get the server DN from the rootDSE'''
1060         res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["serverName"])
1061         return str(res[0]["serverName"][0])
1062
1063     def dns_lookup(self, dns_name, dns_partition=None):
1064         '''Do a DNS lookup in the database, returns the NDR database structures'''
1065         if dns_partition is None:
1066             return dsdb_dns.lookup(self, dns_name)
1067         else:
1068             return dsdb_dns.lookup(self, dns_name,
1069                                    dns_partition=dns_partition)
1070
1071     def dns_extract(self, el):
1072         '''Return the NDR database structures from a dnsRecord element'''
1073         return dsdb_dns.extract(self, el)
1074
1075     def dns_replace(self, dns_name, new_records):
1076         '''Do a DNS modification on the database, sets the NDR database
1077         structures on a DNS name
1078         '''
1079         return dsdb_dns.replace(self, dns_name, new_records)
1080
1081     def dns_replace_by_dn(self, dn, new_records):
1082         '''Do a DNS modification on the database, sets the NDR database
1083         structures on a LDB DN
1084
1085         This routine is important because if the last record on the DN
1086         is removed, this routine will put a tombstone in the record.
1087         '''
1088         return dsdb_dns.replace_by_dn(self, dn, new_records)
1089
1090     def garbage_collect_tombstones(self, dn, current_time,
1091                                    tombstone_lifetime=None):
1092         '''garbage_collect_tombstones(lp, samdb, [dn], current_time, tombstone_lifetime)
1093         -> (num_objects_expunged, num_links_expunged)'''
1094
1095         if tombstone_lifetime is None:
1096             return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1097                                                          current_time)
1098         else:
1099             return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1100                                                          current_time,
1101                                                          tombstone_lifetime)
1102
1103     def create_own_rid_set(self):
1104         '''create a RID set for this DSA'''
1105         return dsdb._dsdb_create_own_rid_set(self)
1106
1107     def allocate_rid(self):
1108         '''return a new RID from the RID Pool on this DSA'''
1109         return dsdb._dsdb_allocate_rid(self)
1110
1111     def normalize_dn_in_domain(self, dn):
1112         '''return a new DN expanded by adding the domain DN
1113
1114         If the dn is already a child of the domain DN, just
1115         return it as-is.
1116
1117         :param dn: relative dn
1118         '''
1119         domain_dn = ldb.Dn(self, self.domain_dn())
1120
1121         if isinstance(dn, ldb.Dn):
1122             dn = str(dn)
1123
1124         full_dn = ldb.Dn(self, dn)
1125         if not full_dn.is_child_of(domain_dn):
1126             full_dn.add_base(domain_dn)
1127         return full_dn