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