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