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