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