s4-samba.samdb: Fix leading indention and trailing ';'
[mat/samba.git] / source4 / scripting / python / samba / samdb.py
1 #!/usr/bin/env python
2
3 # Unix SMB/CIFS implementation.
4 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010
5 # Copyright (C) Matthias Dieter Wallnoefer 2009
6 #
7 # Based on the original in EJS:
8 # Copyright (C) Andrew Tridgell <tridge@samba.org> 2005
9 #   
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #   
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #   
20 # You should have received a copy of the GNU General Public License
21 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 #
23
24 """Convenience functions for using the SAM."""
25
26 import dsdb
27 import samba
28 import ldb
29 import time
30 import base64
31 from samba.ndr import ndr_unpack, ndr_pack
32 from samba.dcerpc import drsblobs, misc
33
34 __docformat__ = "restructuredText"
35
36 class SamDB(samba.Ldb):
37     """The SAM database."""
38
39     hash_oid_name = {}
40
41     def __init__(self, url=None, lp=None, modules_dir=None, session_info=None,
42                  credentials=None, flags=0, options=None, global_schema=True,
43                  auto_connect=True, am_rodc=None):
44         self.lp = lp
45         if not auto_connect:
46             url = None
47         elif url is None and lp is not None:
48             url = lp.get("sam database")
49
50         super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir,
51                 session_info=session_info, credentials=credentials, flags=flags,
52                 options=options)
53
54         if global_schema:
55             dsdb._dsdb_set_global_schema(self)
56
57         if am_rodc is not None:
58             dsdb._dsdb_set_am_rodc(self, am_rodc)
59
60     def connect(self, url=None, flags=0, options=None):
61         if self.lp is not None:
62             url = self.lp.private_path(url)
63
64         super(SamDB, self).connect(url=url, flags=flags,
65                 options=options)
66
67     def am_rodc(self):
68         return dsdb._am_rodc(self)
69
70     def domain_dn(self):
71         # find the DNs for the domain
72         res = self.search(base="",
73                           scope=ldb.SCOPE_BASE,
74                           expression="(defaultNamingContext=*)",
75                           attrs=["defaultNamingContext"])
76         assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None)
77         return res[0]["defaultNamingContext"][0]
78
79     def enable_account(self, filter):
80         """Enables an account
81         
82         :param filter: LDAP filter to find the user (eg samccountname=name)
83         """
84         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
85                           expression=filter, attrs=["userAccountControl"])
86         assert(len(res) == 1)
87         user_dn = res[0].dn
88
89         userAccountControl = int(res[0]["userAccountControl"][0])
90         if (userAccountControl & 0x2):
91             userAccountControl = userAccountControl & ~0x2 # remove disabled bit
92         if (userAccountControl & 0x20):
93             userAccountControl = userAccountControl & ~0x20 # remove 'no password required' bit
94
95         mod = """
96 dn: %s
97 changetype: modify
98 replace: userAccountControl
99 userAccountControl: %u
100 """ % (user_dn, userAccountControl)
101         self.modify_ldif(mod)
102         
103     def force_password_change_at_next_login(self, filter):
104         """Forces a password change at next login
105         
106         :param filter: LDAP filter to find the user (eg samccountname=name)
107         """
108         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
109                           expression=filter, attrs=[])
110         assert(len(res) == 1)
111         user_dn = res[0].dn
112
113         mod = """
114 dn: %s
115 changetype: modify
116 replace: pwdLastSet
117 pwdLastSet: 0
118 """ % (user_dn)
119         self.modify_ldif(mod)
120
121     def newgroup(self, groupname, groupou=None, grouptype=None,
122                  description=None, mailaddress=None, notes=None):
123         """Adds a new group with additional parameters
124
125         :param groupname: Name of the new group
126         :param grouptype: Type of the new group
127         :param description: Description of the new group
128         :param mailaddress: Email address of the new group
129         :param notes: Notes of the new group
130         """
131
132         group_dn = "CN=%s,%s,%s" % (groupname, (groupou or "CN=Users"), self.domain_dn())
133
134         # The new user record. Note the reliance on the SAMLDB module which
135         # fills in the default informations
136         ldbmessage = {"dn": group_dn,
137             "sAMAccountName": groupname,
138             "objectClass": "group"}
139
140         if grouptype is not None:
141             ldbmessage["groupType"] = "%d" % grouptype
142
143         if description is not None:
144             ldbmessage["description"] = description
145
146         if mailaddress is not None:
147             ldbmessage["mail"] = mailaddress
148
149         if notes is not None:
150             ldbmessage["info"] = notes
151
152         self.add(ldbmessage)
153
154     def deletegroup(self, groupname):
155         """Deletes a group
156
157         :param groupname: Name of the target group
158         """
159
160         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
161         self.transaction_start()
162         try:
163             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
164                                expression=groupfilter, attrs=[])
165             if len(targetgroup) == 0:
166                 raise Exception('Unable to find group "%s"' % groupname)
167             assert(len(targetgroup) == 1)
168             self.delete(targetgroup[0].dn)
169         except:
170             self.transaction_cancel()
171             raise
172         else:
173             self.transaction_commit()
174
175     def add_remove_group_members(self, groupname, listofmembers,
176                                   add_members_operation=True):
177         """Adds or removes group members
178
179         :param groupname: Name of the target group
180         :param listofmembers: Comma-separated list of group members
181         :param add_members_operation: Defines if its an add or remove operation
182         """
183
184         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
185         groupmembers = listofmembers.split(',')
186
187         self.transaction_start()
188         try:
189             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
190                                expression=groupfilter, attrs=['member'])
191             if len(targetgroup) == 0:
192                 raise Exception('Unable to find group "%s"' % groupname)
193             assert(len(targetgroup) == 1)
194
195             modified = False
196
197             addtargettogroup = """
198 dn: %s
199 changetype: modify
200 """ % (str(targetgroup[0].dn))
201
202             for member in groupmembers:
203                 targetmember = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
204                                     expression="(|(sAMAccountName=%s)(CN=%s))" % (member, member), attrs=[])
205
206                 if len(targetmember) != 1:
207                     continue
208
209                 if add_members_operation is True and (targetgroup[0].get('member') is None or str(targetmember[0].dn) not in targetgroup[0]['member']):
210                     modified = True
211                     addtargettogroup += """add: member
212 member: %s
213 """ % (str(targetmember[0].dn))
214
215                 elif add_members_operation is False and (targetgroup[0].get('member') is not None and str(targetmember[0].dn) in targetgroup[0]['member']):
216                     modified = True
217                     addtargettogroup += """delete: member
218 member: %s
219 """ % (str(targetmember[0].dn))
220
221             if modified is True:
222                 self.modify_ldif(addtargettogroup)
223
224         except:
225             self.transaction_cancel()
226             raise
227         else:
228             self.transaction_commit()
229
230     def newuser(self, username, password,
231                 force_password_change_at_next_login_req=False,
232                 useusernameascn=False, userou=None, surname=None, givenname=None, initials=None,
233                 profilepath=None, scriptpath=None, homedrive=None, homedirectory=None,
234                 jobtitle=None, department=None, company=None, description=None,
235                 mailaddress=None, internetaddress=None, telephonenumber=None,
236                 physicaldeliveryoffice=None):
237         """Adds a new user with additional parameters
238
239         :param username: Name of the new user
240         :param password: Password for the new user
241         :param force_password_change_at_next_login_req: Force password change
242         :param useusernameascn: Use username as cn rather that firstname + initials + lastname
243         :param userou: Object container (without domainDN postfix) for new user
244         :param surname: Surname of the new user
245         :param givenname: First name of the new user
246         :param initials: Initials of the new user
247         :param profilepath: Profile path of the new user
248         :param scriptpath: Logon script path of the new user
249         :param homedrive: Home drive of the new user
250         :param homedirectory: Home directory of the new user
251         :param jobtitle: Job title of the new user
252         :param department: Department of the new user
253         :param company: Company of the new user
254         :param description: of the new user
255         :param mailaddress: Email address of the new user
256         :param internetaddress: Home page of the new user
257         :param telephonenumber: Phone number of the new user
258         :param physicaldeliveryoffice: Office location of the new user
259         """
260
261         displayname = ""
262         if givenname is not None:
263             displayname += givenname
264
265         if initials is not None:
266             displayname += ' %s.' % initials
267
268         if surname is not None:
269             displayname += ' %s' % surname
270
271         cn = username
272         if useusernameascn is None and displayname is not "":
273             cn = displayname
274
275         user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn())
276
277         # The new user record. Note the reliance on the SAMLDB module which
278         # fills in the default informations
279         ldbmessage = {"dn": user_dn,
280                       "sAMAccountName": username,
281                       "objectClass": "user"}
282
283         if surname is not None:
284             ldbmessage["sn"] = surname
285
286         if givenname is not None:
287             ldbmessage["givenName"] = givenname
288
289         if displayname is not "":
290             ldbmessage["displayName"] = displayname
291             ldbmessage["name"] = displayname
292
293         if initials is not None:
294             ldbmessage["initials"] = '%s.' % initials
295
296         if profilepath is not None:
297             ldbmessage["profilePath"] = profilepath
298
299         if scriptpath is not None:
300             ldbmessage["scriptPath"] = scriptpath
301
302         if homedrive is not None:
303             ldbmessage["homeDrive"] = homedrive
304
305         if homedirectory is not None:
306             ldbmessage["homeDirectory"] = homedirectory
307
308         if jobtitle is not None:
309             ldbmessage["title"] = jobtitle
310
311         if department is not None:
312             ldbmessage["department"] = department
313
314         if company is not None:
315             ldbmessage["company"] = company
316
317         if description is not None:
318             ldbmessage["description"] = description
319
320         if mailaddress is not None:
321             ldbmessage["mail"] = mailaddress
322
323         if internetaddress is not None:
324             ldbmessage["wWWHomePage"] = internetaddress
325
326         if telephonenumber is not None:
327             ldbmessage["telephoneNumber"] = telephonenumber
328
329         if physicaldeliveryoffice is not None:
330             ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
331
332         self.transaction_start()
333         try:
334             self.add(ldbmessage)
335
336             # Sets the password for it
337             self.setpassword("(dn=" + user_dn + ")", password,
338               force_password_change_at_next_login_req)
339         except:
340             self.transaction_cancel()
341             raise
342         else:
343             self.transaction_commit()
344
345     def setpassword(self, filter, password,
346                     force_change_at_next_login=False,
347                     username=None):
348         """Sets the password for a user
349         
350         :param filter: LDAP filter to find the user (eg samccountname=name)
351         :param password: Password for the user
352         :param force_change_at_next_login: Force password change
353         """
354         self.transaction_start()
355         try:
356             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
357                               expression=filter, attrs=[])
358             if len(res) == 0:
359                 raise Exception('Unable to find user "%s"' % (username or filter))
360             assert(len(res) == 1)
361             user_dn = res[0].dn
362
363             setpw = """
364 dn: %s
365 changetype: modify
366 replace: unicodePwd
367 unicodePwd:: %s
368 """ % (user_dn, base64.b64encode(("\"" + password + "\"").encode('utf-16-le')))
369
370             self.modify_ldif(setpw)
371
372             if force_change_at_next_login:
373                 self.force_password_change_at_next_login(
374                   "(dn=" + str(user_dn) + ")")
375
376             #  modify the userAccountControl to remove the disabled bit
377             self.enable_account(filter)
378         except:
379             self.transaction_cancel()
380             raise
381         else:
382             self.transaction_commit()
383
384     def setexpiry(self, filter, expiry_seconds, no_expiry_req=False):
385         """Sets the account expiry for a user
386         
387         :param filter: LDAP filter to find the user (eg samccountname=name)
388         :param expiry_seconds: expiry time from now in seconds
389         :param no_expiry_req: if set, then don't expire password
390         """
391         self.transaction_start()
392         try:
393             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
394                           expression=filter,
395                           attrs=["userAccountControl", "accountExpires"])
396             assert(len(res) == 1)
397             user_dn = res[0].dn
398
399             userAccountControl = int(res[0]["userAccountControl"][0])
400             accountExpires     = int(res[0]["accountExpires"][0])
401             if no_expiry_req:
402                 userAccountControl = userAccountControl | 0x10000
403                 accountExpires = 0
404             else:
405                 userAccountControl = userAccountControl & ~0x10000
406                 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
407
408             setexp = """
409 dn: %s
410 changetype: modify
411 replace: userAccountControl
412 userAccountControl: %u
413 replace: accountExpires
414 accountExpires: %u
415 """ % (user_dn, userAccountControl, accountExpires)
416
417             self.modify_ldif(setexp)
418         except:
419             self.transaction_cancel()
420             raise
421         else:
422             self.transaction_commit()
423
424     def set_domain_sid(self, sid):
425         """Change the domain SID used by this LDB.
426
427         :param sid: The new domain sid to use.
428         """
429         dsdb._samdb_set_domain_sid(self, sid)
430
431     def get_domain_sid(self):
432         """Read the domain SID used by this LDB.
433
434         """
435         return dsdb._samdb_get_domain_sid(self)
436
437     def set_invocation_id(self, invocation_id):
438         """Set the invocation id for this SamDB handle.
439
440         :param invocation_id: GUID of the invocation id.
441         """
442         dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
443
444     def get_oid_from_attid(self, attid):
445         return dsdb._dsdb_get_oid_from_attid(self, attid)
446
447     def get_attid_from_lDAPDisplayName(self, ldap_display_name, is_schema_nc=False):
448         return dsdb._dsdb_get_attid_from_lDAPDisplayName(self, ldap_display_name, is_schema_nc)
449
450     def get_invocation_id(self):
451         "Get the invocation_id id"
452         return dsdb._samdb_ntds_invocation_id(self)
453
454     def set_ntds_settings_dn(self, ntds_settings_dn):
455         """Set the NTDS Settings DN, as would be returned on the dsServiceName rootDSE attribute
456
457         This allows the DN to be set before the database fully exists
458
459         :param ntds_settings_dn: The new DN to use
460         """
461         dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
462
463     invocation_id = property(get_invocation_id, set_invocation_id)
464
465     domain_sid = property(get_domain_sid, set_domain_sid)
466
467     def get_ntds_GUID(self):
468         "Get the NTDS objectGUID"
469         return dsdb._samdb_ntds_objectGUID(self)
470
471     def server_site_name(self):
472         "Get the server site name"
473         return dsdb._samdb_server_site_name(self)
474
475     def load_partition_usn(self, base_dn):
476         return dsdb._dsdb_load_partition_usn(self, base_dn)
477
478     def set_schema(self, schema):
479         self.set_schema_from_ldb(schema.ldb)
480
481     def set_schema_from_ldb(self, ldb):
482         dsdb._dsdb_set_schema_from_ldb(self, ldb)
483
484     def get_attribute_from_attid(self, attid):
485         """ Get from an attid the associated attribute
486
487            :param attid: The attribute id for searched attribute
488            :return: The name of the attribute associated with this id
489         """
490         if len(self.hash_oid_name.keys()) == 0:
491             self._populate_oid_attid()
492         if self.hash_oid_name.has_key(self.get_oid_from_attid(attid)):
493             return self.hash_oid_name[self.get_oid_from_attid(attid)]
494         else:
495             return None
496
497
498     def _populate_oid_attid(self):
499         """Populate the hash hash_oid_name
500
501            This hash contains the oid of the attribute as a key and
502            its display name as a value
503         """
504         self.hash_oid_name = {}
505         res = self.search(expression="objectClass=attributeSchema",
506                            controls=["search_options:1:2"],
507                            attrs=["attributeID",
508                            "lDAPDisplayName"])
509         if len(res) > 0:
510             for e in res:
511                 strDisplay = str(e.get("lDAPDisplayName"))
512                 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
513
514
515     def get_attribute_replmetadata_version(self, dn, att):
516         """ Get the version field trom the replPropertyMetaData for
517             the given field
518
519            :param dn: The on which we want to get the version
520            :param att: The name of the attribute
521            :return: The value of the version field in the replPropertyMetaData
522              for the given attribute. None if the attribute is not replicated
523         """
524
525         res = self.search(expression="dn=%s" % dn,
526                             scope=ldb.SCOPE_SUBTREE,
527                             controls=["search_options:1:2"],
528                             attrs=["replPropertyMetaData"])
529         if len(res) == 0:
530             return None
531
532         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
533                             str(res[0]["replPropertyMetaData"]))
534         ctr = repl.ctr
535         if len(self.hash_oid_name.keys()) == 0:
536             self._populate_oid_attid()
537         for o in ctr.array:
538             # Search for Description
539             att_oid = self.get_oid_from_attid(o.attid)
540             if self.hash_oid_name.has_key(att_oid) and\
541                att.lower() == self.hash_oid_name[att_oid].lower():
542                 return o.version
543         return None
544
545
546     def set_attribute_replmetadata_version(self, dn, att, value, addifnotexist=False):
547         res = self.search(expression="dn=%s" % dn,
548                             scope=ldb.SCOPE_SUBTREE,
549                             controls=["search_options:1:2"],
550                             attrs=["replPropertyMetaData"])
551         if len(res) == 0:
552             return None
553
554         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
555                             str(res[0]["replPropertyMetaData"]))
556         ctr = repl.ctr
557         now = samba.unix2nttime(int(time.time()))
558         found = False
559         if len(self.hash_oid_name.keys()) == 0:
560             self._populate_oid_attid()
561         for o in ctr.array:
562             # Search for Description
563             att_oid = self.get_oid_from_attid(o.attid)
564             if self.hash_oid_name.has_key(att_oid) and\
565                att.lower() == self.hash_oid_name[att_oid].lower():
566                 found = True
567                 seq = self.sequence_number(ldb.SEQ_NEXT)
568                 o.version = value
569                 o.originating_change_time = now
570                 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
571                 o.originating_usn = seq
572                 o.local_usn = seq
573
574         if not found and addifnotexist and len(ctr.array) >0:
575             o2 = drsblobs.replPropertyMetaData1()
576             o2.attid = 589914
577             att_oid = self.get_oid_from_attid(o2.attid)
578             seq = self.sequence_number(ldb.SEQ_NEXT)
579             o2.version = value
580             o2.originating_change_time = now
581             o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
582             o2.originating_usn = seq
583             o2.local_usn = seq
584             found = True
585             tab = ctr.array
586             tab.append(o2)
587             ctr.count = ctr.count + 1
588             ctr.array = tab
589
590         if found :
591             replBlob = ndr_pack(repl)
592             msg = ldb.Message()
593             msg.dn = res[0].dn
594             msg["replPropertyMetaData"] = ldb.MessageElement(replBlob,
595                                                 ldb.FLAG_MOD_REPLACE,
596                                                 "replPropertyMetaData")
597             self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
598
599
600     def write_prefixes_from_schema(self):
601         dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
602
603     def get_partitions_dn(self):
604         return dsdb._dsdb_get_partitions_dn(self)