3 # Unix SMB/CIFS implementation.
4 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010
5 # Copyright (C) Matthias Dieter Wallnoefer 2009
7 # Based on the original in EJS:
8 # Copyright (C) Andrew Tridgell <tridge@samba.org> 2005
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 """Convenience functions for using the SAM."""
30 from samba
import dsdb
31 from samba
.ndr
import ndr_unpack
, ndr_pack
32 from samba
.dcerpc
import drsblobs
, misc
34 __docformat__
= "restructuredText"
37 class SamDB(samba
.Ldb
):
38 """The SAM database."""
42 def __init__(self
, url
=None, lp
=None, modules_dir
=None, session_info
=None,
43 credentials
=None, flags
=0, options
=None, global_schema
=True,
44 auto_connect
=True, am_rodc
=None):
48 elif url
is None and lp
is not None:
51 super(SamDB
, self
).__init
__(url
=url
, lp
=lp
, modules_dir
=modules_dir
,
52 session_info
=session_info
, credentials
=credentials
, flags
=flags
,
56 dsdb
._dsdb
_set
_global
_schema
(self
)
58 if am_rodc
is not None:
59 dsdb
._dsdb
_set
_am
_rodc
(self
, am_rodc
)
61 def connect(self
, url
=None, flags
=0, options
=None):
62 if self
.lp
is not None:
63 url
= self
.lp
.private_path(url
)
65 super(SamDB
, self
).connect(url
=url
, flags
=flags
,
69 return dsdb
._am
_rodc
(self
)
72 return str(self
.get_default_basedn())
74 def enable_account(self
, search_filter
):
77 :param search_filter: LDAP filter to find the user (eg
80 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
81 expression
=search_filter
, attrs
=["userAccountControl"])
83 raise Exception('Unable to find user "%s"' % search_filter
)
87 userAccountControl
= int(res
[0]["userAccountControl"][0])
88 if userAccountControl
& 0x2:
90 userAccountControl
= userAccountControl
& ~
0x2
91 if userAccountControl
& 0x20:
92 # remove 'no password required' bit
93 userAccountControl
= userAccountControl
& ~
0x20
98 replace: userAccountControl
99 userAccountControl: %u
100 """ % (user_dn
, userAccountControl
)
101 self
.modify_ldif(mod
)
103 def force_password_change_at_next_login(self
, search_filter
):
104 """Forces a password change at next login
106 :param search_filter: LDAP filter to find the user (eg
109 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
110 expression
=search_filter
, attrs
=[])
112 raise Exception('Unable to find user "%s"' % search_filter
)
113 assert(len(res
) == 1)
122 self
.modify_ldif(mod
)
124 def newgroup(self
, groupname
, groupou
=None, grouptype
=None,
125 description
=None, mailaddress
=None, notes
=None, sd
=None):
126 """Adds a new group with additional parameters
128 :param groupname: Name of the new group
129 :param grouptype: Type of the new group
130 :param description: Description of the new group
131 :param mailaddress: Email address of the new group
132 :param notes: Notes of the new group
133 :param sd: security descriptor of the object
136 group_dn
= "CN=%s,%s,%s" % (groupname
, (groupou
or "CN=Users"), self
.domain_dn())
138 # The new user record. Note the reliance on the SAMLDB module which
139 # fills in the default informations
140 ldbmessage
= {"dn": group_dn
,
141 "sAMAccountName": groupname
,
142 "objectClass": "group"}
144 if grouptype
is not None:
145 ldbmessage
["groupType"] = "%d" % grouptype
147 if description
is not None:
148 ldbmessage
["description"] = description
150 if mailaddress
is not None:
151 ldbmessage
["mail"] = mailaddress
153 if notes
is not None:
154 ldbmessage
["info"] = notes
157 ldbmessage
["nTSecurityDescriptor"] = ndr_pack(sd
)
161 def deletegroup(self
, groupname
):
164 :param groupname: Name of the target group
167 groupfilter
= "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname
, "CN=Group,CN=Schema,CN=Configuration", self
.domain_dn())
168 self
.transaction_start()
170 targetgroup
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
171 expression
=groupfilter
, attrs
=[])
172 if len(targetgroup
) == 0:
173 raise Exception('Unable to find group "%s"' % groupname
)
174 assert(len(targetgroup
) == 1)
175 self
.delete(targetgroup
[0].dn
)
177 self
.transaction_cancel()
180 self
.transaction_commit()
182 def add_remove_group_members(self
, groupname
, listofmembers
,
183 add_members_operation
=True):
184 """Adds or removes group members
186 :param groupname: Name of the target group
187 :param listofmembers: Comma-separated list of group members
188 :param add_members_operation: Defines if its an add or remove
192 groupfilter
= "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (groupname
, "CN=Group,CN=Schema,CN=Configuration", self
.domain_dn())
193 groupmembers
= listofmembers
.split(',')
195 self
.transaction_start()
197 targetgroup
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
198 expression
=groupfilter
, attrs
=['member'])
199 if len(targetgroup
) == 0:
200 raise Exception('Unable to find group "%s"' % groupname
)
201 assert(len(targetgroup
) == 1)
205 addtargettogroup
= """
208 """ % (str(targetgroup
[0].dn
))
210 for member
in groupmembers
:
211 targetmember
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
212 expression
="(|(sAMAccountName=%s)(CN=%s))" % (member
, member
), attrs
=[])
214 if len(targetmember
) != 1:
217 if add_members_operation
is True and (targetgroup
[0].get('member') is None or str(targetmember
[0].dn
) not in targetgroup
[0]['member']):
219 addtargettogroup
+= """add: member
221 """ % (str(targetmember
[0].dn
))
223 elif add_members_operation
is False and (targetgroup
[0].get('member') is not None and str(targetmember
[0].dn
) in targetgroup
[0]['member']):
225 addtargettogroup
+= """delete: member
227 """ % (str(targetmember
[0].dn
))
230 self
.modify_ldif(addtargettogroup
)
233 self
.transaction_cancel()
236 self
.transaction_commit()
238 def newuser(self
, username
, password
,
239 force_password_change_at_next_login_req
=False,
240 useusernameascn
=False, userou
=None, surname
=None, givenname
=None,
241 initials
=None, profilepath
=None, scriptpath
=None, homedrive
=None,
242 homedirectory
=None, jobtitle
=None, department
=None, company
=None,
243 description
=None, mailaddress
=None, internetaddress
=None,
244 telephonenumber
=None, physicaldeliveryoffice
=None, sd
=None,
246 """Adds a new user with additional parameters
248 :param username: Name of the new user
249 :param password: Password for the new user
250 :param force_password_change_at_next_login_req: Force password change
251 :param useusernameascn: Use username as cn rather that firstname +
253 :param userou: Object container (without domainDN postfix) for new user
254 :param surname: Surname of the new user
255 :param givenname: First name of the new user
256 :param initials: Initials of the new user
257 :param profilepath: Profile path of the new user
258 :param scriptpath: Logon script path of the new user
259 :param homedrive: Home drive of the new user
260 :param homedirectory: Home directory of the new user
261 :param jobtitle: Job title of the new user
262 :param department: Department of the new user
263 :param company: Company of the new user
264 :param description: of the new user
265 :param mailaddress: Email address of the new user
266 :param internetaddress: Home page of the new user
267 :param telephonenumber: Phone number of the new user
268 :param physicaldeliveryoffice: Office location of the new user
269 :param sd: security descriptor of the object
270 :param setpassword: optionally disable password reset
274 if givenname
is not None:
275 displayname
+= givenname
277 if initials
is not None:
278 displayname
+= ' %s.' % initials
280 if surname
is not None:
281 displayname
+= ' %s' % surname
284 if useusernameascn
is None and displayname
is not "":
287 user_dn
= "CN=%s,%s,%s" % (cn
, (userou
or "CN=Users"), self
.domain_dn())
289 dnsdomain
= ldb
.Dn(self
, self
.domain_dn()).canonical_str().replace("/", "")
290 user_principal_name
= "%s@%s" % (username
, dnsdomain
)
291 # The new user record. Note the reliance on the SAMLDB module which
292 # fills in the default informations
293 ldbmessage
= {"dn": user_dn
,
294 "sAMAccountName": username
,
295 "userPrincipalName": user_principal_name
,
296 "objectClass": "user"}
298 if surname
is not None:
299 ldbmessage
["sn"] = surname
301 if givenname
is not None:
302 ldbmessage
["givenName"] = givenname
304 if displayname
is not "":
305 ldbmessage
["displayName"] = displayname
306 ldbmessage
["name"] = displayname
308 if initials
is not None:
309 ldbmessage
["initials"] = '%s.' % initials
311 if profilepath
is not None:
312 ldbmessage
["profilePath"] = profilepath
314 if scriptpath
is not None:
315 ldbmessage
["scriptPath"] = scriptpath
317 if homedrive
is not None:
318 ldbmessage
["homeDrive"] = homedrive
320 if homedirectory
is not None:
321 ldbmessage
["homeDirectory"] = homedirectory
323 if jobtitle
is not None:
324 ldbmessage
["title"] = jobtitle
326 if department
is not None:
327 ldbmessage
["department"] = department
329 if company
is not None:
330 ldbmessage
["company"] = company
332 if description
is not None:
333 ldbmessage
["description"] = description
335 if mailaddress
is not None:
336 ldbmessage
["mail"] = mailaddress
338 if internetaddress
is not None:
339 ldbmessage
["wWWHomePage"] = internetaddress
341 if telephonenumber
is not None:
342 ldbmessage
["telephoneNumber"] = telephonenumber
344 if physicaldeliveryoffice
is not None:
345 ldbmessage
["physicalDeliveryOfficeName"] = physicaldeliveryoffice
348 ldbmessage
["nTSecurityDescriptor"] = ndr_pack(sd
)
350 self
.transaction_start()
354 # Sets the password for it
356 self
.setpassword("(samAccountName=%s)" % username
, password
,
357 force_password_change_at_next_login_req
)
359 self
.transaction_cancel()
362 self
.transaction_commit()
364 def setpassword(self
, search_filter
, password
,
365 force_change_at_next_login
=False, username
=None):
366 """Sets the password for a user
368 :param search_filter: LDAP filter to find the user (eg
370 :param password: Password for the user
371 :param force_change_at_next_login: Force password change
373 self
.transaction_start()
375 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
376 expression
=search_filter
, attrs
=[])
378 raise Exception('Unable to find user "%s"' % (username
or search_filter
))
380 raise Exception('Matched %u multiple users with filter "%s"' % (len(res
), search_filter
))
387 """ % (user_dn
, base64
.b64encode(("\"" + password
+ "\"").encode('utf-16-le')))
389 self
.modify_ldif(setpw
)
391 if force_change_at_next_login
:
392 self
.force_password_change_at_next_login(
393 "(dn=" + str(user_dn
) + ")")
395 # modify the userAccountControl to remove the disabled bit
396 self
.enable_account(search_filter
)
398 self
.transaction_cancel()
401 self
.transaction_commit()
403 def setexpiry(self
, search_filter
, expiry_seconds
, no_expiry_req
=False):
404 """Sets the account expiry for a user
406 :param search_filter: LDAP filter to find the user (eg
408 :param expiry_seconds: expiry time from now in seconds
409 :param no_expiry_req: if set, then don't expire password
411 self
.transaction_start()
413 res
= self
.search(base
=self
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
414 expression
=search_filter
,
415 attrs
=["userAccountControl", "accountExpires"])
417 raise Exception('Unable to find user "%s"' % search_filter
)
418 assert(len(res
) == 1)
421 userAccountControl
= int(res
[0]["userAccountControl"][0])
422 accountExpires
= int(res
[0]["accountExpires"][0])
424 userAccountControl
= userAccountControl |
0x10000
427 userAccountControl
= userAccountControl
& ~
0x10000
428 accountExpires
= samba
.unix2nttime(expiry_seconds
+ int(time
.time()))
433 replace: userAccountControl
434 userAccountControl: %u
435 replace: accountExpires
437 """ % (user_dn
, userAccountControl
, accountExpires
)
439 self
.modify_ldif(setexp
)
441 self
.transaction_cancel()
444 self
.transaction_commit()
446 def set_domain_sid(self
, sid
):
447 """Change the domain SID used by this LDB.
449 :param sid: The new domain sid to use.
451 dsdb
._samdb
_set
_domain
_sid
(self
, sid
)
453 def get_domain_sid(self
):
454 """Read the domain SID used by this LDB. """
455 return dsdb
._samdb
_get
_domain
_sid
(self
)
457 domain_sid
= property(get_domain_sid
, set_domain_sid
,
458 "SID for the domain")
460 def set_invocation_id(self
, invocation_id
):
461 """Set the invocation id for this SamDB handle.
463 :param invocation_id: GUID of the invocation id.
465 dsdb
._dsdb
_set
_ntds
_invocation
_id
(self
, invocation_id
)
467 def get_invocation_id(self
):
468 """Get the invocation_id id"""
469 return dsdb
._samdb
_ntds
_invocation
_id
(self
)
471 invocation_id
= property(get_invocation_id
, set_invocation_id
,
472 "Invocation ID GUID")
474 def get_oid_from_attid(self
, attid
):
475 return dsdb
._dsdb
_get
_oid
_from
_attid
(self
, attid
)
477 def get_attid_from_lDAPDisplayName(self
, ldap_display_name
,
479 return dsdb
._dsdb
_get
_attid
_from
_lDAPDisplayName
(self
,
480 ldap_display_name
, is_schema_nc
)
482 def set_ntds_settings_dn(self
, ntds_settings_dn
):
483 """Set the NTDS Settings DN, as would be returned on the dsServiceName
486 This allows the DN to be set before the database fully exists
488 :param ntds_settings_dn: The new DN to use
490 dsdb
._samdb
_set
_ntds
_settings
_dn
(self
, ntds_settings_dn
)
492 def get_ntds_GUID(self
):
493 """Get the NTDS objectGUID"""
494 return dsdb
._samdb
_ntds
_objectGUID
(self
)
496 def server_site_name(self
):
497 """Get the server site name"""
498 return dsdb
._samdb
_server
_site
_name
(self
)
500 def load_partition_usn(self
, base_dn
):
501 return dsdb
._dsdb
_load
_partition
_usn
(self
, base_dn
)
503 def set_schema(self
, schema
):
504 self
.set_schema_from_ldb(schema
.ldb
)
506 def set_schema_from_ldb(self
, ldb_conn
):
507 dsdb
._dsdb
_set
_schema
_from
_ldb
(self
, ldb_conn
)
509 def dsdb_DsReplicaAttribute(self
, ldb
, ldap_display_name
, ldif_elements
):
510 return dsdb
._dsdb
_DsReplicaAttribute
(ldb
, ldap_display_name
, ldif_elements
)
512 def get_attribute_from_attid(self
, attid
):
513 """ Get from an attid the associated attribute
515 :param attid: The attribute id for searched attribute
516 :return: The name of the attribute associated with this id
518 if len(self
.hash_oid_name
.keys()) == 0:
519 self
._populate
_oid
_attid
()
520 if self
.hash_oid_name
.has_key(self
.get_oid_from_attid(attid
)):
521 return self
.hash_oid_name
[self
.get_oid_from_attid(attid
)]
525 def _populate_oid_attid(self
):
526 """Populate the hash hash_oid_name.
528 This hash contains the oid of the attribute as a key and
529 its display name as a value
531 self
.hash_oid_name
= {}
532 res
= self
.search(expression
="objectClass=attributeSchema",
533 controls
=["search_options:1:2"],
534 attrs
=["attributeID",
538 strDisplay
= str(e
.get("lDAPDisplayName"))
539 self
.hash_oid_name
[str(e
.get("attributeID"))] = strDisplay
541 def get_attribute_replmetadata_version(self
, dn
, att
):
542 """Get the version field trom the replPropertyMetaData for
545 :param dn: The on which we want to get the version
546 :param att: The name of the attribute
547 :return: The value of the version field in the replPropertyMetaData
548 for the given attribute. None if the attribute is not replicated
551 res
= self
.search(expression
="dn=%s" % dn
,
552 scope
=ldb
.SCOPE_SUBTREE
,
553 controls
=["search_options:1:2"],
554 attrs
=["replPropertyMetaData"])
558 repl
= ndr_unpack(drsblobs
.replPropertyMetaDataBlob
,
559 str(res
[0]["replPropertyMetaData"]))
561 if len(self
.hash_oid_name
.keys()) == 0:
562 self
._populate
_oid
_attid
()
564 # Search for Description
565 att_oid
= self
.get_oid_from_attid(o
.attid
)
566 if self
.hash_oid_name
.has_key(att_oid
) and\
567 att
.lower() == self
.hash_oid_name
[att_oid
].lower():
571 def set_attribute_replmetadata_version(self
, dn
, att
, value
,
572 addifnotexist
=False):
573 res
= self
.search(expression
="dn=%s" % dn
,
574 scope
=ldb
.SCOPE_SUBTREE
,
575 controls
=["search_options:1:2"],
576 attrs
=["replPropertyMetaData"])
580 repl
= ndr_unpack(drsblobs
.replPropertyMetaDataBlob
,
581 str(res
[0]["replPropertyMetaData"]))
583 now
= samba
.unix2nttime(int(time
.time()))
585 if len(self
.hash_oid_name
.keys()) == 0:
586 self
._populate
_oid
_attid
()
588 # Search for Description
589 att_oid
= self
.get_oid_from_attid(o
.attid
)
590 if self
.hash_oid_name
.has_key(att_oid
) and\
591 att
.lower() == self
.hash_oid_name
[att_oid
].lower():
593 seq
= self
.sequence_number(ldb
.SEQ_NEXT
)
595 o
.originating_change_time
= now
596 o
.originating_invocation_id
= misc
.GUID(self
.get_invocation_id())
597 o
.originating_usn
= seq
600 if not found
and addifnotexist
and len(ctr
.array
) >0:
601 o2
= drsblobs
.replPropertyMetaData1()
603 att_oid
= self
.get_oid_from_attid(o2
.attid
)
604 seq
= self
.sequence_number(ldb
.SEQ_NEXT
)
606 o2
.originating_change_time
= now
607 o2
.originating_invocation_id
= misc
.GUID(self
.get_invocation_id())
608 o2
.originating_usn
= seq
613 ctr
.count
= ctr
.count
+ 1
617 replBlob
= ndr_pack(repl
)
620 msg
["replPropertyMetaData"] = ldb
.MessageElement(replBlob
,
621 ldb
.FLAG_MOD_REPLACE
,
622 "replPropertyMetaData")
623 self
.modify(msg
, ["local_oid:1.3.6.1.4.1.7165.4.3.14:0"])
625 def write_prefixes_from_schema(self
):
626 dsdb
._dsdb
_write
_prefixes
_from
_schema
_to
_ldb
(self
)
628 def get_partitions_dn(self
):
629 return dsdb
._dsdb
_get
_partitions
_dn
(self
)
631 def set_minPwdAge(self
, value
):
633 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
634 m
["minPwdAge"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "minPwdAge")
637 def get_minPwdAge(self
):
638 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["minPwdAge"])
641 elif not "minPwdAge" in res
[0]:
644 return res
[0]["minPwdAge"][0]
646 def set_minPwdLength(self
, value
):
648 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
649 m
["minPwdLength"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "minPwdLength")
652 def get_minPwdLength(self
):
653 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["minPwdLength"])
656 elif not "minPwdLength" in res
[0]:
659 return res
[0]["minPwdLength"][0]
661 def set_pwdProperties(self
, value
):
663 m
.dn
= ldb
.Dn(self
, self
.domain_dn())
664 m
["pwdProperties"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "pwdProperties")
667 def get_pwdProperties(self
):
668 res
= self
.search(self
.domain_dn(), scope
=ldb
.SCOPE_BASE
, attrs
=["pwdProperties"])
671 elif not "pwdProperties" in res
[0]:
674 return res
[0]["pwdProperties"][0]
676 def set_dsheuristics(self
, dsheuristics
):
678 m
.dn
= ldb
.Dn(self
, "CN=Directory Service,CN=Windows NT,CN=Services,%s"
679 % self
.get_config_basedn().get_linearized())
680 if dsheuristics
is not None:
681 m
["dSHeuristics"] = ldb
.MessageElement(dsheuristics
,
682 ldb
.FLAG_MOD_REPLACE
, "dSHeuristics")
684 m
["dSHeuristics"] = ldb
.MessageElement([], ldb
.FLAG_MOD_DELETE
,
688 def get_dsheuristics(self
):
689 res
= self
.search("CN=Directory Service,CN=Windows NT,CN=Services,%s"
690 % self
.get_config_basedn().get_linearized(),
691 scope
=ldb
.SCOPE_BASE
, attrs
=["dSHeuristics"])
694 elif "dSHeuristics" in res
[0]:
695 dsheuristics
= res
[0]["dSHeuristics"][0]
701 def create_ou(self
, ou_dn
, description
=None, name
=None, sd
=None):
702 """Creates an organizationalUnit object
703 :param ou_dn: dn of the new object
704 :param description: description attribute
705 :param name: name atttribute
706 :param sd: security descriptor of the object, can be
707 an SDDL string or security.descriptor type
710 "objectClass": "organizationalUnit"}
713 m
["description"] = description
718 m
["nTSecurityDescriptor"] = ndr_pack(sd
)