samdb: Fix formatting, move get_oid_from_attid from Ldb to SamDB.
[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                 print('Unable to find group "%s"' % (groupname or expression))
166                 raise
167             assert(len(targetgroup) == 1)
168
169             self.delete (targetgroup[0].dn);
170
171         except:
172             self.transaction_cancel()
173             raise
174         else:
175             self.transaction_commit()
176
177     def add_remove_group_members (self, groupname, listofmembers,
178                                   add_members_operation=True):
179         """Adds or removes group members
180
181         :param groupname: Name of the target group
182         :param listofmembers: Comma-separated list of group members
183         :param add_members_operation: Defines if its an add or remove operation
184         """
185
186         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
187         groupmembers = listofmembers.split(',')
188
189         self.transaction_start()
190         try:
191             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
192                                expression=groupfilter, attrs=['member'])
193             if len(targetgroup) == 0:
194                 print('Unable to find group "%s"' % (groupname or expression))
195                 raise
196             assert(len(targetgroup) == 1)
197
198             modified = False
199
200             addtargettogroup = """
201 dn: %s
202 changetype: modify
203 """ % (str(targetgroup[0].dn))
204
205             for member in groupmembers:
206                 targetmember = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
207                                     expression="(|(sAMAccountName=%s)(CN=%s))" % (member, member), attrs=[])
208
209                 if len(targetmember) != 1:
210                    continue
211
212                 if add_members_operation is True and (targetgroup[0].get('member') is None or str(targetmember[0].dn) not in targetgroup[0]['member']):
213                    modified = True
214                    addtargettogroup += """add: member
215 member: %s
216 """ % (str(targetmember[0].dn))
217
218                 elif add_members_operation is False and (targetgroup[0].get('member') is not None and str(targetmember[0].dn) in targetgroup[0]['member']):
219                    modified = True
220                    addtargettogroup += """delete: member
221 member: %s
222 """ % (str(targetmember[0].dn))
223
224             if modified is True:
225                self.modify_ldif(addtargettogroup)
226
227         except:
228             self.transaction_cancel()
229             raise
230         else:
231             self.transaction_commit()
232
233     def newuser(self, username, password,
234                 force_password_change_at_next_login_req=False,
235                 useusernameascn=False, userou=None, surname=None, givenname=None, initials=None,
236                 profilepath=None, scriptpath=None, homedrive=None, homedirectory=None,
237                 jobtitle=None, department=None, company=None, description=None,
238                 mailaddress=None, internetaddress=None, telephonenumber=None,
239                 physicaldeliveryoffice=None):
240         """Adds a new user with additional parameters
241
242         :param username: Name of the new user
243         :param password: Password for the new user
244         :param force_password_change_at_next_login_req: Force password change
245         :param useusernameascn: Use username as cn rather that firstname + initials + lastname
246         :param userou: Object container (without domainDN postfix) for new user
247         :param surname: Surname of the new user
248         :param givenname: First name of the new user
249         :param initials: Initials of the new user
250         :param profilepath: Profile path of the new user
251         :param scriptpath: Logon script path of the new user
252         :param homedrive: Home drive of the new user
253         :param homedirectory: Home directory of the new user
254         :param jobtitle: Job title of the new user
255         :param department: Department of the new user
256         :param company: Company of the new user
257         :param description: of the new user
258         :param mailaddress: Email address of the new user
259         :param internetaddress: Home page of the new user
260         :param telephonenumber: Phone number of the new user
261         :param physicaldeliveryoffice: Office location of the new user
262         """
263
264         displayname = "";
265         if givenname is not None:
266             displayname += givenname
267
268         if initials is not None:
269             displayname += ' %s.' % initials
270
271         if surname is not None:
272             displayname += ' %s' % surname
273
274         cn = username
275         if useusernameascn is None and displayname is not "":
276             cn = displayname
277
278         user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn())
279
280         # The new user record. Note the reliance on the SAMLDB module which
281         # fills in the default informations
282         ldbmessage = {"dn": user_dn,
283             "sAMAccountName": username,
284             "objectClass": "user"}
285
286         if surname is not None:
287             ldbmessage["sn"] = surname
288
289         if givenname is not None:
290             ldbmessage["givenName"] = givenname
291
292         if displayname is not "":
293             ldbmessage["displayName"] = displayname
294             ldbmessage["name"] = displayname
295
296         if initials is not None:
297             ldbmessage["initials"] = '%s.' % initials
298
299         if profilepath is not None:
300             ldbmessage["profilePath"] = profilepath
301
302         if scriptpath is not None:
303             ldbmessage["scriptPath"] = scriptpath
304
305         if homedrive is not None:
306             ldbmessage["homeDrive"] = homedrive
307
308         if homedirectory is not None:
309             ldbmessage["homeDirectory"] = homedirectory
310
311         if jobtitle is not None:
312             ldbmessage["title"] = jobtitle
313
314         if department is not None:
315             ldbmessage["department"] = department
316
317         if company is not None:
318             ldbmessage["company"] = company
319
320         if description is not None:
321             ldbmessage["description"] = description
322
323         if mailaddress is not None:
324             ldbmessage["mail"] = mailaddress
325
326         if internetaddress is not None:
327             ldbmessage["wWWHomePage"] = internetaddress
328
329         if telephonenumber is not None:
330             ldbmessage["telephoneNumber"] = telephonenumber
331
332         if physicaldeliveryoffice is not None:
333             ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
334
335         self.transaction_start()
336         try:
337             self.add(ldbmessage)
338
339             # Sets the password for it
340             self.setpassword("(dn=" + user_dn + ")", password,
341               force_password_change_at_next_login_req)
342
343         except:
344             self.transaction_cancel()
345             raise
346         else:
347             self.transaction_commit()
348
349     def setpassword(self, filter, password,
350                     force_change_at_next_login=False,
351                     username=None):
352         """Sets the password for a user
353         
354         Note: This call uses the "userPassword" attribute to set the password.
355         This works correctly on SAMBA 4 and on Windows DCs with
356         "2003 Native" or higer domain function level.
357
358         :param filter: LDAP filter to find the user (eg samccountname=name)
359         :param password: Password for the user
360         :param force_change_at_next_login: Force password change
361         """
362         self.transaction_start()
363         try:
364             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
365                               expression=filter, attrs=[])
366             if len(res) == 0:
367                 print('Unable to find user "%s"' % (username or filter))
368                 raise
369             assert(len(res) == 1)
370             user_dn = res[0].dn
371
372             setpw = """
373 dn: %s
374 changetype: modify
375 replace: userPassword
376 userPassword:: %s
377 """ % (user_dn, base64.b64encode(password))
378
379             self.modify_ldif(setpw)
380
381             if force_change_at_next_login:
382                 self.force_password_change_at_next_login(
383                   "(dn=" + str(user_dn) + ")")
384
385             #  modify the userAccountControl to remove the disabled bit
386             self.enable_account(filter)
387         except:
388             self.transaction_cancel()
389             raise
390         else:
391             self.transaction_commit()
392
393     def setexpiry(self, filter, expiry_seconds, no_expiry_req=False):
394         """Sets the account expiry for a user
395         
396         :param filter: LDAP filter to find the user (eg samccountname=name)
397         :param expiry_seconds: expiry time from now in seconds
398         :param no_expiry_req: if set, then don't expire password
399         """
400         self.transaction_start()
401         try:
402             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
403                           expression=filter,
404                           attrs=["userAccountControl", "accountExpires"])
405             assert(len(res) == 1)
406             user_dn = res[0].dn
407
408             userAccountControl = int(res[0]["userAccountControl"][0])
409             accountExpires     = int(res[0]["accountExpires"][0])
410             if no_expiry_req:
411                 userAccountControl = userAccountControl | 0x10000
412                 accountExpires = 0
413             else:
414                 userAccountControl = userAccountControl & ~0x10000
415                 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
416
417             setexp = """
418 dn: %s
419 changetype: modify
420 replace: userAccountControl
421 userAccountControl: %u
422 replace: accountExpires
423 accountExpires: %u
424 """ % (user_dn, userAccountControl, accountExpires)
425
426             self.modify_ldif(setexp)
427         except:
428             self.transaction_cancel()
429             raise
430         else:
431             self.transaction_commit()
432
433     def set_domain_sid(self, sid):
434         """Change the domain SID used by this LDB.
435
436         :param sid: The new domain sid to use.
437         """
438         dsdb.samdb_set_domain_sid(self, sid)
439
440     def get_domain_sid(self):
441         """Read the domain SID used by this LDB.
442
443         """
444         dsdb.samdb_get_domain_sid(self)
445
446     def set_invocation_id(self, invocation_id):
447         """Set the invocation id for this SamDB handle.
448
449         :param invocation_id: GUID of the invocation id.
450         """
451         dsdb.dsdb_set_ntds_invocation_id(self, invocation_id)
452
453     def get_oid_from_attid(self, attid):
454         return dsdb.dsdb_get_oid_from_attid(self, attid)
455
456     def get_invocation_id(self):
457         "Get the invocation_id id"
458         return dsdb.samdb_ntds_invocation_id(self)
459
460     def set_ntds_settings_dn(self, ntds_settings_dn):
461         """Set the NTDS Settings DN, as would be returned on the dsServiceName rootDSE attribute
462
463         This allows the DN to be set before the database fully exists
464
465         :param ntds_settings_dn: The new DN to use
466         """
467         dsdb.samdb_set_ntds_settings_dn(self, ntds_settings_dn)
468
469     invocation_id = property(get_invocation_id, set_invocation_id)
470
471     domain_sid = property(get_domain_sid, set_domain_sid)
472
473     def get_ntds_GUID(self):
474         "Get the NTDS objectGUID"
475         return dsdb.samdb_ntds_objectGUID(self)
476
477     def server_site_name(self):
478         "Get the server site name"
479         return dsdb.samdb_server_site_name(self)
480
481     def load_partition_usn(self, base_dn):
482         return dsdb.dsdb_load_partition_usn(self, base_dn)