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