d83e0a6f7c8c5ebcd1869131b49b327026e9c613
[kai/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, members,
230                                   add_members_operation=True):
231         """Adds or removes group members
232
233         :param groupname: Name of the target group
234         :param members: 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
242         self.transaction_start()
243         try:
244             targetgroup = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
245                                expression=groupfilter, attrs=['member'])
246             if len(targetgroup) == 0:
247                 raise Exception('Unable to find group "%s"' % groupname)
248             assert(len(targetgroup) == 1)
249
250             modified = False
251
252             addtargettogroup = """
253 dn: %s
254 changetype: modify
255 """ % (str(targetgroup[0].dn))
256
257             for member in members:
258                 targetmember = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
259                                     expression="(|(sAMAccountName=%s)(CN=%s))" % (
260                     ldb.binary_encode(member), ldb.binary_encode(member)), attrs=[])
261
262                 if len(targetmember) != 1:
263                     continue
264
265                 if add_members_operation is True and (targetgroup[0].get('member') is None or str(targetmember[0].dn) not in targetgroup[0]['member']):
266                     modified = True
267                     addtargettogroup += """add: member
268 member: %s
269 """ % (str(targetmember[0].dn))
270
271                 elif add_members_operation is False and (targetgroup[0].get('member') is not None and str(targetmember[0].dn) in targetgroup[0]['member']):
272                     modified = True
273                     addtargettogroup += """delete: member
274 member: %s
275 """ % (str(targetmember[0].dn))
276
277             if modified is True:
278                 self.modify_ldif(addtargettogroup)
279
280         except:
281             self.transaction_cancel()
282             raise
283         else:
284             self.transaction_commit()
285
286     def newuser(self, username, password,
287             force_password_change_at_next_login_req=False,
288             useusernameascn=False, userou=None, surname=None, givenname=None,
289             initials=None, profilepath=None, scriptpath=None, homedrive=None,
290             homedirectory=None, jobtitle=None, department=None, company=None,
291             description=None, mailaddress=None, internetaddress=None,
292             telephonenumber=None, physicaldeliveryoffice=None, sd=None,
293             setpassword=True):
294         """Adds a new user with additional parameters
295
296         :param username: Name of the new user
297         :param password: Password for the new user
298         :param force_password_change_at_next_login_req: Force password change
299         :param useusernameascn: Use username as cn rather that firstname +
300             initials + lastname
301         :param userou: Object container (without domainDN postfix) for new user
302         :param surname: Surname of the new user
303         :param givenname: First name of the new user
304         :param initials: Initials of the new user
305         :param profilepath: Profile path of the new user
306         :param scriptpath: Logon script path of the new user
307         :param homedrive: Home drive of the new user
308         :param homedirectory: Home directory of the new user
309         :param jobtitle: Job title of the new user
310         :param department: Department of the new user
311         :param company: Company of the new user
312         :param description: of the new user
313         :param mailaddress: Email address of the new user
314         :param internetaddress: Home page of the new user
315         :param telephonenumber: Phone number of the new user
316         :param physicaldeliveryoffice: Office location of the new user
317         :param sd: security descriptor of the object
318         :param setpassword: optionally disable password reset
319         """
320
321         displayname = ""
322         if givenname is not None:
323             displayname += givenname
324
325         if initials is not None:
326             displayname += ' %s.' % initials
327
328         if surname is not None:
329             displayname += ' %s' % surname
330
331         cn = username
332         if useusernameascn is None and displayname is not "":
333             cn = displayname
334
335         user_dn = "CN=%s,%s,%s" % (cn, (userou or "CN=Users"), self.domain_dn())
336
337         dnsdomain = ldb.Dn(self, self.domain_dn()).canonical_str().replace("/", "")
338         user_principal_name = "%s@%s" % (username, dnsdomain)
339         # The new user record. Note the reliance on the SAMLDB module which
340         # fills in the default informations
341         ldbmessage = {"dn": user_dn,
342                       "sAMAccountName": username,
343                       "userPrincipalName": user_principal_name,
344                       "objectClass": "user"}
345
346         if surname is not None:
347             ldbmessage["sn"] = surname
348
349         if givenname is not None:
350             ldbmessage["givenName"] = givenname
351
352         if displayname is not "":
353             ldbmessage["displayName"] = displayname
354             ldbmessage["name"] = displayname
355
356         if initials is not None:
357             ldbmessage["initials"] = '%s.' % initials
358
359         if profilepath is not None:
360             ldbmessage["profilePath"] = profilepath
361
362         if scriptpath is not None:
363             ldbmessage["scriptPath"] = scriptpath
364
365         if homedrive is not None:
366             ldbmessage["homeDrive"] = homedrive
367
368         if homedirectory is not None:
369             ldbmessage["homeDirectory"] = homedirectory
370
371         if jobtitle is not None:
372             ldbmessage["title"] = jobtitle
373
374         if department is not None:
375             ldbmessage["department"] = department
376
377         if company is not None:
378             ldbmessage["company"] = company
379
380         if description is not None:
381             ldbmessage["description"] = description
382
383         if mailaddress is not None:
384             ldbmessage["mail"] = mailaddress
385
386         if internetaddress is not None:
387             ldbmessage["wWWHomePage"] = internetaddress
388
389         if telephonenumber is not None:
390             ldbmessage["telephoneNumber"] = telephonenumber
391
392         if physicaldeliveryoffice is not None:
393             ldbmessage["physicalDeliveryOfficeName"] = physicaldeliveryoffice
394
395         if sd is not None:
396             ldbmessage["nTSecurityDescriptor"] = ndr_pack(sd)
397
398         self.transaction_start()
399         try:
400             self.add(ldbmessage)
401
402             # Sets the password for it
403             if setpassword:
404                 self.setpassword("(samAccountName=%s)" % ldb.binary_encode(username), password,
405                                  force_password_change_at_next_login_req)
406         except:
407             self.transaction_cancel()
408             raise
409         else:
410             self.transaction_commit()
411
412
413     def deleteuser(self, username):
414         """Deletes a user
415
416         :param username: Name of the target user
417         """
418
419         filter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (ldb.binary_encode(username), "CN=Person,CN=Schema,CN=Configuration", self.domain_dn())
420         self.transaction_start()
421         try:
422             target = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
423                                  expression=filter, attrs=[])
424             if len(target) == 0:
425                 raise Exception('Unable to find user "%s"' % username)
426             assert(len(target) == 1)
427             self.delete(target[0].dn)
428         except:
429             self.transaction_cancel()
430             raise
431         else:
432             self.transaction_commit()
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                   "(distinguishedName=" + str(user_dn) + ")")
464
465             #  modify the userAccountControl to remove the disabled bit
466             self.enable_account(search_filter)
467         except:
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:
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, write_indices_and_attributes=True):
611         self.set_schema_from_ldb(schema.ldb, write_indices_and_attributes=write_indices_and_attributes)
612
613     def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True):
614         dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes)
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="distinguishedName=%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="distinguishedName=%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 get_nc_root(self, dn):
744         return dsdb._dsdb_get_nc_root(self, dn)
745
746     def get_wellknown_dn(self, nc_root, wkguid):
747         return dsdb._dsdb_get_wellknown_dn(self, nc_root, wkguid)
748
749     def set_minPwdAge(self, value):
750         m = ldb.Message()
751         m.dn = ldb.Dn(self, self.domain_dn())
752         m["minPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdAge")
753         self.modify(m)
754
755     def get_minPwdAge(self):
756         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdAge"])
757         if len(res) == 0:
758             return None
759         elif not "minPwdAge" in res[0]:
760             return None
761         else:
762             return res[0]["minPwdAge"][0]
763
764     def set_minPwdLength(self, value):
765         m = ldb.Message()
766         m.dn = ldb.Dn(self, self.domain_dn())
767         m["minPwdLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "minPwdLength")
768         self.modify(m)
769
770     def get_minPwdLength(self):
771         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["minPwdLength"])
772         if len(res) == 0:
773             return None
774         elif not "minPwdLength" in res[0]:
775             return None
776         else:
777             return res[0]["minPwdLength"][0]
778
779     def set_pwdProperties(self, value):
780         m = ldb.Message()
781         m.dn = ldb.Dn(self, self.domain_dn())
782         m["pwdProperties"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdProperties")
783         self.modify(m)
784
785     def get_pwdProperties(self):
786         res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["pwdProperties"])
787         if len(res) == 0:
788             return None
789         elif not "pwdProperties" in res[0]:
790             return None
791         else:
792             return res[0]["pwdProperties"][0]
793
794     def set_dsheuristics(self, dsheuristics):
795         m = ldb.Message()
796         m.dn = ldb.Dn(self, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
797                       % self.get_config_basedn().get_linearized())
798         if dsheuristics is not None:
799             m["dSHeuristics"] = ldb.MessageElement(dsheuristics,
800                 ldb.FLAG_MOD_REPLACE, "dSHeuristics")
801         else:
802             m["dSHeuristics"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
803                 "dSHeuristics")
804         self.modify(m)
805
806     def get_dsheuristics(self):
807         res = self.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
808                           % self.get_config_basedn().get_linearized(),
809                           scope=ldb.SCOPE_BASE, attrs=["dSHeuristics"])
810         if len(res) == 0:
811             dsheuristics = None
812         elif "dSHeuristics" in res[0]:
813             dsheuristics = res[0]["dSHeuristics"][0]
814         else:
815             dsheuristics = None
816
817         return dsheuristics
818
819     def create_ou(self, ou_dn, description=None, name=None, sd=None):
820         """Creates an organizationalUnit object
821         :param ou_dn: dn of the new object
822         :param description: description attribute
823         :param name: name atttribute
824         :param sd: security descriptor of the object, can be
825         an SDDL string or security.descriptor type
826         """
827         m = {"dn": ou_dn,
828              "objectClass": "organizationalUnit"}
829
830         if description:
831             m["description"] = description
832         if name:
833             m["name"] = name
834
835         if sd:
836             m["nTSecurityDescriptor"] = ndr_pack(sd)
837         self.add(m)
838
839     def sequence_number(self, seq_type):
840         """Returns the value of the sequence number according to the requested type
841         :param seq_type: type of sequence number
842          """
843         self.transaction_start()
844         try:
845             seq = super(SamDB, self).sequence_number(seq_type)
846         except:
847             self.transaction_cancel()
848             raise
849         else:
850             self.transaction_commit()
851         return seq
852
853     def get_dsServiceName(self):
854         '''get the NTDS DN from the rootDSE'''
855         res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
856         return res[0]["dsServiceName"][0]
857
858     def get_serverName(self):
859         '''get the server DN from the rootDSE'''
860         res = self.search(base="", scope=ldb.SCOPE_BASE, attrs=["serverName"])
861         return res[0]["serverName"][0]