s4-dsdb: Extended samdb.newuser to accept security descriptor for the object and...
[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 samba
27 import ldb
28 import time
29 import base64
30 from samba import dsdb
31 from samba.ndr import ndr_unpack, ndr_pack
32 from samba.dcerpc import drsblobs, misc, security
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         return str(self.get_default_basedn())
72
73     def enable_account(self, search_filter):
74         """Enables an account
75         
76         :param search_filter: LDAP filter to find the user (eg samccountname=name)
77         """
78         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
79                           expression=search_filter, attrs=["userAccountControl"])
80         assert(len(res) == 1)
81         user_dn = res[0].dn
82
83         userAccountControl = int(res[0]["userAccountControl"][0])
84         if (userAccountControl & 0x2):
85             userAccountControl = userAccountControl & ~0x2 # remove disabled bit
86         if (userAccountControl & 0x20):
87             userAccountControl = userAccountControl & ~0x20 # remove 'no password required' bit
88
89         mod = """
90 dn: %s
91 changetype: modify
92 replace: userAccountControl
93 userAccountControl: %u
94 """ % (user_dn, userAccountControl)
95         self.modify_ldif(mod)
96         
97     def force_password_change_at_next_login(self, search_filter):
98         """Forces a password change at next login
99         
100         :param search_filter: LDAP filter to find the user (eg samccountname=name)
101         """
102         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
103                           expression=search_filter, attrs=[])
104         assert(len(res) == 1)
105         user_dn = res[0].dn
106
107         mod = """
108 dn: %s
109 changetype: modify
110 replace: pwdLastSet
111 pwdLastSet: 0
112 """ % (user_dn)
113         self.modify_ldif(mod)
114
115     def newgroup(self, groupname, groupou=None, grouptype=None,
116                  description=None, mailaddress=None, notes=None):
117         """Adds a new group with additional parameters
118
119         :param groupname: Name of the new group
120         :param grouptype: Type of the new group
121         :param description: Description of the new group
122         :param mailaddress: Email address of the new group
123         :param notes: Notes of the new group
124         """
125
126         group_dn = "CN=%s,%s,%s" % (groupname, (groupou or "CN=Users"), self.domain_dn())
127
128         # The new user record. Note the reliance on the SAMLDB module which
129         # fills in the default informations
130         ldbmessage = {"dn": group_dn,
131             "sAMAccountName": groupname,
132             "objectClass": "group"}
133
134         if grouptype is not None:
135             ldbmessage["groupType"] = "%d" % grouptype
136
137         if description is not None:
138             ldbmessage["description"] = description
139
140         if mailaddress is not None:
141             ldbmessage["mail"] = mailaddress
142
143         if notes is not None:
144             ldbmessage["info"] = notes
145
146         self.add(ldbmessage)
147
148     def deletegroup(self, groupname):
149         """Deletes a group
150
151         :param groupname: Name of the target group
152         """
153
154         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
155         self.transaction_start()
156         try:
157             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
158                                expression=groupfilter, attrs=[])
159             if len(targetgroup) == 0:
160                 raise Exception('Unable to find group "%s"' % groupname)
161             assert(len(targetgroup) == 1)
162             self.delete(targetgroup[0].dn)
163         except:
164             self.transaction_cancel()
165             raise
166         else:
167             self.transaction_commit()
168
169     def add_remove_group_members(self, groupname, listofmembers,
170                                   add_members_operation=True):
171         """Adds or removes group members
172
173         :param groupname: Name of the target group
174         :param listofmembers: Comma-separated list of group members
175         :param add_members_operation: Defines if its an add or remove operation
176         """
177
178         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname, "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
179         groupmembers = listofmembers.split(',')
180
181         self.transaction_start()
182         try:
183             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
184                                expression=groupfilter, attrs=['member'])
185             if len(targetgroup) == 0:
186                 raise Exception('Unable to find group "%s"' % groupname)
187             assert(len(targetgroup) == 1)
188
189             modified = False
190
191             addtargettogroup = """
192 dn: %s
193 changetype: modify
194 """ % (str(targetgroup[0].dn))
195
196             for member in groupmembers:
197                 targetmember = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
198                                     expression="(|(sAMAccountName=%s)(CN=%s))" % (member, member), attrs=[])
199
200                 if len(targetmember) != 1:
201                     continue
202
203                 if add_members_operation is True and (targetgroup[0].get('member') is None or str(targetmember[0].dn) not in targetgroup[0]['member']):
204                     modified = True
205                     addtargettogroup += """add: member
206 member: %s
207 """ % (str(targetmember[0].dn))
208
209                 elif add_members_operation is False and (targetgroup[0].get('member') is not None and str(targetmember[0].dn) in targetgroup[0]['member']):
210                     modified = True
211                     addtargettogroup += """delete: member
212 member: %s
213 """ % (str(targetmember[0].dn))
214
215             if modified is True:
216                 self.modify_ldif(addtargettogroup)
217
218         except:
219             self.transaction_cancel()
220             raise
221         else:
222             self.transaction_commit()
223
224     def newuser(self, username, password,
225                 force_password_change_at_next_login_req=False,
226                 useusernameascn=False, userou=None, surname=None, givenname=None, initials=None,
227                 profilepath=None, scriptpath=None, homedrive=None, homedirectory=None,
228                 jobtitle=None, department=None, company=None, description=None,
229                 mailaddress=None, internetaddress=None, telephonenumber=None,
230                 physicaldeliveryoffice=None, sd=None, setpassword=True):
231         """Adds a new user with additional parameters
232
233         :param username: Name of the new user
234         :param password: Password for the new user
235         :param force_password_change_at_next_login_req: Force password change
236         :param useusernameascn: Use username as cn rather that firstname + initials + lastname
237         :param userou: Object container (without domainDN postfix) for new user
238         :param surname: Surname of the new user
239         :param givenname: First name of the new user
240         :param initials: Initials of the new user
241         :param profilepath: Profile path of the new user
242         :param scriptpath: Logon script path of the new user
243         :param homedrive: Home drive of the new user
244         :param homedirectory: Home directory of the new user
245         :param jobtitle: Job title of the new user
246         :param department: Department of the new user
247         :param company: Company of the new user
248         :param description: of the new user
249         :param mailaddress: Email address of the new user
250         :param internetaddress: Home page of the new user
251         :param telephonenumber: Phone number of the new user
252         :param physicaldeliveryoffice: Office location of the new user
253         :param sd: security descriptor of the object
254         :param setpassword: optionally disable password reset
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         dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "")
274         user_principal_name = "%s@%s" % (username, dnsdomain)
275         # The new user record. Note the reliance on the SAMLDB module which
276         # fills in the default informations
277         ldbmessage = {"dn": user_dn,
278                       "sAMAccountName": username,
279                       "userPrincipalName": user_principal_name,
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         if sd is not None:
332             ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
333
334         self.transaction_start()
335         try:
336             self.add(ldbmessage)
337
338             # Sets the password for it
339             if setpassword:
340                 self.setpassword("(dn=" + user_dn + ")", password,
341                                  force_password_change_at_next_login_req)
342         except:
343             self.transaction_cancel()
344             raise
345         else:
346             self.transaction_commit()
347
348     def setpassword(self, search_filter, password,
349                     force_change_at_next_login=False,
350                     username=None):
351         """Sets the password for a user
352         
353         :param search_filter: LDAP filter to find the user (eg samccountname=name)
354         :param password: Password for the user
355         :param force_change_at_next_login: Force password change
356         """
357         self.transaction_start()
358         try:
359             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
360                               expression=search_filter, attrs=[])
361             if len(res) == 0:
362                 raise Exception('Unable to find user "%s"' % (username or search_filter))
363             assert(len(res) == 1)
364             user_dn = res[0].dn
365
366             setpw = """
367 dn: %s
368 changetype: modify
369 replace: unicodePwd
370 unicodePwd:: %s
371 """ % (user_dn, base64.b64encode(("\"" + password + "\"").encode('utf-16-le')))
372
373             self.modify_ldif(setpw)
374
375             if force_change_at_next_login:
376                 self.force_password_change_at_next_login(
377                   "(dn=" + str(user_dn) + ")")
378
379             #  modify the userAccountControl to remove the disabled bit
380             self.enable_account(search_filter)
381         except:
382             self.transaction_cancel()
383             raise
384         else:
385             self.transaction_commit()
386
387     def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False):
388         """Sets the account expiry for a user
389         
390         :param search_filter: LDAP filter to find the user (eg samccountname=name)
391         :param expiry_seconds: expiry time from now in seconds
392         :param no_expiry_req: if set, then don't expire password
393         """
394         self.transaction_start()
395         try:
396             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
397                           expression=search_filter,
398                           attrs=["userAccountControl", "accountExpires"])
399             assert(len(res) == 1)
400             user_dn = res[0].dn
401
402             userAccountControl = int(res[0]["userAccountControl"][0])
403             accountExpires     = int(res[0]["accountExpires"][0])
404             if no_expiry_req:
405                 userAccountControl = userAccountControl | 0x10000
406                 accountExpires = 0
407             else:
408                 userAccountControl = userAccountControl & ~0x10000
409                 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
410
411             setexp = """
412 dn: %s
413 changetype: modify
414 replace: userAccountControl
415 userAccountControl: %u
416 replace: accountExpires
417 accountExpires: %u
418 """ % (user_dn, userAccountControl, accountExpires)
419
420             self.modify_ldif(setexp)
421         except:
422             self.transaction_cancel()
423             raise
424         else:
425             self.transaction_commit()
426
427     def set_domain_sid(self, sid):
428         """Change the domain SID used by this LDB.
429
430         :param sid: The new domain sid to use.
431         """
432         dsdb._samdb_set_domain_sid(self, sid)
433
434     def get_domain_sid(self):
435         """Read the domain SID used by this LDB.
436
437         """
438         return dsdb._samdb_get_domain_sid(self)
439
440     def set_invocation_id(self, invocation_id):
441         """Set the invocation id for this SamDB handle.
442
443         :param invocation_id: GUID of the invocation id.
444         """
445         dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
446
447     def get_oid_from_attid(self, attid):
448         return dsdb._dsdb_get_oid_from_attid(self, attid)
449
450     def get_attid_from_lDAPDisplayName(self, ldap_display_name, is_schema_nc=False):
451         return dsdb._dsdb_get_attid_from_lDAPDisplayName(self, ldap_display_name, is_schema_nc)
452
453     def get_invocation_id(self):
454         "Get the invocation_id id"
455         return dsdb._samdb_ntds_invocation_id(self)
456
457     def set_ntds_settings_dn(self, ntds_settings_dn):
458         """Set the NTDS Settings DN, as would be returned on the dsServiceName rootDSE attribute
459
460         This allows the DN to be set before the database fully exists
461
462         :param ntds_settings_dn: The new DN to use
463         """
464         dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
465
466     invocation_id = property(get_invocation_id, set_invocation_id)
467
468     domain_sid = property(get_domain_sid, set_domain_sid)
469
470     def get_ntds_GUID(self):
471         "Get the NTDS objectGUID"
472         return dsdb._samdb_ntds_objectGUID(self)
473
474     def server_site_name(self):
475         "Get the server site name"
476         return dsdb._samdb_server_site_name(self)
477
478     def load_partition_usn(self, base_dn):
479         return dsdb._dsdb_load_partition_usn(self, base_dn)
480
481     def set_schema(self, schema):
482         self.set_schema_from_ldb(schema.ldb)
483
484     def set_schema_from_ldb(self, ldb_conn):
485         dsdb._dsdb_set_schema_from_ldb(self, ldb_conn)
486
487     def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
488         return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
489
490     def get_attribute_from_attid(self, attid):
491         """ Get from an attid the associated attribute
492
493            :param attid: The attribute id for searched attribute
494            :return: The name of the attribute associated with this id
495         """
496         if len(self.hash_oid_name.keys()) == 0:
497             self._populate_oid_attid()
498         if self.hash_oid_name.has_key(self.get_oid_from_attid(attid)):
499             return self.hash_oid_name[self.get_oid_from_attid(attid)]
500         else:
501             return None
502
503
504     def _populate_oid_attid(self):
505         """Populate the hash hash_oid_name
506
507            This hash contains the oid of the attribute as a key and
508            its display name as a value
509         """
510         self.hash_oid_name = {}
511         res = self.search(expression="objectClass=attributeSchema",
512                            controls=["search_options:1:2"],
513                            attrs=["attributeID",
514                            "lDAPDisplayName"])
515         if len(res) > 0:
516             for e in res:
517                 strDisplay = str(e.get("lDAPDisplayName"))
518                 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
519
520
521     def get_attribute_replmetadata_version(self, dn, att):
522         """ Get the version field trom the replPropertyMetaData for
523             the given field
524
525            :param dn: The on which we want to get the version
526            :param att: The name of the attribute
527            :return: The value of the version field in the replPropertyMetaData
528              for the given attribute. None if the attribute is not replicated
529         """
530
531         res = self.search(expression="dn=%s" % dn,
532                             scope=ldb.SCOPE_SUBTREE,
533                             controls=["search_options:1:2"],
534                             attrs=["replPropertyMetaData"])
535         if len(res) == 0:
536             return None
537
538         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
539                             str(res[0]["replPropertyMetaData"]))
540         ctr = repl.ctr
541         if len(self.hash_oid_name.keys()) == 0:
542             self._populate_oid_attid()
543         for o in ctr.array:
544             # Search for Description
545             att_oid = self.get_oid_from_attid(o.attid)
546             if self.hash_oid_name.has_key(att_oid) and\
547                att.lower() == self.hash_oid_name[att_oid].lower():
548                 return o.version
549         return None
550
551
552     def set_attribute_replmetadata_version(self, dn, att, value, addifnotexist=False):
553         res = self.search(expression="dn=%s" % dn,
554                             scope=ldb.SCOPE_SUBTREE,
555                             controls=["search_options:1:2"],
556                             attrs=["replPropertyMetaData"])
557         if len(res) == 0:
558             return None
559
560         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
561                             str(res[0]["replPropertyMetaData"]))
562         ctr = repl.ctr
563         now = samba.unix2nttime(int(time.time()))
564         found = False
565         if len(self.hash_oid_name.keys()) == 0:
566             self._populate_oid_attid()
567         for o in ctr.array:
568             # Search for Description
569             att_oid = self.get_oid_from_attid(o.attid)
570             if self.hash_oid_name.has_key(att_oid) and\
571                att.lower() == self.hash_oid_name[att_oid].lower():
572                 found = True
573                 seq = self.sequence_number(ldb.SEQ_NEXT)
574                 o.version = value
575                 o.originating_change_time = now
576                 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
577                 o.originating_usn = seq
578                 o.local_usn = seq
579
580         if not found and addifnotexist and len(ctr.array) >0:
581             o2 = drsblobs.replPropertyMetaData1()
582             o2.attid = 589914
583             att_oid = self.get_oid_from_attid(o2.attid)
584             seq = self.sequence_number(ldb.SEQ_NEXT)
585             o2.version = value
586             o2.originating_change_time = now
587             o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
588             o2.originating_usn = seq
589             o2.local_usn = seq
590             found = True
591             tab = ctr.array
592             tab.append(o2)
593             ctr.count = ctr.count + 1
594             ctr.array = tab
595
596         if found :
597             replBlob = ndr_pack(repl)
598             msg = ldb.Message()
599             msg.dn = res[0].dn
600             msg["replPropertyMetaData"] = ldb.MessageElement(replBlob,
601                                                 ldb.FLAG_MOD_REPLACE,
602                                                 "replPropertyMetaData")
603             self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
604
605
606     def write_prefixes_from_schema(self):
607         dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
608
609     def get_partitions_dn(self):
610         return dsdb._dsdb_get_partitions_dn(self)
611
612     def set_minPwdAge(self, value):
613         m = ldb.Message()
614         m.dn = ldb.Dn(self, self.domain_dn())
615         m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
616         self.modify(m)
617
618     def get_minPwdAge(self):
619         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
620         if len(res) == 0:
621             return None
622         elif not "minPwdAge" in res[0]:
623             return None
624         else:
625             return res[0]["minPwdAge"][0]
626
627     def set_dsheuristics(self, dsheuristics):
628         m = ldb.Message()
629         m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
630                       % self.get_config_basedn().get_linearized())
631         if dsheuristics is not None:
632             m["dSHeuristics"] = ldb.MessageElement(dsheuristics, ldb.FLAG_MOD_REPLACE,
633                                                "dSHeuristics")
634         else:
635             m["dSHeuristics"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "dSHeuristics")
636         self.modify(m)
637
638     def get_dsheuristics(self):
639         res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
640                           % self.get_config_basedn().get_linearized(),
641                           scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
642         if len(res) == 0:
643             dsheuristics = None
644         elif "dSHeuristics" in res[0]:
645             dsheuristics = res[0]["dSHeuristics"][0]
646         else:
647             dsheuristics = None
648
649         return dsheuristics
650
651     def create_ou(self, ou_dn, description=None, name=None, sd=None):
652         """Creates an organizationalUnit object
653         :param ou_dn: dn of the new object
654         :param description: description attribute
655         :param name: name atttribute
656         :param sd: security descriptor of the object, can be
657         an SDDL string or security.descriptor type
658         """
659         m = ldb.Message()
660         m.dn = ldb.Dn(self, ou_dn)
661         m["ou"] = ou_dn.split(",")[0][3:]
662         m["objectClass"] = "organizationalUnit"
663
664         if description:
665              m["description"] = description
666         if name:
667              m["description"] = name
668
669         if sd:
670             assert(isinstance(sd, str) or isinstance(sd, security.descriptor))
671             if isinstance(sd, str):
672                 sid = security.dom_sid(self.get_domain_sid())
673                 tmp_desc = security.descriptor.from_sddl(sd, sid)
674                 m["nTSecurityDescriptor"] = ndr_pack(tmp_desc)
675             elif isinstance(sd, security.descriptor):
676                 m["nTSecurityDescriptor"] = ndr_pack(sd)
677         self.add(m)