3 # Helpers for provision stuff
4 # Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010
6 # Based on provision a Samba4 server by
7 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008
8 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008
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.
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.
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/>.
31 from samba import Ldb, version, ntacls
32 from samba.dsdb import DS_DOMAIN_FUNCTION_2000
33 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE
35 from samba.provision import ProvisionNames, provision_paths_from_lp,\
36 getpolicypath, set_gpo_acl, create_gpo_struct,\
37 FILL_FULL, provision, ProvisioningError,\
39 from samba.dcerpc import misc, security, xattr
40 from samba.ndr import ndr_unpack
42 # All the ldb related to registry are commented because the path for them is relative
43 # in the provisionPath object
44 # And so opening them create a file in the current directory which is not what we want
45 # I still keep them commented because I plan soon to make more cleaner
54 hashAttrNotCopied = { "dn": 1, "whenCreated": 1, "whenChanged": 1,
55 "objectGUID": 1, "uSNCreated": 1,
56 "replPropertyMetaData": 1, "uSNChanged": 1,
57 "parentGUID": 1, "objectCategory": 1,
58 "distinguishedName": 1, "nTMixedDomain": 1,
59 "showInAdvancedViewOnly": 1, "instanceType": 1,
60 "msDS-Behavior-Version":1, "nextRid":1, "cn": 1,
61 "versionNumber":1, "lmPwdHistory":1, "pwdLastSet": 1,
62 "ntPwdHistory":1, "unicodePwd":1,"dBCSPwd":1,
63 "supplementalCredentials":1, "gPCUserExtensionNames":1,
64 "gPCMachineExtensionNames":1,"maxPwdAge":1, "secret":1,
65 "possibleInferiors":1, "privilege":1,
68 class ProvisionLDB(object):
79 def startTransactions(self):
80 self.sam.transaction_start()
81 self.secrets.transaction_start()
82 self.idmap.transaction_start()
83 self.privilege.transaction_start()
84 # self.hkcr.transaction_start()
85 # self.hkcu.transaction_start()
86 # self.hku.transaction_start()
87 # self.hklm.transaction_start()
89 def groupedRollback(self):
90 self.sam.transaction_cancel()
91 self.secrets.transaction_cancel()
92 self.idmap.transaction_cancel()
93 self.privilege.transaction_cancel()
94 # self.hkcr.transaction_cancel()
95 # self.hkcu.transaction_cancel()
96 # self.hku.transaction_cancel()
97 # self.hklm.transaction_cancel()
99 def groupedCommit(self):
100 self.sam.transaction_prepare_commit()
101 self.secrets.transaction_prepare_commit()
102 self.idmap.transaction_prepare_commit()
103 self.privilege.transaction_prepare_commit()
104 # self.hkcr.transaction_prepare_commit()
105 # self.hkcu.transaction_prepare_commit()
106 # self.hku.transaction_prepare_commit()
107 # self.hklm.transaction_prepare_commit()
109 self.sam.transaction_commit()
110 self.secrets.transaction_commit()
111 self.idmap.transaction_commit()
112 self.privilege.transaction_commit()
113 # self.hkcr.transaction_commit()
114 # self.hkcu.transaction_commit()
115 # self.hku.transaction_commit()
116 # self.hklm.transaction_commit()
118 def get_ldbs(paths, creds, session, lp):
119 """Return LDB object mapped on most important databases
121 :param paths: An object holding the different importants paths for provision object
122 :param creds: Credential used for openning LDB files
123 :param session: Session to use for openning LDB files
124 :param lp: A loadparam object
125 :return: A ProvisionLDB object that contains LDB object for the different LDB files of the provision"""
127 ldbs = ProvisionLDB()
129 ldbs.sam = Ldb(paths.samdb, session_info=session, credentials=creds, lp=lp, options=["modules:samba_dsdb"])
130 ldbs.secrets = Ldb(paths.secrets, session_info=session, credentials=creds, lp=lp)
131 ldbs.idmap = Ldb(paths.idmapdb, session_info=session, credentials=creds, lp=lp)
132 ldbs.privilege = Ldb(paths.privilege, session_info=session, credentials=creds, lp=lp)
133 # ldbs.hkcr = Ldb(paths.hkcr, session_info=session, credentials=creds, lp=lp)
134 # ldbs.hkcu = Ldb(paths.hkcu, session_info=session, credentials=creds, lp=lp)
135 # ldbs.hku = Ldb(paths.hku, session_info=session, credentials=creds, lp=lp)
136 # ldbs.hklm = Ldb(paths.hklm, session_info=session, credentials=creds, lp=lp)
140 def usn_in_range(usn, range):
141 """Check if the usn is in one of the range provided.
142 To do so, the value is checked to be between the lower bound and
143 higher bound of a range
145 :param usn: A integer value corresponding to the usn that we want to update
146 :param range: A list of integer representing ranges, lower bounds are in
147 the even indices, higher in odd indices
148 :return: 1 if the usn is in one of the range, 0 otherwise"""
154 if idx == len(range):
157 if usn < int(range[idx]):
161 if usn == int(range[idx]):
167 def get_paths(param, targetdir=None, smbconf=None):
168 """Get paths to important provision objects (smb.conf, ldb files, ...)
170 :param param: Param object
171 :param targetdir: Directory where the provision is (or will be) stored
172 :param smbconf: Path to the smb.conf file
173 :return: A list with the path of important provision objects"""
174 if targetdir is not None:
175 etcdir = os.path.join(targetdir, "etc")
176 if not os.path.exists(etcdir):
178 smbconf = os.path.join(etcdir, "smb.conf")
180 smbconf = param.default_path()
182 if not os.path.exists(smbconf):
183 raise ProvisioningError("Unable to find smb.conf")
185 lp = param.LoadParm()
187 paths = provision_paths_from_lp(lp, lp.get("realm"))
191 def find_provision_key_parameters(samdb, secretsdb, idmapdb, paths, smbconf, lp):
192 """Get key provision parameters (realm, domain, ...) from a given provision
194 :param samdb: An LDB object connected to the sam.ldb file
195 :param secretsdb: An LDB object connected to the secrets.ldb file
196 :param idmapdb: An LDB object connected to the idmap.ldb file
197 :param paths: A list of path to provision object
198 :param smbconf: Path to the smb.conf file
199 :param lp: A LoadParm object
200 :return: A list of key provision parameters"""
202 names = ProvisionNames()
203 names.adminpass = None
205 # NT domain, kerberos realm, root dn, domain dn, domain dns name
206 names.domain = string.upper(lp.get("workgroup"))
207 names.realm = lp.get("realm")
208 basedn = "DC=" + names.realm.replace(".",",DC=")
209 names.dnsdomain = names.realm.lower()
210 names.realm = string.upper(names.realm)
212 # Get the netbiosname first (could be obtained from smb.conf in theory)
213 res = secretsdb.search(expression="(flatname=%s)"%names.domain,base="CN=Primary Domains", scope=SCOPE_SUBTREE, attrs=["sAMAccountName"])
214 names.netbiosname = str(res[0]["sAMAccountName"]).replace("$","")
216 names.smbconf = smbconf
218 # That's a bit simplistic but it's ok as long as we have only 3
220 current = samdb.search(expression="(objectClass=*)",
221 base="", scope=SCOPE_BASE,
222 attrs=["defaultNamingContext", "schemaNamingContext",
223 "configurationNamingContext","rootDomainNamingContext"])
225 names.configdn = current[0]["configurationNamingContext"]
226 configdn = str(names.configdn)
227 names.schemadn = current[0]["schemaNamingContext"]
228 if ldb.Dn(samdb, basedn) != ldb.Dn(samdb, current[0]["defaultNamingContext"][0]):
229 raise ProvisioningError("basedn in %s (%s) and from %s (%s) is not the same ..." % (paths.samdb, str(current[0]["defaultNamingContext"][0]), paths.smbconf, basedn))
231 names.domaindn=current[0]["defaultNamingContext"]
232 names.rootdn=current[0]["rootDomainNamingContext"]
234 res3 = samdb.search(expression="(objectClass=*)",
235 base="CN=Sites,"+configdn, scope=SCOPE_ONELEVEL, attrs=["cn"])
236 names.sitename = str(res3[0]["cn"])
238 # dns hostname and server dn
239 res4 = samdb.search(expression="(CN=%s)" % names.netbiosname,
240 base="OU=Domain Controllers,"+basedn, scope=SCOPE_ONELEVEL, attrs=["dNSHostName"])
241 names.hostname = str(res4[0]["dNSHostName"]).replace("."+names.dnsdomain,"")
243 server_res = samdb.search(expression="serverReference=%s" % res4[0].dn,
244 attrs=[], base=configdn)
245 names.serverdn = server_res[0].dn
247 # invocation id/objectguid
248 res5 = samdb.search(expression="(objectClass=*)",
249 base="CN=NTDS Settings,%s" % str(names.serverdn), scope=SCOPE_BASE,
250 attrs=["invocationID", "objectGUID"])
251 names.invocation = str(ndr_unpack(misc.GUID, res5[0]["invocationId"][0]))
252 names.ntdsguid = str(ndr_unpack(misc.GUID, res5[0]["objectGUID"][0]))
255 res6 = samdb.search(expression="(objectClass=*)",base=basedn,
256 scope=SCOPE_BASE, attrs=["objectGUID",
257 "objectSid","msDS-Behavior-Version" ])
258 names.domainguid = str(ndr_unpack( misc.GUID,res6[0]["objectGUID"][0]))
259 names.domainsid = ndr_unpack( security.dom_sid,res6[0]["objectSid"][0])
260 if (res6[0].get("msDS-Behavior-Version") is None or
261 int(res6[0]["msDS-Behavior-Version"][0]) < DS_DOMAIN_FUNCTION_2000):
262 names.domainlevel = DS_DOMAIN_FUNCTION_2000
264 names.domainlevel = int(res6[0]["msDS-Behavior-Version"][0])
267 res7 = samdb.search(expression="(displayName=Default Domain Policy)",
268 base="CN=Policies,CN=System,"+basedn, scope=SCOPE_ONELEVEL,
269 attrs=["cn","displayName"])
270 names.policyid = str(res7[0]["cn"]).replace("{","").replace("}","")
272 res8 = samdb.search(expression="(displayName=Default Domain Controllers Policy)",
273 base="CN=Policies,CN=System,"+basedn, scope=SCOPE_ONELEVEL,
274 attrs=["cn","displayName"])
276 names.policyid_dc = str(res8[0]["cn"]).replace("{","").replace("}","")
278 names.policyid_dc = None
279 res9 = idmapdb.search(expression="(cn=%s)" % (security.SID_BUILTIN_ADMINISTRATORS),
282 names.wheel_gid = res9[0]["xidNumber"]
284 raise ProvisioningError("Unable to find uid/gid for Domain Admins rid")
288 def newprovision(names, setup_dir, creds, session, smbconf, provdir, logger):
289 """Create a new provision.
291 This provision will be the reference for knowing what has changed in the
292 since the latest upgrade in the current provision
294 :param names: List of provision parameters
295 :param setup_dis: Directory where the setup files are stored
296 :param creds: Credentials for the authentification
297 :param session: Session object
298 :param smbconf: Path to the smb.conf file
299 :param provdir: Directory where the provision will be stored
300 :param logger: A `Logger`
302 if os.path.isdir(provdir):
303 shutil.rmtree(provdir)
304 os.chdir(os.path.join(setup_dir,".."))
306 logger.info("Provision stored in %s", provdir)
307 provision(setup_dir, logger, session, creds, smbconf=smbconf,
308 targetdir=provdir, samdb_fill=FILL_FULL, realm=names.realm,
309 domain=names.domain, domainguid=names.domainguid,
310 domainsid=str(names.domainsid), ntdsguid=names.ntdsguid,
311 policyguid=names.policyid, policyguid_dc=names.policyid_dc,
312 hostname=names.netbiosname, hostip=None, hostip6=None,
313 invocationid=names.invocation, adminpass=names.adminpass,
314 krbtgtpass=None, machinepass=None, dnspass=None, root=None,
315 nobody=None, wheel=None, users=None,
316 serverrole="domain controller", ldap_backend_extra_port=None,
317 backend_type=None, ldapadminpass=None, ol_mmr_urls=None,
318 slapd_path=None, setup_ds_path=None, nosync=None,
319 dom_for_fun_level=names.domainlevel,
320 ldap_dryrun_mode=None, useeadb=True)
324 """Sorts two DNs in the lexicographical order it and put higher level DN
327 So given the dns cn=bar,cn=foo and cn=foo the later will be return as
330 :param x: First object to compare
331 :param y: Second object to compare
333 p = re.compile(r'(?<!\\), ?')
334 tab1 = p.split(str(x))
335 tab2 = p.split(str(y))
336 minimum = min(len(tab1), len(tab2))
339 # Note: python range go up to upper limit but do not include it
340 for i in range(0, minimum):
341 ret = cmp(tab1[len1-i], tab2[len2-i])
346 assert len1 != len2, "PB PB PB"+" ".join(tab1)+" / "+" ".join(tab2)
353 def identic_rename(ldbobj, dn):
354 """Perform a back and forth rename to trigger renaming on attribute that
355 can't be directly modified.
357 :param lbdobj: An Ldb Object
358 :param dn: DN of the object to manipulate """
359 (before, sep, after)=str(dn).partition('=')
360 ldbobj.rename(dn, ldb.Dn(ldbobj, "%s=foo%s" % (before, after)))
361 ldbobj.rename(ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), dn)
364 """Return separate ACE of an ACL
366 :param acl: A string representing the ACL
367 :return: A hash with different parts
370 p = re.compile(r'(\w+)?(\(.*?\))')
378 hash["aces"].append(e[1])
383 def chunck_sddl(sddl):
384 """ Return separate parts of the SDDL (owner, group, ...)
386 :param sddl: An string containing the SDDL to chunk
387 :return: A hash with the different chunk
390 p = re.compile(r'([OGDS]:)(.*?)(?=(?:[GDS]:|$))')
391 tab = p.findall(sddl)
406 def get_diff_sddls(refsddl, cursddl):
407 """Get the difference between 2 sddl
408 This function split the textual representation of ACL into smaller
409 chunck in order to not to report a simple permutation as a difference
411 :param refsddl: First sddl to compare
412 :param cursddl: Second sddl to compare
413 :return: A string that explain difference between sddls"""
416 hash_new = chunck_sddl(cursddl)
417 hash_ref = chunck_sddl(refsddl)
419 if hash_new["owner"] != hash_ref["owner"]:
420 txt = "\tOwner mismatch: %s (in ref) %s" \
421 "(in current)\n" % (hash_ref["owner"], hash_new["owner"])
423 if hash_new["group"] != hash_ref["group"]:
424 txt = "%s\tGroup mismatch: %s (in ref) %s" \
425 "(in current)\n" % (txt, hash_ref["group"], hash_new["group"])
427 for part in ["dacl", "sacl"]:
428 if hash_new.has_key(part) and hash_ref.has_key(part):
430 # both are present, check if they contain the same ACE
433 c_new = chunck_acl(hash_new[part])
434 c_ref = chunck_acl(hash_ref[part])
436 for elem in c_new["aces"]:
439 for elem in c_ref["aces"]:
442 for k in h_ref.keys():
447 if len(h_new.keys()) + len(h_ref.keys()) > 0:
448 txt = "%s\tPart %s is different between reference" \
449 " and current here is the detail:\n" % (txt, part)
451 for item in h_new.keys():
452 txt = "%s\t\t%s ACE is not present in the" \
453 " reference\n" % (txt, item)
455 for item in h_ref.keys():
456 txt = "%s\t\t%s ACE is not present in the" \
457 " current\n" % (txt, item)
459 elif hash_new.has_key(part) and not hash_ref.has_key(part):
460 txt = "%s\tReference ACL hasn't a %s part\n" % (txt, part)
461 elif not hash_new.has_key(part) and hash_ref.has_key(part):
462 txt = "%s\tCurrent ACL hasn't a %s part\n" % (txt, part)
467 def update_secrets(newsecrets_ldb, secrets_ldb, messagefunc):
468 """Update secrets.ldb
470 :param newsecrets_ldb: An LDB object that is connected to the secrets.ldb
471 of the reference provision
472 :param secrets_ldb: An LDB object that is connected to the secrets.ldb
473 of the updated provision
476 messagefunc(SIMPLE, "update secrets.ldb")
477 reference = newsecrets_ldb.search(expression="dn=@MODULES", base="",
479 current = secrets_ldb.search(expression="dn=@MODULES", base="",
481 assert reference, "Reference modules list can not be empty"
482 if len(current) == 0:
484 delta = secrets_ldb.msg_diff(ldb.Message(), reference[0])
485 delta.dn = reference[0].dn
486 secrets_ldb.add(reference[0])
488 delta = secrets_ldb.msg_diff(current[0], reference[0])
489 delta.dn = current[0].dn
490 secrets_ldb.modify(delta)
492 reference = newsecrets_ldb.search(expression="objectClass=top", base="",
493 scope=SCOPE_SUBTREE, attrs=["dn"])
494 current = secrets_ldb.search(expression="objectClass=top", base="",
495 scope=SCOPE_SUBTREE, attrs=["dn"])
501 empty = ldb.Message()
502 for i in range(0, len(reference)):
503 hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"]
505 # Create a hash for speeding the search of existing object in the
507 for i in range(0, len(current)):
508 hash[str(current[i]["dn"]).lower()] = current[i]["dn"]
510 for k in hash_new.keys():
511 if not hash.has_key(k):
512 listMissing.append(hash_new[k])
514 listPresent.append(hash_new[k])
516 for entry in listMissing:
517 reference = newsecrets_ldb.search(expression="dn=%s" % entry,
518 base="", scope=SCOPE_SUBTREE)
519 current = secrets_ldb.search(expression="dn=%s" % entry,
520 base="", scope=SCOPE_SUBTREE)
521 delta = secrets_ldb.msg_diff(empty, reference[0])
522 for att in hashAttrNotCopied.keys():
524 messagefunc(CHANGE, "Entry %s is missing from secrets.ldb" % reference[0].dn)
526 messagefunc(CHANGE, " Adding attribute %s" % att)
527 delta.dn = reference[0].dn
528 secrets_ldb.add(delta)
530 for entry in listPresent:
531 reference = newsecrets_ldb.search(expression="dn=%s" % entry,
532 base="", scope=SCOPE_SUBTREE)
533 current = secrets_ldb.search(expression="dn=%s" % entry, base="",
535 delta = secrets_ldb.msg_diff(current[0], reference[0])
536 for att in hashAttrNotCopied.keys():
540 messagefunc(CHANGE, "Found attribute name on %s," \
541 " must rename the DN" % (current[0].dn))
542 identic_rename(secrets_ldb, reference[0].dn)
546 for entry in listPresent:
547 reference = newsecrets_ldb.search(expression="dn=%s" % entry, base="",
549 current = secrets_ldb.search(expression="dn=%s" % entry, base="",
551 delta = secrets_ldb.msg_diff(current[0], reference[0])
552 for att in hashAttrNotCopied.keys():
557 "Adding/Changing attribute %s to %s" % (att, current[0].dn))
559 delta.dn = current[0].dn
560 secrets_ldb.modify(delta)
562 def getOEMInfo(samdb, rootdn):
563 """Return OEM Information on the top level
564 Samba4 use to store version info in this field
566 :param samdb: An LDB object connect to sam.ldb
567 :param rootdn: Root DN of the domain
568 :return: The content of the field oEMInformation (if any)"""
569 res = samdb.search(expression="(objectClass=*)", base=str(rootdn),
570 scope=SCOPE_BASE, attrs=["dn", "oEMInformation"])
572 info = res[0]["oEMInformation"]
577 def updateOEMInfo(samdb, rootdn):
578 """Update the OEMinfo field to add information about upgrade
579 :param samdb: an LDB object connected to the sam DB
580 :param rootdn: The string representation of the root DN of
581 the provision (ie. DC=...,DC=...)
583 res = samdb.search(expression="(objectClass=*)", base=rootdn,
584 scope=SCOPE_BASE, attrs=["dn", "oEMInformation"])
586 info = res[0]["oEMInformation"]
587 info = "%s, upgrade to %s" % (info, version)
588 delta = ldb.Message()
589 delta.dn = ldb.Dn(samdb, str(res[0]["dn"]))
590 delta["oEMInformation"] = ldb.MessageElement(info, ldb.FLAG_MOD_REPLACE,
594 def update_gpo(paths, samdb, names, lp, message, force=0):
595 """Create missing GPO file object if needed
597 Set ACL correctly also.
598 Check ACLs for sysvol/netlogon dirs also
602 ntacls.checkset_backend(lp, None, None)
603 eadbname = lp.get("posix:eadb")
604 if eadbname is not None and eadbname != "":
606 attribute = samba.xattr_tdb.wrap_getxattr(eadbname, paths.sysvol,
607 xattr.XATTR_NTACL_NAME)
609 attribute = samba.xattr_native.wrap_getxattr(paths.sysvol,
610 xattr.XATTR_NTACL_NAME)
612 attribute = samba.xattr_native.wrap_getxattr(paths.sysvol,
613 xattr.XATTR_NTACL_NAME)
620 dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid)
621 if not os.path.isdir(dir):
622 create_gpo_struct(dir)
624 dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid_dc)
625 if not os.path.isdir(dir):
626 create_gpo_struct(dir)
627 # We always reinforce acls on GPO folder because they have to be in sync
629 set_gpo_acl(paths.sysvol, names.dnsdomain, names.domainsid,
630 names.domaindn, samdb, lp)
633 setsysvolacl(samdb, paths.netlogon, paths.sysvol, names.wheel_gid,
634 names.domainsid, names.dnsdomain, names.domaindn, lp)
636 def delta_update_basesamdb(refsam, sam, creds, session, lp, message):
637 """Update the provision container db: sam.ldb
638 This function is aimed for alpha9 and newer;
640 :param refsam: Path to the samdb in the reference provision
641 :param sam: Path to the samdb in the upgraded provision
642 :param creds: Credential used for openning LDB files
643 :param session: Session to use for openning LDB files
644 :param lp: A loadparam object"""
647 "Update base samdb by searching difference with reference one")
648 refsam = Ldb(refsam, session_info=session, credentials=creds,
649 lp=lp, options=["modules:"])
650 sam = Ldb(sam, session_info=session, credentials=creds, lp=lp,
651 options=["modules:"])
653 empty = ldb.Message()
655 reference = refsam.search(expression="")
657 for refentry in reference:
658 entry = sam.search(expression="dn=%s" % refentry["dn"],
661 delta = sam.msg_diff(empty, refentry)
662 message(CHANGE, "Adding %s to sam db" % str(refentry.dn))
663 if str(refentry.dn) == "@PROVISION" and\
664 delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
665 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
666 delta.dn = refentry.dn
669 delta = sam.msg_diff(entry[0], refentry)
670 if str(refentry.dn) == "@PROVISION" and\
671 delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
672 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
673 if len(delta.items()) > 1:
674 delta.dn = refentry.dn