s4-upgradeprovision: renames need a relax to avoid subtree_rename checks
[samba.git] / source4 / scripting / python / samba / upgradehelpers.py
1 #!/usr/bin/env python
2 #
3 # Helpers for provision stuff
4 # Copyright (C) Matthieu Patou <mat@matws.net> 2009-2010
5 #
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
9 #
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
24 """Helers used for upgrading between different database formats."""
25
26 import os
27 import string
28 import re
29 import shutil
30 import samba
31
32 from samba import Ldb, version, ntacls
33 from samba.dsdb import DS_DOMAIN_FUNCTION_2000
34 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE
35 import ldb
36 from samba.provision import (ProvisionNames, provision_paths_from_lp,
37                             getpolicypath, set_gpos_acl, create_gpo_struct,
38                             FILL_FULL, provision, ProvisioningError,
39                             setsysvolacl, secretsdb_self_join)
40 from samba.dcerpc import misc, security, xattr
41 from samba.dcerpc.misc import SEC_CHAN_BDC
42 from samba.ndr import ndr_unpack
43 from samba.samdb import SamDB
44
45 # All the ldb related to registry are commented because the path for them is
46 # relative in the provisionPath object
47 # And so opening them create a file in the current directory which is not what
48 # we want
49 # I still keep them commented because I plan soon to make more cleaner
50 ERROR =     -1
51 SIMPLE =     0x00
52 CHANGE =     0x01
53 CHANGESD =     0x02
54 GUESS =     0x04
55 PROVISION =    0x08
56 CHANGEALL =    0xff
57
58 hashAttrNotCopied = set(["dn", "whenCreated", "whenChanged", "objectGUID",
59     "uSNCreated", "replPropertyMetaData", "uSNChanged", "parentGUID",
60     "objectCategory", "distinguishedName", "nTMixedDomain",
61     "showInAdvancedViewOnly", "instanceType", "msDS-Behavior-Version",
62     "nextRid", "cn", "versionNumber", "lmPwdHistory", "pwdLastSet",
63     "ntPwdHistory", "unicodePwd","dBCSPwd", "supplementalCredentials",
64     "gPCUserExtensionNames", "gPCMachineExtensionNames","maxPwdAge", "secret",
65     "possibleInferiors", "privilege", "sAMAccountType"])
66
67
68 class ProvisionLDB(object):
69
70     def __init__(self):
71         self.sam = None
72         self.secrets = None
73         self.idmap = None
74         self.privilege = None
75         self.hkcr = None
76         self.hkcu = None
77         self.hku = None
78         self.hklm = None
79
80     def startTransactions(self):
81         self.sam.transaction_start()
82         self.secrets.transaction_start()
83         self.idmap.transaction_start()
84         self.privilege.transaction_start()
85 # TO BE DONE
86 #        self.hkcr.transaction_start()
87 #        self.hkcu.transaction_start()
88 #        self.hku.transaction_start()
89 #        self.hklm.transaction_start()
90
91     def groupedRollback(self):
92         ok = True
93         try:
94             self.sam.transaction_cancel()
95         except Exception:
96             ok = False
97
98         try:
99             self.secrets.transaction_cancel()
100         except Exception:
101             ok = False
102
103         try:
104             self.idmap.transaction_cancel()
105         except Exception:
106             ok = False
107
108         try:
109             self.privilege.transaction_cancel()
110         except Exception:
111             ok = False
112
113         return ok
114 # TO BE DONE
115 #        self.hkcr.transaction_cancel()
116 #        self.hkcu.transaction_cancel()
117 #        self.hku.transaction_cancel()
118 #        self.hklm.transaction_cancel()
119
120     def groupedCommit(self):
121         try:
122             self.sam.transaction_prepare_commit()
123             self.secrets.transaction_prepare_commit()
124             self.idmap.transaction_prepare_commit()
125             self.privilege.transaction_prepare_commit()
126         except Exception:
127             return self.groupedRollback()
128 # TO BE DONE
129 #        self.hkcr.transaction_prepare_commit()
130 #        self.hkcu.transaction_prepare_commit()
131 #        self.hku.transaction_prepare_commit()
132 #        self.hklm.transaction_prepare_commit()
133         try:
134             self.sam.transaction_commit()
135             self.secrets.transaction_commit()
136             self.idmap.transaction_commit()
137             self.privilege.transaction_commit()
138         except Exception:
139             return self.groupedRollback()
140
141 # TO BE DONE
142 #        self.hkcr.transaction_commit()
143 #        self.hkcu.transaction_commit()
144 #        self.hku.transaction_commit()
145 #        self.hklm.transaction_commit()
146         return True
147
148 def get_ldbs(paths, creds, session, lp):
149     """Return LDB object mapped on most important databases
150
151     :param paths: An object holding the different importants paths for provision object
152     :param creds: Credential used for openning LDB files
153     :param session: Session to use for openning LDB files
154     :param lp: A loadparam object
155     :return: A ProvisionLDB object that contains LDB object for the different LDB files of the provision"""
156
157     ldbs = ProvisionLDB()
158
159     ldbs.sam = SamDB(paths.samdb, session_info=session, credentials=creds, lp=lp, options=["modules:samba_dsdb"])
160     ldbs.secrets = Ldb(paths.secrets, session_info=session, credentials=creds, lp=lp)
161     ldbs.idmap = Ldb(paths.idmapdb, session_info=session, credentials=creds, lp=lp)
162     ldbs.privilege = Ldb(paths.privilege, session_info=session, credentials=creds, lp=lp)
163 #    ldbs.hkcr = Ldb(paths.hkcr, session_info=session, credentials=creds, lp=lp)
164 #    ldbs.hkcu = Ldb(paths.hkcu, session_info=session, credentials=creds, lp=lp)
165 #    ldbs.hku = Ldb(paths.hku, session_info=session, credentials=creds, lp=lp)
166 #    ldbs.hklm = Ldb(paths.hklm, session_info=session, credentials=creds, lp=lp)
167
168     return ldbs
169
170
171 def usn_in_range(usn, range):
172     """Check if the usn is in one of the range provided.
173     To do so, the value is checked to be between the lower bound and
174     higher bound of a range
175
176     :param usn: A integer value corresponding to the usn that we want to update
177     :param range: A list of integer representing ranges, lower bounds are in
178                   the even indices, higher in odd indices
179     :return: True if the usn is in one of the range, False otherwise
180     """
181
182     idx = 0
183     cont = True
184     ok = False
185     while cont:
186         if idx ==  len(range):
187             cont = False
188             continue
189         if usn < int(range[idx]):
190             if idx %2 == 1:
191                 ok = True
192             cont = False
193         if usn == int(range[idx]):
194             cont = False
195             ok = True
196         idx = idx + 1
197     return ok
198
199
200 def get_paths(param, targetdir=None, smbconf=None):
201     """Get paths to important provision objects (smb.conf, ldb files, ...)
202
203     :param param: Param object
204     :param targetdir: Directory where the provision is (or will be) stored
205     :param smbconf: Path to the smb.conf file
206     :return: A list with the path of important provision objects"""
207     if targetdir is not None:
208         etcdir = os.path.join(targetdir, "etc")
209         if not os.path.exists(etcdir):
210             os.makedirs(etcdir)
211         smbconf = os.path.join(etcdir, "smb.conf")
212     if smbconf is None:
213         smbconf = param.default_path()
214
215     if not os.path.exists(smbconf):
216         raise ProvisioningError("Unable to find smb.conf")
217
218     lp = param.LoadParm()
219     lp.load(smbconf)
220     paths = provision_paths_from_lp(lp, lp.get("realm"))
221     return paths
222
223 def update_policyids(names, samdb):
224     """Update policy ids that could have changed after sam update
225
226     :param names: List of key provision parameters
227     :param samdb: An Ldb object conntected with the sam DB
228     """
229     # policy guid
230     res = samdb.search(expression="(displayName=Default Domain Policy)",
231                         base="CN=Policies,CN=System," + str(names.rootdn),
232                         scope=SCOPE_ONELEVEL, attrs=["cn","displayName"])
233     names.policyid = str(res[0]["cn"]).replace("{","").replace("}","")
234     # dc policy guid
235     res2 = samdb.search(expression="(displayName=Default Domain Controllers"
236                                    " Policy)",
237                             base="CN=Policies,CN=System," + str(names.rootdn),
238                             scope=SCOPE_ONELEVEL, attrs=["cn","displayName"])
239     if len(res2) == 1:
240         names.policyid_dc = str(res2[0]["cn"]).replace("{","").replace("}","")
241     else:
242         names.policyid_dc = None
243
244
245 def find_provision_key_parameters(samdb, secretsdb, idmapdb, paths, smbconf, lp):
246     """Get key provision parameters (realm, domain, ...) from a given provision
247
248     :param samdb: An LDB object connected to the sam.ldb file
249     :param secretsdb: An LDB object connected to the secrets.ldb file
250     :param idmapdb: An LDB object connected to the idmap.ldb file
251     :param paths: A list of path to provision object
252     :param smbconf: Path to the smb.conf file
253     :param lp: A LoadParm object
254     :return: A list of key provision parameters
255     """
256     names = ProvisionNames()
257     names.adminpass = None
258
259     # NT domain, kerberos realm, root dn, domain dn, domain dns name
260     names.domain = string.upper(lp.get("workgroup"))
261     names.realm = lp.get("realm")
262     basedn = "DC=" + names.realm.replace(".",",DC=")
263     names.dnsdomain = names.realm.lower()
264     names.realm = string.upper(names.realm)
265     # netbiosname
266     # Get the netbiosname first (could be obtained from smb.conf in theory)
267     res = secretsdb.search(expression="(flatname=%s)" %
268                             names.domain,base="CN=Primary Domains",
269                             scope=SCOPE_SUBTREE, attrs=["sAMAccountName"])
270     names.netbiosname = str(res[0]["sAMAccountName"]).replace("$","")
271
272     names.smbconf = smbconf
273
274     # That's a bit simplistic but it's ok as long as we have only 3
275     # partitions
276     current = samdb.search(expression="(objectClass=*)", 
277         base="", scope=SCOPE_BASE,
278         attrs=["defaultNamingContext", "schemaNamingContext",
279                "configurationNamingContext","rootDomainNamingContext"])
280
281     names.configdn = current[0]["configurationNamingContext"]
282     configdn = str(names.configdn)
283     names.schemadn = current[0]["schemaNamingContext"]
284     if not (ldb.Dn(samdb, basedn) == (ldb.Dn(samdb,
285                                        current[0]["defaultNamingContext"][0]))):
286         raise ProvisioningError(("basedn in %s (%s) and from %s (%s)"
287                                  "is not the same ..." % (paths.samdb,
288                                     str(current[0]["defaultNamingContext"][0]),
289                                     paths.smbconf, basedn)))
290
291     names.domaindn=current[0]["defaultNamingContext"]
292     names.rootdn=current[0]["rootDomainNamingContext"]
293     # default site name
294     res3 = samdb.search(expression="(objectClass=*)", 
295         base="CN=Sites," + configdn, scope=SCOPE_ONELEVEL, attrs=["cn"])
296     names.sitename = str(res3[0]["cn"])
297
298     # dns hostname and server dn
299     res4 = samdb.search(expression="(CN=%s)" % names.netbiosname,
300                             base="OU=Domain Controllers,%s" % basedn,
301                             scope=SCOPE_ONELEVEL, attrs=["dNSHostName"])
302     names.hostname = str(res4[0]["dNSHostName"]).replace("." + names.dnsdomain,"")
303
304     server_res = samdb.search(expression="serverReference=%s" % res4[0].dn,
305                                 attrs=[], base=configdn)
306     names.serverdn = server_res[0].dn
307
308     # invocation id/objectguid
309     res5 = samdb.search(expression="(objectClass=*)",
310             base="CN=NTDS Settings,%s" % str(names.serverdn), scope=SCOPE_BASE,
311             attrs=["invocationID", "objectGUID"])
312     names.invocation = str(ndr_unpack(misc.GUID, res5[0]["invocationId"][0]))
313     names.ntdsguid = str(ndr_unpack(misc.GUID, res5[0]["objectGUID"][0]))
314
315     # domain guid/sid
316     res6 = samdb.search(expression="(objectClass=*)", base=basedn,
317             scope=SCOPE_BASE, attrs=["objectGUID",
318                 "objectSid","msDS-Behavior-Version" ])
319     names.domainguid = str(ndr_unpack(misc.GUID, res6[0]["objectGUID"][0]))
320     names.domainsid = ndr_unpack( security.dom_sid, res6[0]["objectSid"][0])
321     if res6[0].get("msDS-Behavior-Version") is None or \
322         int(res6[0]["msDS-Behavior-Version"][0]) < DS_DOMAIN_FUNCTION_2000:
323         names.domainlevel = DS_DOMAIN_FUNCTION_2000
324     else:
325         names.domainlevel = int(res6[0]["msDS-Behavior-Version"][0])
326
327     # policy guid
328     res7 = samdb.search(expression="(displayName=Default Domain Policy)",
329                         base="CN=Policies,CN=System," + basedn,
330                         scope=SCOPE_ONELEVEL, attrs=["cn","displayName"])
331     names.policyid = str(res7[0]["cn"]).replace("{","").replace("}","")
332     # dc policy guid
333     res8 = samdb.search(expression="(displayName=Default Domain Controllers"
334                                    " Policy)",
335                             base="CN=Policies,CN=System," + basedn,
336                             scope=SCOPE_ONELEVEL, attrs=["cn","displayName"])
337     if len(res8) == 1:
338         names.policyid_dc = str(res8[0]["cn"]).replace("{","").replace("}","")
339     else:
340         names.policyid_dc = None
341     res9 = idmapdb.search(expression="(cn=%s)" %
342                             (security.SID_BUILTIN_ADMINISTRATORS),
343                             attrs=["xidNumber"])
344     if len(res9) == 1:
345         names.wheel_gid = res9[0]["xidNumber"]
346     else:
347         raise ProvisioningError("Unable to find uid/gid for Domain Admins rid")
348     return names
349
350
351 def newprovision(names, setup_dir, creds, session, smbconf, provdir, logger):
352     """Create a new provision.
353
354     This provision will be the reference for knowing what has changed in the
355     since the latest upgrade in the current provision
356
357     :param names: List of provision parameters
358     :param setup_dir: Directory where the setup files are stored
359     :param creds: Credentials for the authentification
360     :param session: Session object
361     :param smbconf: Path to the smb.conf file
362     :param provdir: Directory where the provision will be stored
363     :param logger: A Logger
364     """
365     if os.path.isdir(provdir):
366         shutil.rmtree(provdir)
367     os.mkdir(provdir)
368     logger.info("Provision stored in %s", provdir)
369     provision(setup_dir, logger, session, creds, smbconf=smbconf,
370             targetdir=provdir, samdb_fill=FILL_FULL, realm=names.realm,
371             domain=names.domain, domainguid=names.domainguid,
372             domainsid=str(names.domainsid), ntdsguid=names.ntdsguid,
373             policyguid=names.policyid, policyguid_dc=names.policyid_dc,
374             hostname=names.netbiosname.lower(), hostip=None, hostip6=None,
375             invocationid=names.invocation, adminpass=names.adminpass,
376             krbtgtpass=None, machinepass=None, dnspass=None, root=None,
377             nobody=None, wheel=None, users=None,
378             serverrole="domain controller", ldap_backend_extra_port=None,
379             backend_type=None, ldapadminpass=None, ol_mmr_urls=None,
380             slapd_path=None, setup_ds_path=None, nosync=None,
381             dom_for_fun_level=names.domainlevel,
382             ldap_dryrun_mode=None, useeadb=True)
383
384
385 def dn_sort(x, y):
386     """Sorts two DNs in the lexicographical order it and put higher level DN
387     before.
388
389     So given the dns cn=bar,cn=foo and cn=foo the later will be return as
390     smaller
391
392     :param x: First object to compare
393     :param y: Second object to compare
394     """
395     p = re.compile(r'(?<!\\), ?')
396     tab1 = p.split(str(x))
397     tab2 = p.split(str(y))
398     minimum = min(len(tab1), len(tab2))
399     len1 = len(tab1)-1
400     len2 = len(tab2)-1
401     # Note: python range go up to upper limit but do not include it
402     for i in range(0, minimum):
403         ret = cmp(tab1[len1-i], tab2[len2-i])
404         if ret != 0:
405             return ret
406         else:
407             if i == minimum-1:
408                 assert len1!=len2,"PB PB PB" + " ".join(tab1)+" / " + " ".join(tab2)
409                 if len1 > len2:
410                     return 1
411                 else:
412                     return -1
413     return ret
414
415
416 def identic_rename(ldbobj, dn):
417     """Perform a back and forth rename to trigger renaming on attribute that
418     can't be directly modified.
419
420     :param lbdobj: An Ldb Object
421     :param dn: DN of the object to manipulate
422     """
423     (before, after) = str(dn).split('=', 1)
424     # we need to use relax to avoid the subtree_rename constraints
425     ldbobj.rename(dn, ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), ["relax:0"])
426     ldbobj.rename(ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), dn, ["relax:0"])
427
428
429 def chunck_acl(acl):
430     """Return separate ACE of an ACL
431
432     :param acl: A string representing the ACL
433     :return: A hash with different parts
434     """
435
436     p = re.compile(r'(\w+)?(\(.*?\))')
437     tab = p.findall(acl)
438
439     hash = {}
440     hash["aces"] = []
441     for e in tab:
442         if len(e[0]) > 0:
443             hash["flags"] = e[0]
444         hash["aces"].append(e[1])
445
446     return hash
447
448
449 def chunck_sddl(sddl):
450     """ Return separate parts of the SDDL (owner, group, ...)
451
452     :param sddl: An string containing the SDDL to chunk
453     :return: A hash with the different chunk
454     """
455
456     p = re.compile(r'([OGDS]:)(.*?)(?=(?:[GDS]:|$))')
457     tab = p.findall(sddl)
458
459     hash = {}
460     for e in tab:
461         if e[0] == "O:":
462             hash["owner"] = e[1]
463         if e[0] == "G:":
464             hash["group"] = e[1]
465         if e[0] == "D:":
466             hash["dacl"] = e[1]
467         if e[0] == "S:":
468             hash["sacl"] = e[1]
469
470     return hash
471
472
473 def get_diff_sddls(refsddl, cursddl):
474     """Get the difference between 2 sddl
475
476     This function split the textual representation of ACL into smaller
477     chunck in order to not to report a simple permutation as a difference
478
479     :param refsddl: First sddl to compare
480     :param cursddl: Second sddl to compare
481     :return: A string that explain difference between sddls
482     """
483
484     txt = ""
485     hash_new = chunck_sddl(cursddl)
486     hash_ref = chunck_sddl(refsddl)
487
488     if hash_new["owner"] != hash_ref["owner"]:
489         txt = "\tOwner mismatch: %s (in ref) %s" \
490               "(in current)\n" % (hash_ref["owner"], hash_new["owner"])
491
492     if hash_new["group"] != hash_ref["group"]:
493         txt = "%s\tGroup mismatch: %s (in ref) %s" \
494               "(in current)\n" % (txt, hash_ref["group"], hash_new["group"])
495
496     for part in ["dacl", "sacl"]:
497         if hash_new.has_key(part) and hash_ref.has_key(part):
498
499             # both are present, check if they contain the same ACE
500             h_new = set()
501             h_ref = set()
502             c_new = chunck_acl(hash_new[part])
503             c_ref = chunck_acl(hash_ref[part])
504
505             for elem in c_new["aces"]:
506                 h_new.add(elem)
507
508             for elem in c_ref["aces"]:
509                 h_ref.add(elem)
510
511             for k in set(h_ref):
512                 if k in h_new:
513                     h_new.remove(k)
514                     h_ref.remove(k)
515
516             if len(h_new) + len(h_ref) > 0:
517                 txt = "%s\tPart %s is different between reference" \
518                       " and current here is the detail:\n" % (txt, part)
519
520                 for item in h_new:
521                     txt = "%s\t\t%s ACE is not present in the" \
522                           " reference\n" % (txt, item)
523
524                 for item in h_ref:
525                     txt = "%s\t\t%s ACE is not present in the" \
526                           " current\n" % (txt, item)
527
528         elif hash_new.has_key(part) and not hash_ref.has_key(part):
529             txt = "%s\tReference ACL hasn't a %s part\n" % (txt, part)
530         elif not hash_new.has_key(part) and hash_ref.has_key(part):
531             txt = "%s\tCurrent ACL hasn't a %s part\n" % (txt, part)
532
533     return txt
534
535
536 def update_secrets(newsecrets_ldb, secrets_ldb, messagefunc):
537     """Update secrets.ldb
538
539     :param newsecrets_ldb: An LDB object that is connected to the secrets.ldb
540         of the reference provision
541     :param secrets_ldb: An LDB object that is connected to the secrets.ldb
542         of the updated provision
543     """
544
545     messagefunc(SIMPLE, "update secrets.ldb")
546     reference = newsecrets_ldb.search(expression="dn=@MODULES", base="",
547                                         scope=SCOPE_SUBTREE)
548     current = secrets_ldb.search(expression="dn=@MODULES", base="",
549                                         scope=SCOPE_SUBTREE)
550     assert reference, "Reference modules list can not be empty"
551     if len(current) == 0:
552         # No modules present
553         delta = secrets_ldb.msg_diff(ldb.Message(), reference[0])
554         delta.dn = reference[0].dn
555         secrets_ldb.add(reference[0])
556     else:
557         delta = secrets_ldb.msg_diff(current[0], reference[0])
558         delta.dn = current[0].dn
559         secrets_ldb.modify(delta)
560
561     reference = newsecrets_ldb.search(expression="objectClass=top", base="",
562                                         scope=SCOPE_SUBTREE, attrs=["dn"])
563     current = secrets_ldb.search(expression="objectClass=top", base="",
564                                         scope=SCOPE_SUBTREE, attrs=["dn"])
565     hash_new = {}
566     hash = {}
567     listMissing = []
568     listPresent = []
569
570     empty = ldb.Message()
571     for i in range(0, len(reference)):
572         hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"]
573
574     # Create a hash for speeding the search of existing object in the
575     # current provision
576     for i in range(0, len(current)):
577         hash[str(current[i]["dn"]).lower()] = current[i]["dn"]
578
579     for k in hash_new.keys():
580         if not hash.has_key(k):
581             listMissing.append(hash_new[k])
582         else:
583             listPresent.append(hash_new[k])
584
585     for entry in listMissing:
586         reference = newsecrets_ldb.search(expression="dn=%s" % entry,
587                                             base="", scope=SCOPE_SUBTREE)
588         current = secrets_ldb.search(expression="dn=%s" % entry,
589                                             base="", scope=SCOPE_SUBTREE)
590         delta = secrets_ldb.msg_diff(empty, reference[0])
591         for att in hashAttrNotCopied:
592             delta.remove(att)
593         messagefunc(CHANGE, "Entry %s is missing from secrets.ldb" %
594                     reference[0].dn)
595         for att in delta:
596             messagefunc(CHANGE, " Adding attribute %s" % att)
597         delta.dn = reference[0].dn
598         secrets_ldb.add(delta)
599
600     for entry in listPresent:
601         reference = newsecrets_ldb.search(expression="dn=%s" % entry,
602                                             base="", scope=SCOPE_SUBTREE)
603         current = secrets_ldb.search(expression="dn=%s" % entry, base="",
604                                             scope=SCOPE_SUBTREE)
605         delta = secrets_ldb.msg_diff(current[0], reference[0])
606         for att in hashAttrNotCopied:
607             delta.remove(att)
608         for att in delta:
609             if att == "name":
610                 messagefunc(CHANGE, "Found attribute name on  %s,"
611                                     " must rename the DN" % (current[0].dn))
612                 identic_rename(secrets_ldb, reference[0].dn)
613             else:
614                 delta.remove(att)
615
616     for entry in listPresent:
617         reference = newsecrets_ldb.search(expression="dn=%s" % entry, base="",
618                                             scope=SCOPE_SUBTREE)
619         current = secrets_ldb.search(expression="dn=%s" % entry, base="",
620                                             scope=SCOPE_SUBTREE)
621         delta = secrets_ldb.msg_diff(current[0], reference[0])
622         for att in hashAttrNotCopied:
623             delta.remove(att)
624         for att in delta:
625             if att == "msDS-KeyVersionNumber":
626                 delta.remove(att)
627             if att != "dn":
628                 messagefunc(CHANGE,
629                             "Adding/Changing attribute %s to %s" %
630                             (att, current[0].dn))
631
632         delta.dn = current[0].dn
633         secrets_ldb.modify(delta)
634
635     res2 = secrets_ldb.search(expression="(samaccountname=dns)",
636                                 scope=SCOPE_SUBTREE, attrs=["dn"])
637
638     if (len(res2) == 1):
639             messagefunc(SIMPLE, "Remove old dns account")
640             secrets_ldb.delete(res2[0]["dn"])
641
642
643 def getOEMInfo(samdb, rootdn):
644     """Return OEM Information on the top level Samba4 use to store version
645     info in this field
646
647     :param samdb: An LDB object connect to sam.ldb
648     :param rootdn: Root DN of the domain
649     :return: The content of the field oEMInformation (if any)
650     """
651     res = samdb.search(expression="(objectClass=*)", base=str(rootdn),
652                             scope=SCOPE_BASE, attrs=["dn", "oEMInformation"])
653     if len(res) > 0:
654         info = res[0]["oEMInformation"]
655         return info
656     else:
657         return ""
658
659
660 def updateOEMInfo(samdb, rootdn):
661     """Update the OEMinfo field to add information about upgrade
662
663     :param samdb: an LDB object connected to the sam DB
664     :param rootdn: The string representation of the root DN of
665         the provision (ie. DC=...,DC=...)
666     """
667     res = samdb.search(expression="(objectClass=*)", base=rootdn,
668                             scope=SCOPE_BASE, attrs=["dn", "oEMInformation"])
669     if len(res) > 0:
670         info = res[0]["oEMInformation"]
671         info = "%s, upgrade to %s" % (info, version)
672         delta = ldb.Message()
673         delta.dn = ldb.Dn(samdb, str(res[0]["dn"]))
674         delta["oEMInformation"] = ldb.MessageElement(info, ldb.FLAG_MOD_REPLACE,
675                                                         "oEMInformation" )
676         samdb.modify(delta)
677
678 def update_gpo(paths, samdb, names, lp, message, force=0):
679     """Create missing GPO file object if needed
680
681     Set ACL correctly also.
682     Check ACLs for sysvol/netlogon dirs also
683     """
684     resetacls = False
685     try:
686         ntacls.checkset_backend(lp, None, None)
687         eadbname = lp.get("posix:eadb")
688         if eadbname is not None and eadbname != "":
689             try:
690                 attribute = samba.xattr_tdb.wrap_getxattr(eadbname,
691                                 paths.sysvol, xattr.XATTR_NTACL_NAME)
692             except Exception:
693                 attribute = samba.xattr_native.wrap_getxattr(paths.sysvol,
694                                 xattr.XATTR_NTACL_NAME)
695         else:
696             attribute = samba.xattr_native.wrap_getxattr(paths.sysvol,
697                                 xattr.XATTR_NTACL_NAME)
698     except Exception:
699        resetacls = True
700
701     if force:
702         resetacls = True
703
704     dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid)
705     if not os.path.isdir(dir):
706         create_gpo_struct(dir)
707
708     if names.policyid_dc is None:
709         raise ProvisioningError("Policy ID for Domain controller is missing")
710     dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid_dc)
711     if not os.path.isdir(dir):
712         create_gpo_struct(dir)
713     # We always reinforce acls on GPO folder because they have to be in sync
714     # with the one in DS
715     try:
716         set_gpos_acl(paths.sysvol, names.dnsdomain, names.domainsid,
717             names.domaindn, samdb, lp)
718     except TypeError, e:
719         message(ERROR, "Unable to set ACLs on policies related objects,"
720                        " if not using posix:eadb, you must be root to do it")
721
722     if resetacls:
723        try:
724             setsysvolacl(samdb, paths.netlogon, paths.sysvol, names.wheel_gid,
725                         names.domainsid, names.dnsdomain, names.domaindn, lp)
726        except TypeError, e:
727             message(ERROR, "Unable to set ACLs on sysvol share, if not using"
728                            "posix:eadb, you must be root to do it")
729
730 def increment_calculated_keyversion_number(samdb, rootdn, hashDns):
731     """For a given hash associating dn and a number, this function will
732     update the replPropertyMetaData of each dn in the hash, so that the
733     calculated value of the msDs-KeyVersionNumber is equal or superior to the
734     one associated to the given dn.
735
736     :param samdb: An SamDB object pointing to the sam
737     :param rootdn: The base DN where we want to start
738     :param hashDns: A hash with dn as key and number representing the
739                  minimum value of msDs-KeyVersionNumber that we want to
740                  have
741     """
742     entry = samdb.search(expression='(objectClass=user)',
743                          base=ldb.Dn(samdb,str(rootdn)),
744                          scope=SCOPE_SUBTREE, attrs=["msDs-KeyVersionNumber"],
745                          controls=["search_options:1:2"])
746     done = 0
747     hashDone = {}
748     if len(entry) == 0:
749         raise ProvisioningError("Unable to find msDs-KeyVersionNumber")
750     else:
751         for e in entry:
752             if hashDns.has_key(str(e.dn).lower()):
753                 val = e.get("msDs-KeyVersionNumber")
754                 if not val:
755                     val = "0"
756                 version = int(str(hashDns[str(e.dn).lower()]))
757                 if int(str(val)) < version:
758                     done = done + 1
759                     samdb.set_attribute_replmetadata_version(str(e.dn),
760                                                               "unicodePwd",
761                                                               version, True)
762 def delta_update_basesamdb(refsampath, sampath, creds, session, lp, message):
763     """Update the provision container db: sam.ldb
764     This function is aimed for alpha9 and newer;
765
766     :param refsampath: Path to the samdb in the reference provision
767     :param sampath: Path to the samdb in the upgraded provision
768     :param creds: Credential used for openning LDB files
769     :param session: Session to use for openning LDB files
770     :param lp: A loadparam object
771     :return: A msg_diff object with the difference between the @ATTRIBUTES
772              of the current provision and the reference provision
773     """
774
775     message(SIMPLE,
776             "Update base samdb by searching difference with reference one")
777     refsam = Ldb(refsampath, session_info=session, credentials=creds,
778                     lp=lp, options=["modules:"])
779     sam = Ldb(sampath, session_info=session, credentials=creds, lp=lp,
780                 options=["modules:"])
781
782     empty = ldb.Message()
783     deltaattr = None
784     reference = refsam.search(expression="")
785
786     for refentry in reference:
787         entry = sam.search(expression="dn=%s" % refentry["dn"],
788                             scope=SCOPE_SUBTREE)
789         if not len(entry):
790             delta = sam.msg_diff(empty, refentry)
791             message(CHANGE, "Adding %s to sam db" % str(refentry.dn))
792             if str(refentry.dn) == "@PROVISION" and\
793                 delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
794                 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
795             delta.dn = refentry.dn
796             sam.add(delta)
797         else:
798             delta = sam.msg_diff(entry[0], refentry)
799             if str(refentry.dn) == "@ATTRIBUTES":
800                 deltaattr = sam.msg_diff(refentry, entry[0])
801             if str(refentry.dn) == "@PROVISION" and\
802                 delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
803                 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
804             if len(delta.items()) > 1:
805                 delta.dn = refentry.dn
806                 sam.modify(delta)
807
808     return deltaattr
809
810
811 def construct_existor_expr(attrs):
812     """Construct a exists or LDAP search expression.
813
814     :param attrs: List of attribute on which we want to create the search
815         expression.
816     :return: A string representing the expression, if attrs is empty an
817         empty string is returned
818     """
819     expr = ""
820     if len(attrs) > 0:
821         expr = "(|"
822         for att in attrs:
823             expr = "%s(%s=*)"%(expr,att)
824         expr = "%s)"%expr
825     return expr
826
827 def update_machine_account_password(samdb, secrets_ldb, names):
828     """Update (change) the password of the current DC both in the SAM db and in
829        secret one
830
831     :param samdb: An LDB object related to the sam.ldb file of a given provision
832     :param secrets_ldb: An LDB object related to the secrets.ldb file of a given
833                         provision
834     :param names: List of key provision parameters"""
835
836     expression = "samAccountName=%s$" % names.netbiosname
837     secrets_msg = secrets_ldb.search(expression=expression,
838                                         attrs=["secureChannelType"])
839     if int(secrets_msg[0]["secureChannelType"][0]) == SEC_CHAN_BDC:
840         res = samdb.search(expression=expression, attrs=[])
841         assert(len(res) == 1)
842
843         msg = ldb.Message(res[0].dn)
844         machinepass = samba.generate_random_password(128, 255)
845         mputf16 = machinepass.encode('utf-16-le')
846         msg["clearTextPassword"] = ldb.MessageElement(mputf16,
847                                                 ldb.FLAG_MOD_REPLACE,
848                                                 "clearTextPassword")
849         samdb.modify(msg)
850
851         res = samdb.search(expression=("samAccountName=%s$" % names.netbiosname),
852                      attrs=["msDs-keyVersionNumber"])
853         assert(len(res) == 1)
854         kvno = int(str(res[0]["msDs-keyVersionNumber"]))
855         secChanType = int(secrets_msg[0]["secureChannelType"][0])
856
857         secretsdb_self_join(secrets_ldb, domain=names.domain,
858                     realm=names.realm,
859                     domainsid=names.domainsid,
860                     dnsdomain=names.dnsdomain,
861                     netbiosname=names.netbiosname,
862                     machinepass=machinepass,
863                     key_version_number=kvno,
864                     secure_channel_type=secChanType)
865     else:
866         raise ProvisioningError("Unable to find a Secure Channel"
867                                 "of type SEC_CHAN_BDC")
868
869 def update_dns_account_password(samdb, secrets_ldb, names):
870     """Update (change) the password of the dns both in the SAM db and in
871        secret one
872
873     :param samdb: An LDB object related to the sam.ldb file of a given provision
874     :param secrets_ldb: An LDB object related to the secrets.ldb file of a given
875                         provision
876     :param names: List of key provision parameters"""
877
878     expression = "samAccountName=dns-%s" % names.netbiosname
879     secrets_msg = secrets_ldb.search(expression=expression)
880     if len(secrets_msg) == 1:
881         res = samdb.search(expression=expression, attrs=[])
882         assert(len(res) == 1)
883
884         msg = ldb.Message(res[0].dn)
885         machinepass = samba.generate_random_password(128, 255)
886         mputf16 = machinepass.encode('utf-16-le')
887         msg["clearTextPassword"] = ldb.MessageElement(mputf16,
888                                                 ldb.FLAG_MOD_REPLACE,
889                                                 "clearTextPassword")
890
891         samdb.modify(msg)
892
893         res = samdb.search(expression=expression,
894                      attrs=["msDs-keyVersionNumber"])
895         assert(len(res) == 1)
896         kvno = str(res[0]["msDs-keyVersionNumber"])
897
898         msg = ldb.Message(secrets_msg[0].dn)
899         msg["secret"] = ldb.MessageElement(machinepass,
900                                                 ldb.FLAG_MOD_REPLACE,
901                                                 "secret")
902         msg["msDS-KeyVersionNumber"] = ldb.MessageElement(kvno,
903                                                 ldb.FLAG_MOD_REPLACE,
904                                                 "msDS-KeyVersionNumber")
905
906         secrets_ldb.modify(msg)
907     else:
908         raise ProvisioningError("Unable to find an object"
909                                 " with %s" % expression )
910
911 def search_constructed_attrs_stored(samdb, rootdn, attrs):
912     """Search a given sam DB for calculated attributes that are
913     still stored in the db.
914
915     :param samdb: An LDB object pointing to the sam
916     :param rootdn: The base DN where the search should start
917     :param attrs: A list of attributes to be searched
918     :return: A hash with attributes as key and an array of
919              array. Each array contains the dn and the associated
920              values for this attribute as they are stored in the
921              sam."""
922
923     hashAtt = {}
924     expr = construct_existor_expr(attrs)
925     if expr == "":
926         return hashAtt
927     entry = samdb.search(expression=expr, base=ldb.Dn(samdb, str(rootdn)),
928                          scope=SCOPE_SUBTREE, attrs=attrs,
929                          controls=["search_options:1:2","bypassoperational:0"])
930     if len(entry) == 0:
931         # Nothing anymore
932         return hashAtt
933
934     for ent in entry:
935         for att in attrs:
936             if ent.get(att):
937                 if hashAtt.has_key(att):
938                     hashAtt[att][str(ent.dn).lower()] = str(ent[att])
939                 else:
940                     hashAtt[att] = {}
941                     hashAtt[att][str(ent.dn).lower()] = str(ent[att])
942
943     return hashAtt
944
945 def int64range2str(value):
946     """Display the int64 range stored in value as xxx-yyy
947
948     :param value: The int64 range
949     :return: A string of the representation of the range
950     """
951
952     lvalue = long(value)
953     str = "%d-%d" % (lvalue&0xFFFFFFFF, lvalue>>32)
954     return str