tests: Add test-case for 'group list --verbose'
[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 != "":
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 |
399                                                    dsdb.UF_SMARTCARD_REQUIRED)
400             setpassword = False
401
402         if surname is not None:
403             ldbmessage["sn"] = surname
404
405         if givenname is not None:
406             ldbmessage["givenName"] = givenname
407
408         if displayname != "":
409             ldbmessage["displayName"] = displayname
410             ldbmessage["name"] = displayname
411
412         if initials is not None:
413             ldbmessage["initials"] = '%s.' % initials
414
415         if profilepath is not None:
416             ldbmessage["profilePath"] = profilepath
417
418         if scriptpath is not None:
419             ldbmessage["scriptPath"] = scriptpath
420
421         if homedrive is not None:
422             ldbmessage["homeDrive"] = homedrive
423
424         if homedirectory is not None:
425             ldbmessage["homeDirectory"] = homedirectory
426
427         if jobtitle is not None:
428             ldbmessage["title"] = jobtitle
429
430         if department is not None:
431             ldbmessage["department"] = department
432
433         if company is not None:
434             ldbmessage["company"] = company
435
436         if description is not None:
437             ldbmessage["description"] = description
438
439         if mailaddress is not None:
440             ldbmessage["mail"] = mailaddress
441
442         if internetaddress is not None:
443             ldbmessage["wWWHomePage"] = internetaddress
444
445         if telephonenumber is not None:
446             ldbmessage["telephoneNumber"] = telephonenumber
447
448         if physicaldeliveryoffice is not None:
449             ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
450
451         if sd is not None:
452             ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
453
454         ldbmessage2 = None
455         if any(map(lambda b: b is not None, (uid, uidnumber, gidnumber, gecos,
456                                              loginshell, nisdomain, unixhome))):
457             ldbmessage2 = ldb.Message()
458             ldbmessage2.dn = ldb.Dn(self, user_dn)
459             if uid is not None:
460                 ldbmessage2["uid"] = ldb.MessageElement(str(uid), ldb.FLAG_MOD_REPLACE, 'uid')
461             if uidnumber is not None:
462                 ldbmessage2["uidNumber"] = ldb.MessageElement(str(uidnumber), ldb.FLAG_MOD_REPLACE, 'uidNumber')
463             if gidnumber is not None:
464                 ldbmessage2["gidNumber"] = ldb.MessageElement(str(gidnumber), ldb.FLAG_MOD_REPLACE, 'gidNumber')
465             if gecos is not None:
466                 ldbmessage2["gecos"] = ldb.MessageElement(str(gecos), ldb.FLAG_MOD_REPLACE, 'gecos')
467             if loginshell is not None:
468                 ldbmessage2["loginShell"] = ldb.MessageElement(str(loginshell), ldb.FLAG_MOD_REPLACE, 'loginShell')
469             if unixhome is not None:
470                 ldbmessage2["unixHomeDirectory"] = ldb.MessageElement(
471                     str(unixhome), ldb.FLAG_MOD_REPLACE, 'unixHomeDirectory')
472             if nisdomain is not None:
473                 ldbmessage2["msSFU30NisDomain"] = ldb.MessageElement(
474                     str(nisdomain), ldb.FLAG_MOD_REPLACE, 'msSFU30NisDomain')
475                 ldbmessage2["msSFU30Name"] = ldb.MessageElement(
476                     str(username), ldb.FLAG_MOD_REPLACE, 'msSFU30Name')
477                 ldbmessage2["unixUserPassword"] = ldb.MessageElement(
478                     'ABCD!efgh12345$67890', ldb.FLAG_MOD_REPLACE,
479                     'unixUserPassword')
480
481         self.transaction_start()
482         try:
483             self.add(ldbmessage)
484             if ldbmessage2:
485                 self.modify(ldbmessage2)
486
487             # Sets the password for it
488             if setpassword:
489                 self.setpassword(("(distinguishedName=%s)" %
490                                   ldb.binary_encode(user_dn)),
491                                  password,
492                                  force_password_change_at_next_login_req)
493         except:
494             self.transaction_cancel()
495             raise
496         else:
497             self.transaction_commit()
498
499     def newcomputer(self, computername, computerou=None, description=None,
500                     prepare_oldjoin=False, ip_address_list=None,
501                     service_principal_name_list=None):
502         """Adds a new user with additional parameters
503
504         :param computername: Name of the new computer
505         :param computerou: Object container for new computer
506         :param description: Description of the new computer
507         :param prepare_oldjoin: Preset computer password for oldjoin mechanism
508         :param ip_address_list: ip address list for DNS A or AAAA record
509         :param service_principal_name_list: string list of servicePincipalName
510         """
511
512         cn = re.sub(r"\$$", "", computername)
513         if cn.count('$'):
514             raise Exception('Illegal computername "%s"' % computername)
515         samaccountname = "%s$" % cn
516
517         computercontainer_dn = "CN=Computers,%s" % self.domain_dn()
518         if computerou:
519             computercontainer_dn = self.normalize_dn_in_domain(computerou)
520
521         computer_dn = "CN=%s,%s" % (cn, computercontainer_dn)
522
523         ldbmessage = {"dn": computer_dn,
524                       "sAMAccountName": samaccountname,
525                       "objectClass": "computer",
526                       }
527
528         if description is not None:
529             ldbmessage["description"] = description
530
531         if service_principal_name_list:
532             ldbmessage["servicePrincipalName"] = service_principal_name_list
533
534         accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
535                              dsdb.UF_ACCOUNTDISABLE)
536         if prepare_oldjoin:
537             accountcontrol = str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
538         ldbmessage["userAccountControl"] = accountcontrol
539
540         if ip_address_list:
541             ldbmessage['dNSHostName'] = '{}.{}'.format(
542                 cn, self.domain_dns_name())
543
544         self.transaction_start()
545         try:
546             self.add(ldbmessage)
547
548             if prepare_oldjoin:
549                 password = cn.lower()
550                 self.setpassword(("(distinguishedName=%s)" %
551                                   ldb.binary_encode(computer_dn)),
552                                  password, False)
553         except:
554             self.transaction_cancel()
555             raise
556         else:
557             self.transaction_commit()
558
559     def deleteuser(self, username):
560         """Deletes a user
561
562         :param username: Name of the target user
563         """
564
565         filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn())
566         self.transaction_start()
567         try:
568             target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
569                                  expression=filter, attrs=[])
570             if len(target) == 0:
571                 raise Exception('Unable to find user "%s"' % username)
572             assert(len(target) == 1)
573             self.delete(target[0].dn)
574         except:
575             self.transaction_cancel()
576             raise
577         else:
578             self.transaction_commit()
579
580     def setpassword(self, search_filter, password,
581                     force_change_at_next_login=False, username=None):
582         """Sets the password for a user
583
584         :param search_filter: LDAP filter to find the user (eg
585             samccountname=name)
586         :param password: Password for the user
587         :param force_change_at_next_login: Force password change
588         """
589         self.transaction_start()
590         try:
591             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
592                               expression=search_filter, attrs=[])
593             if len(res) == 0:
594                 raise Exception('Unable to find user "%s"' % (username or search_filter))
595             if len(res) > 1:
596                 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter))
597             user_dn = res[0].dn
598             if not isinstance(password, text_type):
599                 pw = password.decode('utf-8')
600             else:
601                 pw = password
602             pw = ('"' + pw + '"').encode('utf-16-le')
603             setpw = """
604 dn: %s
605 changetype: modify
606 replace: unicodePwd
607 unicodePwd:: %s
608 """ % (user_dn, base64.b64encode(pw).decode('utf-8'))
609
610             self.modify_ldif(setpw)
611
612             if force_change_at_next_login:
613                 self.force_password_change_at_next_login(
614                     "(distinguishedName=" + str(user_dn) + ")")
615
616             #  modify the userAccountControl to remove the disabled bit
617             self.enable_account(search_filter)
618         except:
619             self.transaction_cancel()
620             raise
621         else:
622             self.transaction_commit()
623
624     def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False):
625         """Sets the account expiry for a user
626
627         :param search_filter: LDAP filter to find the user (eg
628             samaccountname=name)
629         :param expiry_seconds: expiry time from now in seconds
630         :param no_expiry_req: if set, then don't expire password
631         """
632         self.transaction_start()
633         try:
634             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
635                               expression=search_filter,
636                               attrs=["userAccountControl", "accountExpires"])
637             if len(res) == 0:
638                 raise Exception('Unable to find user "%s"' % search_filter)
639             assert(len(res) == 1)
640             user_dn = res[0].dn
641
642             userAccountControl = int(res[0]["userAccountControl"][0])
643             accountExpires     = int(res[0]["accountExpires"][0])
644             if no_expiry_req:
645                 userAccountControl = userAccountControl | 0x10000
646                 accountExpires = 0
647             else:
648                 userAccountControl = userAccountControl & ~0x10000
649                 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
650
651             setexp = """
652 dn: %s
653 changetype: modify
654 replace: userAccountControl
655 userAccountControl: %u
656 replace: accountExpires
657 accountExpires: %u
658 """ % (user_dn, userAccountControl, accountExpires)
659
660             self.modify_ldif(setexp)
661         except:
662             self.transaction_cancel()
663             raise
664         else:
665             self.transaction_commit()
666
667     def set_domain_sid(self, sid):
668         """Change the domain SID used by this LDB.
669
670         :param sid: The new domain sid to use.
671         """
672         dsdb._samdb_set_domain_sid(self, sid)
673
674     def get_domain_sid(self):
675         """Read the domain SID used by this LDB. """
676         return dsdb._samdb_get_domain_sid(self)
677
678     domain_sid = property(get_domain_sid, set_domain_sid,
679                           doc="SID for the domain")
680
681     def set_invocation_id(self, invocation_id):
682         """Set the invocation id for this SamDB handle.
683
684         :param invocation_id: GUID of the invocation id.
685         """
686         dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
687
688     def get_invocation_id(self):
689         """Get the invocation_id id"""
690         return dsdb._samdb_ntds_invocation_id(self)
691
692     invocation_id = property(get_invocation_id, set_invocation_id,
693                              doc="Invocation ID GUID")
694
695     def get_oid_from_attid(self, attid):
696         return dsdb._dsdb_get_oid_from_attid(self, attid)
697
698     def get_attid_from_lDAPDisplayName(self, ldap_display_name,
699                                        is_schema_nc=False):
700         '''return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI'''
701         return dsdb._dsdb_get_attid_from_lDAPDisplayName(self,
702                                                          ldap_display_name, is_schema_nc)
703
704     def get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name):
705         '''return the syntax OID for a LDAP attribute as a string'''
706         return dsdb._dsdb_get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name)
707
708     def get_systemFlags_from_lDAPDisplayName(self, ldap_display_name):
709         '''return the systemFlags for a LDAP attribute as a integer'''
710         return dsdb._dsdb_get_systemFlags_from_lDAPDisplayName(self, ldap_display_name)
711
712     def get_linkId_from_lDAPDisplayName(self, ldap_display_name):
713         '''return the linkID for a LDAP attribute as a integer'''
714         return dsdb._dsdb_get_linkId_from_lDAPDisplayName(self, ldap_display_name)
715
716     def get_lDAPDisplayName_by_attid(self, attid):
717         '''return the lDAPDisplayName from an integer DRS attribute ID'''
718         return dsdb._dsdb_get_lDAPDisplayName_by_attid(self, attid)
719
720     def get_backlink_from_lDAPDisplayName(self, ldap_display_name):
721         '''return the attribute name of the corresponding backlink from the name
722         of a forward link attribute. If there is no backlink return None'''
723         return dsdb._dsdb_get_backlink_from_lDAPDisplayName(self, ldap_display_name)
724
725     def set_ntds_settings_dn(self, ntds_settings_dn):
726         """Set the NTDS Settings DN, as would be returned on the dsServiceName
727         rootDSE attribute.
728
729         This allows the DN to be set before the database fully exists
730
731         :param ntds_settings_dn: The new DN to use
732         """
733         dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
734
735     def get_ntds_GUID(self):
736         """Get the NTDS objectGUID"""
737         return dsdb._samdb_ntds_objectGUID(self)
738
739     def server_site_name(self):
740         """Get the server site name"""
741         return dsdb._samdb_server_site_name(self)
742
743     def host_dns_name(self):
744         """return the DNS name of this host"""
745         res = self.search(base='', scope=ldb.SCOPE_BASE, attrs=['dNSHostName'])
746         return str(res[0]['dNSHostName'][0])
747
748     def domain_dns_name(self):
749         """return the DNS name of the domain root"""
750         domain_dn = self.get_default_basedn()
751         return domain_dn.canonical_str().split('/')[0]
752
753     def forest_dns_name(self):
754         """return the DNS name of the forest root"""
755         forest_dn = self.get_root_basedn()
756         return forest_dn.canonical_str().split('/')[0]
757
758     def load_partition_usn(self, base_dn):
759         return dsdb._dsdb_load_partition_usn(self, base_dn)
760
761     def set_schema(self, schema, write_indices_and_attributes=True):
762         self.set_schema_from_ldb(schema.ldb, write_indices_and_attributes=write_indices_and_attributes)
763
764     def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True):
765         dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes)
766
767     def set_schema_update_now(self):
768         ldif = """
769 dn:
770 changetype: modify
771 add: schemaUpdateNow
772 schemaUpdateNow: 1
773 """
774         self.modify_ldif(ldif)
775
776     def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
777         '''convert a list of attribute values to a DRSUAPI DsReplicaAttribute'''
778         return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
779
780     def dsdb_normalise_attributes(self, ldb, ldap_display_name, ldif_elements):
781         '''normalise a list of attribute values'''
782         return dsdb._dsdb_normalise_attributes(ldb, ldap_display_name, ldif_elements)
783
784     def get_attribute_from_attid(self, attid):
785         """ Get from an attid the associated attribute
786
787         :param attid: The attribute id for searched attribute
788         :return: The name of the attribute associated with this id
789         """
790         if len(self.hash_oid_name.keys()) == 0:
791             self._populate_oid_attid()
792         if self.get_oid_from_attid(attid) in self.hash_oid_name:
793             return self.hash_oid_name[self.get_oid_from_attid(attid)]
794         else:
795             return None
796
797     def _populate_oid_attid(self):
798         """Populate the hash hash_oid_name.
799
800         This hash contains the oid of the attribute as a key and
801         its display name as a value
802         """
803         self.hash_oid_name = {}
804         res = self.search(expression="objectClass=attributeSchema",
805                           controls=["search_options:1:2"],
806                           attrs=["attributeID",
807                                  "lDAPDisplayName"])
808         if len(res) > 0:
809             for e in res:
810                 strDisplay = str(e.get("lDAPDisplayName"))
811                 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
812
813     def get_attribute_replmetadata_version(self, dn, att):
814         """Get the version field trom the replPropertyMetaData for
815         the given field
816
817         :param dn: The on which we want to get the version
818         :param att: The name of the attribute
819         :return: The value of the version field in the replPropertyMetaData
820             for the given attribute. None if the attribute is not replicated
821         """
822
823         res = self.search(expression="distinguishedName=%s" % dn,
824                           scope=ldb.SCOPE_SUBTREE,
825                           controls=["search_options:1:2"],
826                           attrs=["replPropertyMetaData"])
827         if len(res) == 0:
828             return None
829
830         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
831                           res[0]["replPropertyMetaData"][0])
832         ctr = repl.ctr
833         if len(self.hash_oid_name.keys()) == 0:
834             self._populate_oid_attid()
835         for o in ctr.array:
836             # Search for Description
837             att_oid = self.get_oid_from_attid(o.attid)
838             if att_oid in self.hash_oid_name and\
839                att.lower() == self.hash_oid_name[att_oid].lower():
840                 return o.version
841         return None
842
843     def set_attribute_replmetadata_version(self, dn, att, value,
844                                            addifnotexist=False):
845         res = self.search(expression="distinguishedName=%s" % dn,
846                           scope=ldb.SCOPE_SUBTREE,
847                           controls=["search_options:1:2"],
848                           attrs=["replPropertyMetaData"])
849         if len(res) == 0:
850             return None
851
852         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
853                           res[0]["replPropertyMetaData"][0])
854         ctr = repl.ctr
855         now = samba.unix2nttime(int(time.time()))
856         found = False
857         if len(self.hash_oid_name.keys()) == 0:
858             self._populate_oid_attid()
859         for o in ctr.array:
860             # Search for Description
861             att_oid = self.get_oid_from_attid(o.attid)
862             if att_oid in self.hash_oid_name and\
863                att.lower() == self.hash_oid_name[att_oid].lower():
864                 found = True
865                 seq = self.sequence_number(ldb.SEQ_NEXT)
866                 o.version = value
867                 o.originating_change_time = now
868                 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
869                 o.originating_usn = seq
870                 o.local_usn = seq
871
872         if not found and addifnotexist and len(ctr.array) > 0:
873             o2 = drsblobs.replPropertyMetaData1()
874             o2.attid = 589914
875             att_oid = self.get_oid_from_attid(o2.attid)
876             seq = self.sequence_number(ldb.SEQ_NEXT)
877             o2.version = value
878             o2.originating_change_time = now
879             o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
880             o2.originating_usn = seq
881             o2.local_usn = seq
882             found = True
883             tab = ctr.array
884             tab.append(o2)
885             ctr.count = ctr.count + 1
886             ctr.array = tab
887
888         if found:
889             replBlob = ndr_pack(repl)
890             msg = ldb.Message()
891             msg.dn = res[0].dn
892             msg["replPropertyMetaData"] = \
893                 ldb.MessageElement(replBlob,
894                                    ldb.FLAG_MOD_REPLACE,
895                                    "replPropertyMetaData")
896             self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
897
898     def write_prefixes_from_schema(self):
899         dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
900
901     def get_partitions_dn(self):
902         return dsdb._dsdb_get_partitions_dn(self)
903
904     def get_nc_root(self, dn):
905         return dsdb._dsdb_get_nc_root(self, dn)
906
907     def get_wellknown_dn(self, nc_root, wkguid):
908         h_nc = self.hash_well_known.get(str(nc_root))
909         dn = None
910         if h_nc is not None:
911             dn = h_nc.get(wkguid)
912         if dn is None:
913             dn = dsdb._dsdb_get_wellknown_dn(self, nc_root, wkguid)
914             if dn is None:
915                 return dn
916             if h_nc is None:
917                 self.hash_well_known[str(nc_root)] = {}
918                 h_nc = self.hash_well_known[str(nc_root)]
919             h_nc[wkguid] = dn
920         return dn
921
922     def set_minPwdAge(self, value):
923         value = str(value).encode('utf8')
924         m = ldb.Message()
925         m.dn = ldb.Dn(self, self.domain_dn())
926         m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
927         self.modify(m)
928
929     def get_minPwdAge(self):
930         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
931         if len(res) == 0:
932             return None
933         elif "minPwdAge" not in res[0]:
934             return None
935         else:
936             return int(res[0]["minPwdAge"][0])
937
938     def set_maxPwdAge(self, value):
939         value = str(value).encode('utf8')
940         m = ldb.Message()
941         m.dn = ldb.Dn(self, self.domain_dn())
942         m["maxPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "maxPwdAge")
943         self.modify(m)
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 "maxPwdAge" not in res[0]:
950             return None
951         else:
952             return int(res[0]["maxPwdAge"][0])
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 "minPwdLength" not 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 "pwdProperties" not 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"] = \
992                 ldb.MessageElement(dsheuristics,
993                                    ldb.FLAG_MOD_REPLACE,
994                                    "dSHeuristics")
995         else:
996             m["dSHeuristics"] = \
997                 ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
998                                    "dSHeuristics")
999         self.modify(m)
1000
1001     def get_dsheuristics(self):
1002         res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
1003                           % self.get_config_basedn().get_linearized(),
1004                           scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
1005         if len(res) == 0:
1006             dsheuristics = None
1007         elif "dSHeuristics" in res[0]:
1008             dsheuristics = res[0]["dSHeuristics"][0]
1009         else:
1010             dsheuristics = None
1011
1012         return dsheuristics
1013
1014     def create_ou(self, ou_dn, description=None, name=None, sd=None):
1015         """Creates an organizationalUnit object
1016         :param ou_dn: dn of the new object
1017         :param description: description attribute
1018         :param name: name atttribute
1019         :param sd: security descriptor of the object, can be
1020         an SDDL string or security.descriptor type
1021         """
1022         m = {"dn": ou_dn,
1023              "objectClass": "organizationalUnit"}
1024
1025         if description:
1026             m["description"] = description
1027         if name:
1028             m["name"] = name
1029
1030         if sd:
1031             m["nTSecurityDescriptor"] = ndr_pack(sd)
1032         self.add(m)
1033
1034     def sequence_number(self, seq_type):
1035         """Returns the value of the sequence number according to the requested type
1036         :param seq_type: type of sequence number
1037          """
1038         self.transaction_start()
1039         try:
1040             seq = super(SamDB, self).sequence_number(seq_type)
1041         except:
1042             self.transaction_cancel()
1043             raise
1044         else:
1045             self.transaction_commit()
1046         return seq
1047
1048     def get_dsServiceName(self):
1049         '''get the NTDS DN from the rootDSE'''
1050         res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
1051         return str(res[0]["dsServiceName"][0])
1052
1053     def get_serverName(self):
1054         '''get the server DN from the rootDSE'''
1055         res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["serverName"])
1056         return str(res[0]["serverName"][0])
1057
1058     def dns_lookup(self, dns_name, dns_partition=None):
1059         '''Do a DNS lookup in the database, returns the NDR database structures'''
1060         if dns_partition is None:
1061             return dsdb_dns.lookup(self, dns_name)
1062         else:
1063             return dsdb_dns.lookup(self, dns_name,
1064                                    dns_partition=dns_partition)
1065
1066     def dns_extract(self, el):
1067         '''Return the NDR database structures from a dnsRecord element'''
1068         return dsdb_dns.extract(self, el)
1069
1070     def dns_replace(self, dns_name, new_records):
1071         '''Do a DNS modification on the database, sets the NDR database
1072         structures on a DNS name
1073         '''
1074         return dsdb_dns.replace(self, dns_name, new_records)
1075
1076     def dns_replace_by_dn(self, dn, new_records):
1077         '''Do a DNS modification on the database, sets the NDR database
1078         structures on a LDB DN
1079
1080         This routine is important because if the last record on the DN
1081         is removed, this routine will put a tombstone in the record.
1082         '''
1083         return dsdb_dns.replace_by_dn(self, dn, new_records)
1084
1085     def garbage_collect_tombstones(self, dn, current_time,
1086                                    tombstone_lifetime=None):
1087         '''garbage_collect_tombstones(lp, samdb, [dn], current_time, tombstone_lifetime)
1088         -> (num_objects_expunged, num_links_expunged)'''
1089
1090         if tombstone_lifetime is None:
1091             return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1092                                                          current_time)
1093         else:
1094             return dsdb._dsdb_garbage_collect_tombstones(self, dn,
1095                                                          current_time,
1096                                                          tombstone_lifetime)
1097
1098     def create_own_rid_set(self):
1099         '''create a RID set for this DSA'''
1100         return dsdb._dsdb_create_own_rid_set(self)
1101
1102     def allocate_rid(self):
1103         '''return a new RID from the RID Pool on this DSA'''
1104         return dsdb._dsdb_allocate_rid(self)
1105
1106     def normalize_dn_in_domain(self, dn):
1107         '''return a new DN expanded by adding the domain DN
1108
1109         If the dn is already a child of the domain DN, just
1110         return it as-is.
1111
1112         :param dn: relative dn
1113         '''
1114         domain_dn = ldb.Dn(self, self.domain_dn())
1115
1116         if isinstance(dn, ldb.Dn):
1117             dn = str(dn)
1118
1119         full_dn = ldb.Dn(self, dn)
1120         if not full_dn.is_child_of(domain_dn):
1121             full_dn.add_base(domain_dn)
1122         return full_dn