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