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