234a9b9df304a684045201482615baf7b198beee
[kai/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 # Copyright (C) Giampaolo Lauria <lauria2@yahoo.com> 2011
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
23 #
24
25 """Convenience functions for using the SAM."""
26
27 import samba
28 import ldb
29 import time
30 import base64
31 import os
32 from samba import dsdb
33 from samba.ndr import ndr_unpack, ndr_pack
34 from samba.dcerpc import drsblobs, misc
35 from samba.common import normalise_int32
36
37 __docformat__ = "restructuredText"
38
39
40 class SamDB(samba.Ldb):
41     """The SAM database."""
42
43     hash_oid_name = {}
44
45     def __init__(self, url=None, lp=None, modules_dir=None, session_info=None,
46                  credentials=None, flags=0, options=None, global_schema=True,
47                  auto_connect=True, am_rodc=None):
48         self.lp = lp
49         if not auto_connect:
50             url = None
51         elif url is None and lp is not None:
52             url = lp.samdb_url()
53
54         self.url = url
55
56         super(SamDB, self).__init__(url=url, lp=lp, modules_dir=modules_dir,
57             session_info=session_info, credentials=credentials, flags=flags,
58             options=options)
59
60         if global_schema:
61             dsdb._dsdb_set_global_schema(self)
62
63         if am_rodc is not None:
64             dsdb._dsdb_set_am_rodc(self, am_rodc)
65
66     def connect(self, url=None, flags=0, options=None):
67         '''connect to the database'''
68         if self.lp is not None and not os.path.exists(url):
69             url = self.lp.private_path(url)
70         self.url = url
71
72         super(SamDB, self).connect(url=url, flags=flags,
73                 options=options)
74
75     def am_rodc(self):
76         '''return True if we are an RODC'''
77         return dsdb._am_rodc(self)
78
79     def domain_dn(self):
80         '''return the domain DN'''
81         return str(self.get_default_basedn())
82
83     def disable_account(self, search_filter):
84         """Disables an account
85
86         :param search_filter: LDAP filter to find the user (eg
87             samccountname=name)
88         """
89
90         flags = samba.dsdb.UF_ACCOUNTDISABLE
91         self.toggle_userAccountFlags(search_filter, flags, on=True)
92
93     def enable_account(self, search_filter):
94         """Enables an account
95
96         :param search_filter: LDAP filter to find the user (eg
97             samccountname=name)
98         """
99
100         flags = samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_PASSWD_NOTREQD
101         self.toggle_userAccountFlags(search_filter, flags, on=False)
102
103     def toggle_userAccountFlags(self, search_filter, flags, flags_str=None,
104                                 on=True, strict=False):
105         """toggle_userAccountFlags
106
107         :param search_filter: LDAP filter to find the user (eg
108             samccountname=name)
109         :flags: samba.dsdb.UF_* flags
110         :on: on=True (default) => set, on=False => unset
111         :strict: strict=False (default) ignore if no action is needed
112                  strict=True raises an Exception if...
113         """
114         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
115                           expression=search_filter, attrs=["userAccountControl"])
116         if len(res) == 0:
117                 raise Exception("Unable to find account where '%s'" % search_filter)
118         assert(len(res) == 1)
119         account_dn = res[0].dn
120
121         old_uac = int(res[0]["userAccountControl"][0])
122         if on:
123             if strict and (old_uac & flags):
124                 error = "Account flag(s) '%s' already set" % flags_str
125                 raise Exception(error)
126
127             new_uac = old_uac | flags
128         else:
129             if strict and not (old_uac & flags):
130                 error = "Account flag(s) '%s' already unset" % flags_str
131                 raise Exception(error)
132
133             new_uac = old_uac & ~flags
134
135         if old_uac == new_uac:
136             return
137
138         mod = """
139 dn: %s
140 changetype: modify
141 delete: userAccountControl
142 userAccountControl: %u
143 add: userAccountControl
144 userAccountControl: %u
145 """ % (account_dn, old_uac, new_uac)
146         self.modify_ldif(mod)
147
148     def force_password_change_at_next_login(self, search_filter):
149         """Forces a password change at next login
150
151         :param search_filter: LDAP filter to find the user (eg
152             samccountname=name)
153         """
154         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
155                           expression=search_filter, attrs=[])
156         if len(res) == 0:
157                 raise Exception('Unable to find user "%s"' % search_filter)
158         assert(len(res) == 1)
159         user_dn = res[0].dn
160
161         mod = """
162 dn: %s
163 changetype: modify
164 replace: pwdLastSet
165 pwdLastSet: 0
166 """ % (user_dn)
167         self.modify_ldif(mod)
168
169     def newgroup(self, groupname, groupou=None, grouptype=None,
170                  description=None, mailaddress=None, notes=None, sd=None):
171         """Adds a new group with additional parameters
172
173         :param groupname: Name of the new group
174         :param grouptype: Type of the new group
175         :param description: Description of the new group
176         :param mailaddress: Email address of the new group
177         :param notes: Notes of the new group
178         :param sd: security descriptor of the object
179         """
180
181         group_dn = "CN=%s,%s,%s" % (groupname, (groupou or "CN=Users"), self.domain_dn())
182
183         # The new user record. Note the reliance on the SAMLDB module which
184         # fills in the default informations
185         ldbmessage = {"dn": group_dn,
186             "sAMAccountName": groupname,
187             "objectClass": "group"}
188
189         if grouptype is not None:
190             ldbmessage["groupType"] = normalise_int32(grouptype)
191
192         if description is not None:
193             ldbmessage["description"] = description
194
195         if mailaddress is not None:
196             ldbmessage["mail"] = mailaddress
197
198         if notes is not None:
199             ldbmessage["info"] = notes
200
201         if sd is not None:
202             ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
203
204         self.add(ldbmessage)
205
206     def deletegroup(self, groupname):
207         """Deletes a group
208
209         :param groupname: Name of the target group
210         """
211
212         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
213         self.transaction_start()
214         try:
215             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
216                                expression=groupfilter, attrs=[])
217             if len(targetgroup) == 0:
218                 raise Exception('Unable to find group "%s"' % groupname)
219             assert(len(targetgroup) == 1)
220             self.delete(targetgroup[0].dn)
221         except Exception:
222             self.transaction_cancel()
223             raise
224         else:
225             self.transaction_commit()
226
227     def add_remove_group_members(self, groupname, listofmembers,
228                                   add_members_operation=True):
229         """Adds or removes group members
230
231         :param groupname: Name of the target group
232         :param listofmembers: Comma-separated list of group members
233         :param add_members_operation: Defines if its an add or remove
234             operation
235         """
236
237         groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (
238             ldb.binary_encode(groupname), "CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
239         groupmembers = listofmembers.split(',')
240
241         self.transaction_start()
242         try:
243             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
244                                expression=groupfilter, attrs=['member'])
245             if len(targetgroup) == 0:
246                 raise Exception('Unable to find group "%s"' % groupname)
247             assert(len(targetgroup) == 1)
248
249             modified = False
250
251             addtargettogroup = """
252 dn: %s
253 changetype: modify
254 """ % (str(targetgroup[0].dn))
255
256             for member in groupmembers:
257                 targetmember = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
258                                     expression="(|(sAMAccountName=%s)(CN=%s))" % (
259                     ldb.binary_encode(member), ldb.binary_encode(member)), attrs=[])
260
261                 if len(targetmember) != 1:
262                     continue
263
264                 if add_members_operation is True and (targetgroup[0].get('member') is None or str(targetmember[0].dn) not in targetgroup[0]['member']):
265                     modified = True
266                     addtargettogroup += """add: member
267 member: %s
268 """ % (str(targetmember[0].dn))
269
270                 elif add_members_operation is False and (targetgroup[0].get('member') is not None and str(targetmember[0].dn) in targetgroup[0]['member']):
271                     modified = True
272                     addtargettogroup += """delete: member
273 member: %s
274 """ % (str(targetmember[0].dn))
275
276             if modified is True:
277                 self.modify_ldif(addtargettogroup)
278
279         except Exception:
280             self.transaction_cancel()
281             raise
282         else:
283             self.transaction_commit()
284
285     def newuser(self, username, password,
286             force_password_change_at_next_login_req=False,
287             useusernameascn=False, userou=None, surname=None, givenname=None,
288             initials=None, profilepath=None, scriptpath=None, homedrive=None,
289             homedirectory=None, jobtitle=None, department=None, company=None,
290             description=None, mailaddress=None, internetaddress=None,
291             telephonenumber=None, physicaldeliveryoffice=None, sd=None,
292             setpassword=True):
293         """Adds a new user with additional parameters
294
295         :param username: Name of the new user
296         :param password: Password for the new user
297         :param force_password_change_at_next_login_req: Force password change
298         :param useusernameascn: Use username as cn rather that firstname +
299             initials + lastname
300         :param userou: Object container (without domainDN postfix) for new user
301         :param surname: Surname of the new user
302         :param givenname: First name of the new user
303         :param initials: Initials of the new user
304         :param profilepath: Profile path of the new user
305         :param scriptpath: Logon script path of the new user
306         :param homedrive: Home drive of the new user
307         :param homedirectory: Home directory of the new user
308         :param jobtitle: Job title of the new user
309         :param department: Department of the new user
310         :param company: Company of the new user
311         :param description: of the new user
312         :param mailaddress: Email address of the new user
313         :param internetaddress: Home page of the new user
314         :param telephonenumber: Phone number of the new user
315         :param physicaldeliveryoffice: Office location of the new user
316         :param sd: security descriptor of the object
317         :param setpassword: optionally disable password reset
318         """
319
320         displayname = ""
321         if givenname is not None:
322             displayname += givenname
323
324         if initials is not None:
325             displayname += ' %s.' % initials
326
327         if surname is not None:
328             displayname += ' %s' % surname
329
330         cn = username
331         if useusernameascn is None and displayname is not "":
332             cn = displayname
333
334         user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn())
335
336         dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "")
337         user_principal_name = "%s@%s" % (username, dnsdomain)
338         # The new user record. Note the reliance on the SAMLDB module which
339         # fills in the default informations
340         ldbmessage = {"dn": user_dn,
341                       "sAMAccountName": username,
342                       "userPrincipalName": user_principal_name,
343                       "objectClass": "user"}
344
345         if surname is not None:
346             ldbmessage["sn"] = surname
347
348         if givenname is not None:
349             ldbmessage["givenName"] = givenname
350
351         if displayname is not "":
352             ldbmessage["displayName"] = displayname
353             ldbmessage["name"] = displayname
354
355         if initials is not None:
356             ldbmessage["initials"] = '%s.' % initials
357
358         if profilepath is not None:
359             ldbmessage["profilePath"] = profilepath
360
361         if scriptpath is not None:
362             ldbmessage["scriptPath"] = scriptpath
363
364         if homedrive is not None:
365             ldbmessage["homeDrive"] = homedrive
366
367         if homedirectory is not None:
368             ldbmessage["homeDirectory"] = homedirectory
369
370         if jobtitle is not None:
371             ldbmessage["title"] = jobtitle
372
373         if department is not None:
374             ldbmessage["department"] = department
375
376         if company is not None:
377             ldbmessage["company"] = company
378
379         if description is not None:
380             ldbmessage["description"] = description
381
382         if mailaddress is not None:
383             ldbmessage["mail"] = mailaddress
384
385         if internetaddress is not None:
386             ldbmessage["wWWHomePage"] = internetaddress
387
388         if telephonenumber is not None:
389             ldbmessage["telephoneNumber"] = telephonenumber
390
391         if physicaldeliveryoffice is not None:
392             ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
393
394         if sd is not None:
395             ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
396
397         self.transaction_start()
398         try:
399             self.add(ldbmessage)
400
401             # Sets the password for it
402             if setpassword:
403                 self.setpassword("(samAccountName=%s)" % ldb.binary_encode(username), password,
404                                  force_password_change_at_next_login_req)
405         except Exception:
406             self.transaction_cancel()
407             raise
408         else:
409             self.transaction_commit()
410
411
412     def deleteuser(self, username):
413         """Deletes a user
414
415         :param username: Name of the target user
416         """
417
418         filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn())
419         self.transaction_start()
420         try:
421             target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
422                                  expression=filter, attrs=[])
423             if len(target) == 0:
424                 raise Exception('Unable to find user "%s"' % username)
425             assert(len(target) == 1)
426             self.delete(target[0].dn)
427         except Exception:
428             self.transaction_cancel()
429             raise
430         else:
431             self.transaction_commit()
432
433
434     def setpassword(self, search_filter, password,
435             force_change_at_next_login=False, username=None):
436         """Sets the password for a user
437
438         :param search_filter: LDAP filter to find the user (eg
439             samccountname=name)
440         :param password: Password for the user
441         :param force_change_at_next_login: Force password change
442         """
443         self.transaction_start()
444         try:
445             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
446                               expression=search_filter, attrs=[])
447             if len(res) == 0:
448                 raise Exception('Unable to find user "%s"' % (username or search_filter))
449             if len(res) > 1:
450                 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), search_filter))
451             user_dn = res[0].dn
452             setpw = """
453 dn: %s
454 changetype: modify
455 replace: unicodePwd
456 unicodePwd:: %s
457 """ % (user_dn, base64.b64encode(("\"" + password + "\"").encode('utf-16-le')))
458
459             self.modify_ldif(setpw)
460
461             if force_change_at_next_login:
462                 self.force_password_change_at_next_login(
463                   "(dn=" + str(user_dn) + ")")
464
465             #  modify the userAccountControl to remove the disabled bit
466             self.enable_account(search_filter)
467         except Exception:
468             self.transaction_cancel()
469             raise
470         else:
471             self.transaction_commit()
472
473     def setexpiry(self, search_filter, expiry_seconds, no_expiry_req=False):
474         """Sets the account expiry for a user
475
476         :param search_filter: LDAP filter to find the user (eg
477             samaccountname=name)
478         :param expiry_seconds: expiry time from now in seconds
479         :param no_expiry_req: if set, then don't expire password
480         """
481         self.transaction_start()
482         try:
483             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
484                           expression=search_filter,
485                           attrs=["userAccountControl", "accountExpires"])
486             if len(res) == 0:
487                 raise Exception('Unable to find user "%s"' % search_filter)
488             assert(len(res) == 1)
489             user_dn = res[0].dn
490
491             userAccountControl = int(res[0]["userAccountControl"][0])
492             accountExpires     = int(res[0]["accountExpires"][0])
493             if no_expiry_req:
494                 userAccountControl = userAccountControl | 0x10000
495                 accountExpires = 0
496             else:
497                 userAccountControl = userAccountControl & ~0x10000
498                 accountExpires = samba.unix2nttime(expiry_seconds + int(time.time()))
499
500             setexp = """
501 dn: %s
502 changetype: modify
503 replace: userAccountControl
504 userAccountControl: %u
505 replace: accountExpires
506 accountExpires: %u
507 """ % (user_dn, userAccountControl, accountExpires)
508
509             self.modify_ldif(setexp)
510         except Exception:
511             self.transaction_cancel()
512             raise
513         else:
514             self.transaction_commit()
515
516     def set_domain_sid(self, sid):
517         """Change the domain SID used by this LDB.
518
519         :param sid: The new domain sid to use.
520         """
521         dsdb._samdb_set_domain_sid(self, sid)
522
523     def get_domain_sid(self):
524         """Read the domain SID used by this LDB. """
525         return dsdb._samdb_get_domain_sid(self)
526
527     domain_sid = property(get_domain_sid, set_domain_sid,
528         "SID for the domain")
529
530     def set_invocation_id(self, invocation_id):
531         """Set the invocation id for this SamDB handle.
532
533         :param invocation_id: GUID of the invocation id.
534         """
535         dsdb._dsdb_set_ntds_invocation_id(self, invocation_id)
536
537     def get_invocation_id(self):
538         """Get the invocation_id id"""
539         return dsdb._samdb_ntds_invocation_id(self)
540
541     invocation_id = property(get_invocation_id, set_invocation_id,
542         "Invocation ID GUID")
543
544     def get_oid_from_attid(self, attid):
545         return dsdb._dsdb_get_oid_from_attid(self, attid)
546
547     def get_attid_from_lDAPDisplayName(self, ldap_display_name,
548             is_schema_nc=False):
549         '''return the attribute ID for a LDAP attribute as an integer as found in DRSUAPI'''
550         return dsdb._dsdb_get_attid_from_lDAPDisplayName(self,
551             ldap_display_name, is_schema_nc)
552
553     def get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name):
554         '''return the syntax OID for a LDAP attribute as a string'''
555         return dsdb._dsdb_get_syntax_oid_from_lDAPDisplayName(self, ldap_display_name)
556
557     def get_systemFlags_from_lDAPDisplayName(self, ldap_display_name):
558         '''return the systemFlags for a LDAP attribute as a integer'''
559         return dsdb._dsdb_get_systemFlags_from_lDAPDisplayName(self, ldap_display_name)
560
561     def get_linkId_from_lDAPDisplayName(self, ldap_display_name):
562         '''return the linkID for a LDAP attribute as a integer'''
563         return dsdb._dsdb_get_linkId_from_lDAPDisplayName(self, ldap_display_name)
564
565     def get_lDAPDisplayName_by_attid(self, attid):
566         '''return the lDAPDisplayName from an integer DRS attribute ID'''
567         return dsdb._dsdb_get_lDAPDisplayName_by_attid(self, attid)
568
569     def get_backlink_from_lDAPDisplayName(self, ldap_display_name):
570         '''return the attribute name of the corresponding backlink from the name
571         of a forward link attribute. If there is no backlink return None'''
572         return dsdb._dsdb_get_backlink_from_lDAPDisplayName(self, ldap_display_name)
573
574     def set_ntds_settings_dn(self, ntds_settings_dn):
575         """Set the NTDS Settings DN, as would be returned on the dsServiceName
576         rootDSE attribute.
577
578         This allows the DN to be set before the database fully exists
579
580         :param ntds_settings_dn: The new DN to use
581         """
582         dsdb._samdb_set_ntds_settings_dn(self, ntds_settings_dn)
583
584     def get_ntds_GUID(self):
585         """Get the NTDS objectGUID"""
586         return dsdb._samdb_ntds_objectGUID(self)
587
588     def server_site_name(self):
589         """Get the server site name"""
590         return dsdb._samdb_server_site_name(self)
591
592     def host_dns_name(self):
593         """return the DNS name of this host"""
594         res = self.search(base='', scope=ldb.SCOPE_BASE, attrs=['dNSHostName'])
595         return res[0]['dNSHostName'][0]
596
597     def domain_dns_name(self):
598         """return the DNS name of the domain root"""
599         domain_dn = self.get_default_basedn()
600         return domain_dn.canonical_str().split('/')[0]
601
602     def forest_dns_name(self):
603         """return the DNS name of the forest root"""
604         forest_dn = self.get_root_basedn()
605         return forest_dn.canonical_str().split('/')[0]
606
607     def load_partition_usn(self, base_dn):
608         return dsdb._dsdb_load_partition_usn(self, base_dn)
609
610     def set_schema(self, schema):
611         self.set_schema_from_ldb(schema.ldb)
612
613     def set_schema_from_ldb(self, ldb_conn):
614         dsdb._dsdb_set_schema_from_ldb(self, ldb_conn)
615
616     def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
617         '''convert a list of attribute values to a DRSUAPI DsReplicaAttribute'''
618         return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
619
620     def dsdb_normalise_attributes(self, ldb, ldap_display_name, ldif_elements):
621         '''normalise a list of attribute values'''
622         return dsdb._dsdb_normalise_attributes(ldb, ldap_display_name, ldif_elements)
623
624     def get_attribute_from_attid(self, attid):
625         """ Get from an attid the associated attribute
626
627         :param attid: The attribute id for searched attribute
628         :return: The name of the attribute associated with this id
629         """
630         if len(self.hash_oid_name.keys()) == 0:
631             self._populate_oid_attid()
632         if self.hash_oid_name.has_key(self.get_oid_from_attid(attid)):
633             return self.hash_oid_name[self.get_oid_from_attid(attid)]
634         else:
635             return None
636
637     def _populate_oid_attid(self):
638         """Populate the hash hash_oid_name.
639
640         This hash contains the oid of the attribute as a key and
641         its display name as a value
642         """
643         self.hash_oid_name = {}
644         res = self.search(expression="objectClass=attributeSchema",
645                            controls=["search_options:1:2"],
646                            attrs=["attributeID",
647                            "lDAPDisplayName"])
648         if len(res) > 0:
649             for e in res:
650                 strDisplay = str(e.get("lDAPDisplayName"))
651                 self.hash_oid_name[str(e.get("attributeID"))] = strDisplay
652
653     def get_attribute_replmetadata_version(self, dn, att):
654         """Get the version field trom the replPropertyMetaData for
655         the given field
656
657         :param dn: The on which we want to get the version
658         :param att: The name of the attribute
659         :return: The value of the version field in the replPropertyMetaData
660             for the given attribute. None if the attribute is not replicated
661         """
662
663         res = self.search(expression="dn=%s" % dn,
664                             scope=ldb.SCOPE_SUBTREE,
665                             controls=["search_options:1:2"],
666                             attrs=["replPropertyMetaData"])
667         if len(res) == 0:
668             return None
669
670         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
671                             str(res[0]["replPropertyMetaData"]))
672         ctr = repl.ctr
673         if len(self.hash_oid_name.keys()) == 0:
674             self._populate_oid_attid()
675         for o in ctr.array:
676             # Search for Description
677             att_oid = self.get_oid_from_attid(o.attid)
678             if self.hash_oid_name.has_key(att_oid) and\
679                att.lower() == self.hash_oid_name[att_oid].lower():
680                 return o.version
681         return None
682
683     def set_attribute_replmetadata_version(self, dn, att, value,
684             addifnotexist=False):
685         res = self.search(expression="dn=%s" % dn,
686                             scope=ldb.SCOPE_SUBTREE,
687                             controls=["search_options:1:2"],
688                             attrs=["replPropertyMetaData"])
689         if len(res) == 0:
690             return None
691
692         repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
693                             str(res[0]["replPropertyMetaData"]))
694         ctr = repl.ctr
695         now = samba.unix2nttime(int(time.time()))
696         found = False
697         if len(self.hash_oid_name.keys()) == 0:
698             self._populate_oid_attid()
699         for o in ctr.array:
700             # Search for Description
701             att_oid = self.get_oid_from_attid(o.attid)
702             if self.hash_oid_name.has_key(att_oid) and\
703                att.lower() == self.hash_oid_name[att_oid].lower():
704                 found = True
705                 seq = self.sequence_number(ldb.SEQ_NEXT)
706                 o.version = value
707                 o.originating_change_time = now
708                 o.originating_invocation_id = misc.GUID(self.get_invocation_id())
709                 o.originating_usn = seq
710                 o.local_usn = seq
711
712         if not found and addifnotexist and len(ctr.array) >0:
713             o2 = drsblobs.replPropertyMetaData1()
714             o2.attid = 589914
715             att_oid = self.get_oid_from_attid(o2.attid)
716             seq = self.sequence_number(ldb.SEQ_NEXT)
717             o2.version = value
718             o2.originating_change_time = now
719             o2.originating_invocation_id = misc.GUID(self.get_invocation_id())
720             o2.originating_usn = seq
721             o2.local_usn = seq
722             found = True
723             tab = ctr.array
724             tab.append(o2)
725             ctr.count = ctr.count + 1
726             ctr.array = tab
727
728         if found :
729             replBlob = ndr_pack(repl)
730             msg = ldb.Message()
731             msg.dn = res[0].dn
732             msg["replPropertyMetaData"] = ldb.MessageElement(replBlob,
733                                                 ldb.FLAG_MOD_REPLACE,
734                                                 "replPropertyMetaData")
735             self.modify(msg, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
736
737     def write_prefixes_from_schema(self):
738         dsdb._dsdb_write_prefixes_from_schema_to_ldb(self)
739
740     def get_partitions_dn(self):
741         return dsdb._dsdb_get_partitions_dn(self)
742
743     def set_minPwdAge(self, value):
744         m = ldb.Message()
745         m.dn = ldb.Dn(self, self.domain_dn())
746         m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
747         self.modify(m)
748
749     def get_minPwdAge(self):
750         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
751         if len(res) == 0:
752             return None
753         elif not "minPwdAge" in res[0]:
754             return None
755         else:
756             return res[0]["minPwdAge"][0]
757
758     def set_minPwdLength(self, value):
759         m = ldb.Message()
760         m.dn = ldb.Dn(self, self.domain_dn())
761         m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength")
762         self.modify(m)
763
764     def get_minPwdLength(self):
765         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"])
766         if len(res) == 0:
767             return None
768         elif not "minPwdLength" in res[0]:
769             return None
770         else:
771             return res[0]["minPwdLength"][0]
772
773     def set_pwdProperties(self, value):
774         m = ldb.Message()
775         m.dn = ldb.Dn(self, self.domain_dn())
776         m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties")
777         self.modify(m)
778
779     def get_pwdProperties(self):
780         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"])
781         if len(res) == 0:
782             return None
783         elif not "pwdProperties" in res[0]:
784             return None
785         else:
786             return res[0]["pwdProperties"][0]
787
788     def set_dsheuristics(self, dsheuristics):
789         m = ldb.Message()
790         m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
791                       % self.get_config_basedn().get_linearized())
792         if dsheuristics is not None:
793             m["dSHeuristics"] = ldb.MessageElement(dsheuristics,
794                 ldb.FLAG_MOD_REPLACE, "dSHeuristics")
795         else:
796             m["dSHeuristics"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
797                 "dSHeuristics")
798         self.modify(m)
799
800     def get_dsheuristics(self):
801         res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
802                           % self.get_config_basedn().get_linearized(),
803                           scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
804         if len(res) == 0:
805             dsheuristics = None
806         elif "dSHeuristics" in res[0]:
807             dsheuristics = res[0]["dSHeuristics"][0]
808         else:
809             dsheuristics = None
810
811         return dsheuristics
812
813     def create_ou(self, ou_dn, description=None, name=None, sd=None):
814         """Creates an organizationalUnit object
815         :param ou_dn: dn of the new object
816         :param description: description attribute
817         :param name: name atttribute
818         :param sd: security descriptor of the object, can be
819         an SDDL string or security.descriptor type
820         """
821         m = {"dn": ou_dn,
822              "objectClass": "organizationalUnit"}
823
824         if description:
825             m["description"] = description
826         if name:
827             m["name"] = name
828
829         if sd:
830             m["nTSecurityDescriptor"] = ndr_pack(sd)
831         self.add(m)