Some more formatting fixes, move schema related functions from Ldb to Schema.
[kamenim/samba.git] / source4 / scripting / python / samba / samdb.py
1 #!/usr/bin/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
32 __docformat__ = "restructuredText"
33
34 class SamDB(samba.Ldb):
35     """The SAM database."""
36
37     def __init__(self, url=None, lp=None, modules_dir=None, session_info=None,
38                  credentials=None, flags=0, options=None, global_schema=True, auto_connect=True,
39                  am_rodc=False):
40         self.lp = lp
41         if not auto_connect:
42             url = None
43         elif url is None and lp is not None:
44             url = lp.get("sam database")
45
46         super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir,
47                 session_info=session_info, credentials=credentials, flags=flags,
48                 options=options)
49
50         if global_schema:
51             dsdb.dsdb_set_global_schema(self)
52
53         dsdb.dsdb_set_am_rodc(self, am_rodc)
54
55     def connect(self, url=None, flags=0, options=None):
56         if self.lp is not None:
57             url = self.lp.private_path(url)
58
59         super(SamDB, self).connect(url=url, flags=flags,
60                 options=options)
61
62     def domain_dn(self):
63         # find the DNs for the domain
64         res = self.search(base="",
65                           scope=ldb.SCOPE_BASE,
66                           expression="(defaultNamingContext=*)",
67                           attrs=["defaultNamingContext"])
68         assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None)
69         return res[0]["defaultNamingContext"][0]
70
71     def enable_account(self, filter):
72         """Enables an account
73         
74         :param filter: LDAP filter to find the user (eg samccountname=name)
75         """
76         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
77                           expression=filter, attrs=["userAccountControl"])
78         assert(len(res) == 1)
79         user_dn = res[0].dn
80
81         userAccountControl = int(res[0]["userAccountControl"][0])
82         if (userAccountControl & 0x2):
83             userAccountControl = userAccountControl & ~0x2 # remove disabled bit
84         if (userAccountControl & 0x20):
85             userAccountControl = userAccountControl & ~0x20 # remove 'no password required' bit
86
87         mod = """
88 dn: %s
89 changetype: modify
90 replace: userAccountControl
91 userAccountControl: %u
92 """ % (user_dn, userAccountControl)
93         self.modify_ldif(mod)
94         
95     def force_password_change_at_next_login(self, filter):
96         """Forces a password change at next login
97         
98         :param filter: LDAP filter to find the user (eg samccountname=name)
99         """
100         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
101                           expression=filter, attrs=[])
102         assert(len(res) == 1)
103         user_dn = res[0].dn
104
105         mod = """
106 dn: %s
107 changetype: modify
108 replace: pwdLastSet
109 pwdLastSet: 0
110 """ % (user_dn)
111         self.modify_ldif(mod)
112
113     def newgroup(self, groupname, groupou=None, grouptype=None,
114                  description=None, mailaddress=None, notes=None):
115         """Adds a new group with additional parameters
116
117         :param groupname: Name of the new group
118         :param grouptype: Type of the new group
119         :param description: Description of the new group
120         :param mailaddress: Email address of the new group
121         :param notes: Notes of the new group
122         """
123
124         group_dn = "CN=%s,%s,%s" % (groupname, (groupou or "CN=Users"), self.domain_dn())
125
126         # The new user record. Note the reliance on the SAMLDB module which
127         # fills in the default informations
128         ldbmessage = {"dn": group_dn,
129             "sAMAccountName": groupname,
130             "objectClass": "group"}
131
132         if grouptype is not None:
133             ldbmessage["groupType"] = "%d" % ((grouptype)-2**32)
134
135         if description is not None:
136             ldbmessage["description"] = description
137
138         if mailaddress is not None:
139             ldbmessage["mail"] = mailaddress
140
141         if notes is not None:
142             ldbmessage["info"] = notes
143
144         self.transaction_start()
145         try:
146             self.add(ldbmessage)
147         except:
148             self.transaction_cancel()
149             raise
150         else:
151             self.transaction_commit()
152
153     def deletegroup(self, groupname):
154         """Deletes a group
155
156         :param groupname: Name of the target group
157         """
158
159         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
160         self.transaction_start()
161         try:
162             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
163                                expression=groupfilter, attrs=[])
164             if len(targetgroup) == 0:
165                 raise Exception('Unable to find group "%s"' % groupname)
166             assert(len(targetgroup) == 1)
167             self.delete(targetgroup[0].dn);
168         except:
169             self.transaction_cancel()
170             raise
171         else:
172             self.transaction_commit()
173
174     def add_remove_group_members(self, groupname, listofmembers,
175                                   add_members_operation=True):
176         """Adds or removes group members
177
178         :param groupname: Name of the target group
179         :param listofmembers: Comma-separated list of group members
180         :param add_members_operation: Defines if its an add or remove operation
181         """
182
183         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
184         groupmembers = listofmembers.split(',')
185
186         self.transaction_start()
187         try:
188             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
189                                expression=groupfilter, attrs=['member'])
190             if len(targetgroup) == 0:
191                 raise Exception('Unable to find group "%s"' % groupname)
192             assert(len(targetgroup) == 1)
193
194             modified = False
195
196             addtargettogroup = """
197 dn: %s
198 changetype: modify
199 """ % (str(targetgroup[0].dn))
200
201             for member in groupmembers:
202                 targetmember = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
203                                     expression="(|(sAMAccountName=%s)(CN=%s))" % (member, member), attrs=[])
204
205                 if len(targetmember) != 1:
206                    continue
207
208                 if add_members_operation is True and (targetgroup[0].get('member') is None or str(targetmember[0].dn) not in targetgroup[0]['member']):
209                    modified = True
210                    addtargettogroup += """add: member
211 member: %s
212 """ % (str(targetmember[0].dn))
213
214                 elif add_members_operation is False and (targetgroup[0].get('member') is not None and str(targetmember[0].dn) in targetgroup[0]['member']):
215                    modified = True
216                    addtargettogroup += """delete: member
217 member: %s
218 """ % (str(targetmember[0].dn))
219
220             if modified is True:
221                self.modify_ldif(addtargettogroup)
222
223         except:
224             self.transaction_cancel()
225             raise
226         else:
227             self.transaction_commit()
228
229     def newuser(self, username, password,
230                 force_password_change_at_next_login_req=False,
231                 useusernameascn=False, userou=None, surname=None, givenname=None, initials=None,
232                 profilepath=None, scriptpath=None, homedrive=None, homedirectory=None,
233                 jobtitle=None, department=None, company=None, description=None,
234                 mailaddress=None, internetaddress=None, telephonenumber=None,
235                 physicaldeliveryoffice=None):
236         """Adds a new user with additional parameters
237
238         :param username: Name of the new user
239         :param password: Password for the new user
240         :param force_password_change_at_next_login_req: Force password change
241         :param useusernameascn: Use username as cn rather that firstname + initials + lastname
242         :param userou: Object container (without domainDN postfix) for new user
243         :param surname: Surname of the new user
244         :param givenname: First name of the new user
245         :param initials: Initials of the new user
246         :param profilepath: Profile path of the new user
247         :param scriptpath: Logon script path of the new user
248         :param homedrive: Home drive of the new user
249         :param homedirectory: Home directory of the new user
250         :param jobtitle: Job title of the new user
251         :param department: Department of the new user
252         :param company: Company of the new user
253         :param description: of the new user
254         :param mailaddress: Email address of the new user
255         :param internetaddress: Home page of the new user
256         :param telephonenumber: Phone number of the new user
257         :param physicaldeliveryoffice: Office location of the new user
258         """
259
260         displayname = "";
261         if givenname is not None:
262             displayname += givenname
263
264         if initials is not None:
265             displayname += ' %s.' % initials
266
267         if surname is not None:
268             displayname += ' %s' % surname
269
270         cn = username
271         if useusernameascn is None and displayname is not "":
272             cn = displayname
273
274         user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn())
275
276         # The new user record. Note the reliance on the SAMLDB module which
277         # fills in the default informations
278         ldbmessage = {"dn": user_dn,
279             "sAMAccountName": username,
280             "objectClass": "user"}
281
282         if surname is not None:
283             ldbmessage["sn"] = surname
284
285         if givenname is not None:
286             ldbmessage["givenName"] = givenname
287
288         if displayname is not "":
289             ldbmessage["displayName"] = displayname
290             ldbmessage["name"] = displayname
291
292         if initials is not None:
293             ldbmessage["initials"] = '%s.' % initials
294
295         if profilepath is not None:
296             ldbmessage["profilePath"] = profilepath
297
298         if scriptpath is not None:
299             ldbmessage["scriptPath"] = scriptpath
300
301         if homedrive is not None:
302             ldbmessage["homeDrive"] = homedrive
303
304         if homedirectory is not None:
305             ldbmessage["homeDirectory"] = homedirectory
306
307         if jobtitle is not None:
308             ldbmessage["title"] = jobtitle
309
310         if department is not None:
311             ldbmessage["department"] = department
312
313         if company is not None:
314             ldbmessage["company"] = company
315
316         if description is not None:
317             ldbmessage["description"] = description
318
319         if mailaddress is not None:
320             ldbmessage["mail"] = mailaddress
321
322         if internetaddress is not None:
323             ldbmessage["wWWHomePage"] = internetaddress
324
325         if telephonenumber is not None:
326             ldbmessage["telephoneNumber"] = telephonenumber
327
328         if physicaldeliveryoffice is not None:
329             ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
330
331         self.transaction_start()
332         try:
333             self.add(ldbmessage)
334
335             # Sets the password for it
336             self.setpassword("(dn=" + user_dn + ")", password,
337               force_password_change_at_next_login_req)
338
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         Note: This call uses the "userPassword" attribute to set the password.
351         This works correctly on SAMBA 4 and on Windows DCs with
352         "2003 Native" or higer domain function level.
353
354         :param filter: LDAP filter to find the user (eg samccountname=name)
355         :param password: Password for the user
356         :param force_change_at_next_login: Force password change
357         """
358         self.transaction_start()
359         try:
360             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
361                               expression=filter, attrs=[])
362             if len(res) == 0:
363                 raise Exception('Unable to find user "%s"' % (username or filter))
364             assert(len(res) == 1)
365             user_dn = res[0].dn
366
367             setpw = """
368 dn: %s
369 changetype: modify
370 replace: userPassword
371 userPassword:: %s
372 """ % (user_dn, base64.b64encode(password))
373
374             self.modify_ldif(setpw)
375
376             if force_change_at_next_login:
377                 self.force_password_change_at_next_login(
378                   "(dn=" + str(user_dn) + ")")
379
380             #  modify the userAccountControl to remove the disabled bit
381             self.enable_account(filter)
382         except:
383             self.transaction_cancel()
384             raise
385         else:
386             self.transaction_commit()
387
388     def setexpiry(self, filter, expiry_seconds, no_expiry_req=False):
389         """Sets the account expiry for a user
390         
391         :param filter: LDAP filter to find the user (eg samccountname=name)
392         :param expiry_seconds: expiry time from now in seconds
393         :param no_expiry_req: if set, then don't expire password
394         """
395         self.transaction_start()
396         try:
397             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
398                           expression=filter,
399                           attrs=["userAccountControl", "accountExpires"])
400             assert(len(res) == 1)
401             user_dn = res[0].dn
402
403             userAccountControl = int(res[0]["userAccountControl"][0])
404             accountExpires     = int(res[0]["accountExpires"][0])
405             if no_expiry_req:
406                 userAccountControl = userAccountControl | 0x10000
407                 accountExpires = 0
408             else:
409                 userAccountControl = userAccountControl & ~0x10000
410                 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
411
412             setexp = """
413 dn: %s
414 changetype: modify
415 replace: userAccountControl
416 userAccountControl: %u
417 replace: accountExpires
418 accountExpires: %u
419 """ % (user_dn, userAccountControl, accountExpires)
420
421             self.modify_ldif(setexp)
422         except:
423             self.transaction_cancel()
424             raise
425         else:
426             self.transaction_commit()
427
428     def set_domain_sid(self, sid):
429         """Change the domain SID used by this LDB.
430
431         :param sid: The new domain sid to use.
432         """
433         dsdb.samdb_set_domain_sid(self, sid)
434
435     def get_domain_sid(self):
436         """Read the domain SID used by this LDB.
437
438         """
439         dsdb.samdb_get_domain_sid(self)
440
441     def set_invocation_id(self, invocation_id):
442         """Set the invocation id for this SamDB handle.
443
444         :param invocation_id: GUID of the invocation id.
445         """
446         dsdb.dsdb_set_ntds_invocation_id(self, invocation_id)
447
448     def get_oid_from_attid(self, attid):
449         return dsdb.dsdb_get_oid_from_attid(self, attid)
450
451     def get_invocation_id(self):
452         "Get the invocation_id id"
453         return dsdb.samdb_ntds_invocation_id(self)
454
455     def set_ntds_settings_dn(self, ntds_settings_dn):
456         """Set the NTDS Settings DN, as would be returned on the dsServiceName rootDSE attribute
457
458         This allows the DN to be set before the database fully exists
459
460         :param ntds_settings_dn: The new DN to use
461         """
462         dsdb.samdb_set_ntds_settings_dn(self, ntds_settings_dn)
463
464     invocation_id = property(get_invocation_id, set_invocation_id)
465
466     domain_sid = property(get_domain_sid, set_domain_sid)
467
468     def get_ntds_GUID(self):
469         "Get the NTDS objectGUID"
470         return dsdb.samdb_ntds_objectGUID(self)
471
472     def server_site_name(self):
473         "Get the server site name"
474         return dsdb.samdb_server_site_name(self)
475
476     def load_partition_usn(self, base_dn):
477         return dsdb.dsdb_load_partition_usn(self, base_dn)
478
479     def set_schema(self, schema):
480         self.set_schema_from_ldb(schema.ldb)
481
482     def set_schema_from_ldb(self, ldb):
483         dsdb.dsdb_set_schema_from_ldb(self, ldb)
484
485     def write_prefixes_from_schema(self):
486         dsdb.dsdb_write_prefixes_from_schema_to_ldb(self)