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