upgradeprovision: Fix typo
[kai/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 """Helpers 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, 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 creds: Credentials for the authentification
359     :param session: Session object
360     :param smbconf: Path to the smb.conf file
361     :param provdir: Directory where the provision will be stored
362     :param logger: A Logger
363     """
364     if os.path.isdir(provdir):
365         shutil.rmtree(provdir)
366     os.mkdir(provdir)
367     logger.info("Provision stored in %s", provdir)
368     provision(logger, session, creds, smbconf=smbconf,
369             targetdir=provdir, samdb_fill=FILL_FULL, realm=names.realm,
370             domain=names.domain, domainguid=names.domainguid,
371             domainsid=str(names.domainsid), ntdsguid=names.ntdsguid,
372             policyguid=names.policyid, policyguid_dc=names.policyid_dc,
373             hostname=names.netbiosname.lower(), hostip=None, hostip6=None,
374             invocationid=names.invocation, adminpass=names.adminpass,
375             krbtgtpass=None, machinepass=None, dnspass=None, root=None,
376             nobody=None, wheel=None, users=None,
377             serverrole="domain controller", ldap_backend_extra_port=None,
378             backend_type=None, ldapadminpass=None, ol_mmr_urls=None,
379             slapd_path=None, setup_ds_path=None, nosync=None,
380             dom_for_fun_level=names.domainlevel,
381             ldap_dryrun_mode=None, useeadb=True)
382
383
384 def dn_sort(x, y):
385     """Sorts two DNs in the lexicographical order it and put higher level DN
386     before.
387
388     So given the dns cn=bar,cn=foo and cn=foo the later will be return as
389     smaller
390
391     :param x: First object to compare
392     :param y: Second object to compare
393     """
394     p = re.compile(r'(?<!\\), ?')
395     tab1 = p.split(str(x))
396     tab2 = p.split(str(y))
397     minimum = min(len(tab1), len(tab2))
398     len1 = len(tab1)-1
399     len2 = len(tab2)-1
400     # Note: python range go up to upper limit but do not include it
401     for i in range(0, minimum):
402         ret = cmp(tab1[len1-i], tab2[len2-i])
403         if ret != 0:
404             return ret
405         else:
406             if i == minimum-1:
407                 assert len1!=len2,"PB PB PB" + " ".join(tab1)+" / " + " ".join(tab2)
408                 if len1 > len2:
409                     return 1
410                 else:
411                     return -1
412     return ret
413
414
415 def identic_rename(ldbobj, dn):
416     """Perform a back and forth rename to trigger renaming on attribute that
417     can't be directly modified.
418
419     :param lbdobj: An Ldb Object
420     :param dn: DN of the object to manipulate
421     """
422     (before, after) = str(dn).split('=', 1)
423     # we need to use relax to avoid the subtree_rename constraints
424     ldbobj.rename(dn, ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), ["relax:0"])
425     ldbobj.rename(ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), dn, ["relax:0"])
426
427
428 def chunck_acl(acl):
429     """Return separate ACE of an ACL
430
431     :param acl: A string representing the ACL
432     :return: A hash with different parts
433     """
434
435     p = re.compile(r'(\w+)?(\(.*?\))')
436     tab = p.findall(acl)
437
438     hash = {}
439     hash["aces"] = []
440     for e in tab:
441         if len(e[0]) > 0:
442             hash["flags"] = e[0]
443         hash["aces"].append(e[1])
444
445     return hash
446
447
448 def chunck_sddl(sddl):
449     """ Return separate parts of the SDDL (owner, group, ...)
450
451     :param sddl: An string containing the SDDL to chunk
452     :return: A hash with the different chunk
453     """
454
455     p = re.compile(r'([OGDS]:)(.*?)(?=(?:[GDS]:|$))')
456     tab = p.findall(sddl)
457
458     hash = {}
459     for e in tab:
460         if e[0] == "O:":
461             hash["owner"] = e[1]
462         if e[0] == "G:":
463             hash["group"] = e[1]
464         if e[0] == "D:":
465             hash["dacl"] = e[1]
466         if e[0] == "S:":
467             hash["sacl"] = e[1]
468
469     return hash
470
471
472 def get_diff_sddls(refsddl, cursddl):
473     """Get the difference between 2 sddl
474
475     This function split the textual representation of ACL into smaller
476     chunck in order to not to report a simple permutation as a difference
477
478     :param refsddl: First sddl to compare
479     :param cursddl: Second sddl to compare
480     :return: A string that explain difference between sddls
481     """
482
483     txt = ""
484     hash_new = chunck_sddl(cursddl)
485     hash_ref = chunck_sddl(refsddl)
486
487     if hash_new["owner"] != hash_ref["owner"]:
488         txt = "\tOwner mismatch: %s (in ref) %s" \
489               "(in current)\n" % (hash_ref["owner"], hash_new["owner"])
490
491     if hash_new["group"] != hash_ref["group"]:
492         txt = "%s\tGroup mismatch: %s (in ref) %s" \
493               "(in current)\n" % (txt, hash_ref["group"], hash_new["group"])
494
495     for part in ["dacl", "sacl"]:
496         if hash_new.has_key(part) and hash_ref.has_key(part):
497
498             # both are present, check if they contain the same ACE
499             h_new = set()
500             h_ref = set()
501             c_new = chunck_acl(hash_new[part])
502             c_ref = chunck_acl(hash_ref[part])
503
504             for elem in c_new["aces"]:
505                 h_new.add(elem)
506
507             for elem in c_ref["aces"]:
508                 h_ref.add(elem)
509
510             for k in set(h_ref):
511                 if k in h_new:
512                     h_new.remove(k)
513                     h_ref.remove(k)
514
515             if len(h_new) + len(h_ref) > 0:
516                 txt = "%s\tPart %s is different between reference" \
517                       " and current here is the detail:\n" % (txt, part)
518
519                 for item in h_new:
520                     txt = "%s\t\t%s ACE is not present in the" \
521                           " reference\n" % (txt, item)
522
523                 for item in h_ref:
524                     txt = "%s\t\t%s ACE is not present in the" \
525                           " current\n" % (txt, item)
526
527         elif hash_new.has_key(part) and not hash_ref.has_key(part):
528             txt = "%s\tReference ACL hasn't a %s part\n" % (txt, part)
529         elif not hash_new.has_key(part) and hash_ref.has_key(part):
530             txt = "%s\tCurrent ACL hasn't a %s part\n" % (txt, part)
531
532     return txt
533
534
535 def update_secrets(newsecrets_ldb, secrets_ldb, messagefunc):
536     """Update secrets.ldb
537
538     :param newsecrets_ldb: An LDB object that is connected to the secrets.ldb
539         of the reference provision
540     :param secrets_ldb: An LDB object that is connected to the secrets.ldb
541         of the updated provision
542     """
543
544     messagefunc(SIMPLE, "update secrets.ldb")
545     reference = newsecrets_ldb.search(expression="dn=@MODULES", base="",
546                                         scope=SCOPE_SUBTREE)
547     current = secrets_ldb.search(expression="dn=@MODULES", base="",
548                                         scope=SCOPE_SUBTREE)
549     assert reference, "Reference modules list can not be empty"
550     if len(current) == 0:
551         # No modules present
552         delta = secrets_ldb.msg_diff(ldb.Message(), reference[0])
553         delta.dn = reference[0].dn
554         secrets_ldb.add(reference[0])
555     else:
556         delta = secrets_ldb.msg_diff(current[0], reference[0])
557         delta.dn = current[0].dn
558         secrets_ldb.modify(delta)
559
560     reference = newsecrets_ldb.search(expression="objectClass=top", base="",
561                                         scope=SCOPE_SUBTREE, attrs=["dn"])
562     current = secrets_ldb.search(expression="objectClass=top", base="",
563                                         scope=SCOPE_SUBTREE, attrs=["dn"])
564     hash_new = {}
565     hash = {}
566     listMissing = []
567     listPresent = []
568
569     empty = ldb.Message()
570     for i in range(0, len(reference)):
571         hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"]
572
573     # Create a hash for speeding the search of existing object in the
574     # current provision
575     for i in range(0, len(current)):
576         hash[str(current[i]["dn"]).lower()] = current[i]["dn"]
577
578     for k in hash_new.keys():
579         if not hash.has_key(k):
580             listMissing.append(hash_new[k])
581         else:
582             listPresent.append(hash_new[k])
583
584     for entry in listMissing:
585         reference = newsecrets_ldb.search(expression="dn=%s" % entry,
586                                             base="", scope=SCOPE_SUBTREE)
587         current = secrets_ldb.search(expression="dn=%s" % entry,
588                                             base="", scope=SCOPE_SUBTREE)
589         delta = secrets_ldb.msg_diff(empty, reference[0])
590         for att in hashAttrNotCopied:
591             delta.remove(att)
592         messagefunc(CHANGE, "Entry %s is missing from secrets.ldb" %
593                     reference[0].dn)
594         for att in delta:
595             messagefunc(CHANGE, " Adding attribute %s" % att)
596         delta.dn = reference[0].dn
597         secrets_ldb.add(delta)
598
599     for entry in listPresent:
600         reference = newsecrets_ldb.search(expression="dn=%s" % entry,
601                                             base="", scope=SCOPE_SUBTREE)
602         current = secrets_ldb.search(expression="dn=%s" % entry, base="",
603                                             scope=SCOPE_SUBTREE)
604         delta = secrets_ldb.msg_diff(current[0], reference[0])
605         for att in hashAttrNotCopied:
606             delta.remove(att)
607         for att in delta:
608             if att == "name":
609                 messagefunc(CHANGE, "Found attribute name on  %s,"
610                                     " must rename the DN" % (current[0].dn))
611                 identic_rename(secrets_ldb, reference[0].dn)
612             else:
613                 delta.remove(att)
614
615     for entry in listPresent:
616         reference = newsecrets_ldb.search(expression="dn=%s" % entry, base="",
617                                             scope=SCOPE_SUBTREE)
618         current = secrets_ldb.search(expression="dn=%s" % entry, base="",
619                                             scope=SCOPE_SUBTREE)
620         delta = secrets_ldb.msg_diff(current[0], reference[0])
621         for att in hashAttrNotCopied:
622             delta.remove(att)
623         for att in delta:
624             if att == "msDS-KeyVersionNumber":
625                 delta.remove(att)
626             if att != "dn":
627                 messagefunc(CHANGE,
628                             "Adding/Changing attribute %s to %s" %
629                             (att, current[0].dn))
630
631         delta.dn = current[0].dn
632         secrets_ldb.modify(delta)
633
634     res2 = secrets_ldb.search(expression="(samaccountname=dns)",
635                                 scope=SCOPE_SUBTREE, attrs=["dn"])
636
637     if (len(res2) == 1):
638             messagefunc(SIMPLE, "Remove old dns account")
639             secrets_ldb.delete(res2[0]["dn"])
640
641
642 def getOEMInfo(samdb, rootdn):
643     """Return OEM Information on the top level Samba4 use to store version
644     info in this field
645
646     :param samdb: An LDB object connect to sam.ldb
647     :param rootdn: Root DN of the domain
648     :return: The content of the field oEMInformation (if any)
649     """
650     res = samdb.search(expression="(objectClass=*)", base=str(rootdn),
651                             scope=SCOPE_BASE, attrs=["dn", "oEMInformation"])
652     if len(res) > 0:
653         info = res[0]["oEMInformation"]
654         return info
655     else:
656         return ""
657
658
659 def updateOEMInfo(samdb, rootdn):
660     """Update the OEMinfo field to add information about upgrade
661
662     :param samdb: an LDB object connected to the sam DB
663     :param rootdn: The string representation of the root DN of
664         the provision (ie. DC=...,DC=...)
665     """
666     res = samdb.search(expression="(objectClass=*)", base=rootdn,
667                             scope=SCOPE_BASE, attrs=["dn", "oEMInformation"])
668     if len(res) > 0:
669         info = res[0]["oEMInformation"]
670         info = "%s, upgrade to %s" % (info, version)
671         delta = ldb.Message()
672         delta.dn = ldb.Dn(samdb, str(res[0]["dn"]))
673         delta["oEMInformation"] = ldb.MessageElement(info, ldb.FLAG_MOD_REPLACE,
674                                                         "oEMInformation" )
675         samdb.modify(delta)
676
677 def update_gpo(paths, samdb, names, lp, message, force=0):
678     """Create missing GPO file object if needed
679
680     Set ACL correctly also.
681     Check ACLs for sysvol/netlogon dirs also
682     """
683     resetacls = False
684     try:
685         ntacls.checkset_backend(lp, None, None)
686         eadbname = lp.get("posix:eadb")
687         if eadbname is not None and eadbname != "":
688             try:
689                 attribute = samba.xattr_tdb.wrap_getxattr(eadbname,
690                                 paths.sysvol, xattr.XATTR_NTACL_NAME)
691             except Exception:
692                 attribute = samba.xattr_native.wrap_getxattr(paths.sysvol,
693                                 xattr.XATTR_NTACL_NAME)
694         else:
695             attribute = samba.xattr_native.wrap_getxattr(paths.sysvol,
696                                 xattr.XATTR_NTACL_NAME)
697     except Exception:
698        resetacls = True
699
700     if force:
701         resetacls = True
702
703     dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid)
704     if not os.path.isdir(dir):
705         create_gpo_struct(dir)
706
707     if names.policyid_dc is None:
708         raise ProvisioningError("Policy ID for Domain controller is missing")
709     dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid_dc)
710     if not os.path.isdir(dir):
711         create_gpo_struct(dir)
712     # We always reinforce acls on GPO folder because they have to be in sync
713     # with the one in DS
714     try:
715         set_gpos_acl(paths.sysvol, names.dnsdomain, names.domainsid,
716             names.domaindn, samdb, lp)
717     except TypeError, e:
718         message(ERROR, "Unable to set ACLs on policies related objects,"
719                        " if not using posix:eadb, you must be root to do it")
720
721     if resetacls:
722        try:
723             setsysvolacl(samdb, paths.netlogon, paths.sysvol, names.wheel_gid,
724                         names.domainsid, names.dnsdomain, names.domaindn, lp)
725        except TypeError, e:
726             message(ERROR, "Unable to set ACLs on sysvol share, if not using"
727                            "posix:eadb, you must be root to do it")
728
729 def increment_calculated_keyversion_number(samdb, rootdn, hashDns):
730     """For a given hash associating dn and a number, this function will
731     update the replPropertyMetaData of each dn in the hash, so that the
732     calculated value of the msDs-KeyVersionNumber is equal or superior to the
733     one associated to the given dn.
734
735     :param samdb: An SamDB object pointing to the sam
736     :param rootdn: The base DN where we want to start
737     :param hashDns: A hash with dn as key and number representing the
738                  minimum value of msDs-KeyVersionNumber that we want to
739                  have
740     """
741     entry = samdb.search(expression='(objectClass=user)',
742                          base=ldb.Dn(samdb,str(rootdn)),
743                          scope=SCOPE_SUBTREE, attrs=["msDs-KeyVersionNumber"],
744                          controls=["search_options:1:2"])
745     done = 0
746     hashDone = {}
747     if len(entry) == 0:
748         raise ProvisioningError("Unable to find msDs-KeyVersionNumber")
749     else:
750         for e in entry:
751             if hashDns.has_key(str(e.dn).lower()):
752                 val = e.get("msDs-KeyVersionNumber")
753                 if not val:
754                     val = "0"
755                 version = int(str(hashDns[str(e.dn).lower()]))
756                 if int(str(val)) < version:
757                     done = done + 1
758                     samdb.set_attribute_replmetadata_version(str(e.dn),
759                                                               "unicodePwd",
760                                                               version, True)
761 def delta_update_basesamdb(refsampath, sampath, creds, session, lp, message):
762     """Update the provision container db: sam.ldb
763     This function is aimed for alpha9 and newer;
764
765     :param refsampath: Path to the samdb in the reference provision
766     :param sampath: Path to the samdb in the upgraded provision
767     :param creds: Credential used for openning LDB files
768     :param session: Session to use for openning LDB files
769     :param lp: A loadparam object
770     :return: A msg_diff object with the difference between the @ATTRIBUTES
771              of the current provision and the reference provision
772     """
773
774     message(SIMPLE,
775             "Update base samdb by searching difference with reference one")
776     refsam = Ldb(refsampath, session_info=session, credentials=creds,
777                     lp=lp, options=["modules:"])
778     sam = Ldb(sampath, session_info=session, credentials=creds, lp=lp,
779                 options=["modules:"])
780
781     empty = ldb.Message()
782     deltaattr = None
783     reference = refsam.search(expression="")
784
785     for refentry in reference:
786         entry = sam.search(expression="dn=%s" % refentry["dn"],
787                             scope=SCOPE_SUBTREE)
788         if not len(entry):
789             delta = sam.msg_diff(empty, refentry)
790             message(CHANGE, "Adding %s to sam db" % str(refentry.dn))
791             if str(refentry.dn) == "@PROVISION" and\
792                 delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
793                 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
794             delta.dn = refentry.dn
795             sam.add(delta)
796         else:
797             delta = sam.msg_diff(entry[0], refentry)
798             if str(refentry.dn) == "@ATTRIBUTES":
799                 deltaattr = sam.msg_diff(refentry, entry[0])
800             if str(refentry.dn) == "@PROVISION" and\
801                 delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
802                 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
803             if len(delta.items()) > 1:
804                 delta.dn = refentry.dn
805                 sam.modify(delta)
806
807     return deltaattr
808
809
810 def construct_existor_expr(attrs):
811     """Construct a exists or LDAP search expression.
812
813     :param attrs: List of attribute on which we want to create the search
814         expression.
815     :return: A string representing the expression, if attrs is empty an
816         empty string is returned
817     """
818     expr = ""
819     if len(attrs) > 0:
820         expr = "(|"
821         for att in attrs:
822             expr = "%s(%s=*)"%(expr,att)
823         expr = "%s)"%expr
824     return expr
825
826 def update_machine_account_password(samdb, secrets_ldb, names):
827     """Update (change) the password of the current DC both in the SAM db and in
828        secret one
829
830     :param samdb: An LDB object related to the sam.ldb file of a given provision
831     :param secrets_ldb: An LDB object related to the secrets.ldb file of a given
832                         provision
833     :param names: List of key provision parameters"""
834
835     expression = "samAccountName=%s$" % names.netbiosname
836     secrets_msg = secrets_ldb.search(expression=expression,
837                                         attrs=["secureChannelType"])
838     if int(secrets_msg[0]["secureChannelType"][0]) == SEC_CHAN_BDC:
839         res = samdb.search(expression=expression, attrs=[])
840         assert(len(res) == 1)
841
842         msg = ldb.Message(res[0].dn)
843         machinepass = samba.generate_random_password(128, 255)
844         mputf16 = machinepass.encode('utf-16-le')
845         msg["clearTextPassword"] = ldb.MessageElement(mputf16,
846                                                 ldb.FLAG_MOD_REPLACE,
847                                                 "clearTextPassword")
848         samdb.modify(msg)
849
850         res = samdb.search(expression=("samAccountName=%s$" % names.netbiosname),
851                      attrs=["msDs-keyVersionNumber"])
852         assert(len(res) == 1)
853         kvno = int(str(res[0]["msDs-keyVersionNumber"]))
854         secChanType = int(secrets_msg[0]["secureChannelType"][0])
855
856         secretsdb_self_join(secrets_ldb, domain=names.domain,
857                     realm=names.realm,
858                     domainsid=names.domainsid,
859                     dnsdomain=names.dnsdomain,
860                     netbiosname=names.netbiosname,
861                     machinepass=machinepass,
862                     key_version_number=kvno,
863                     secure_channel_type=secChanType)
864     else:
865         raise ProvisioningError("Unable to find a Secure Channel"
866                                 "of type SEC_CHAN_BDC")
867
868 def update_dns_account_password(samdb, secrets_ldb, names):
869     """Update (change) the password of the dns both in the SAM db and in
870        secret one
871
872     :param samdb: An LDB object related to the sam.ldb file of a given provision
873     :param secrets_ldb: An LDB object related to the secrets.ldb file of a given
874                         provision
875     :param names: List of key provision parameters"""
876
877     expression = "samAccountName=dns-%s" % names.netbiosname
878     secrets_msg = secrets_ldb.search(expression=expression)
879     if len(secrets_msg) == 1:
880         res = samdb.search(expression=expression, attrs=[])
881         assert(len(res) == 1)
882
883         msg = ldb.Message(res[0].dn)
884         machinepass = samba.generate_random_password(128, 255)
885         mputf16 = machinepass.encode('utf-16-le')
886         msg["clearTextPassword"] = ldb.MessageElement(mputf16,
887                                                 ldb.FLAG_MOD_REPLACE,
888                                                 "clearTextPassword")
889
890         samdb.modify(msg)
891
892         res = samdb.search(expression=expression,
893                      attrs=["msDs-keyVersionNumber"])
894         assert(len(res) == 1)
895         kvno = str(res[0]["msDs-keyVersionNumber"])
896
897         msg = ldb.Message(secrets_msg[0].dn)
898         msg["secret"] = ldb.MessageElement(machinepass,
899                                                 ldb.FLAG_MOD_REPLACE,
900                                                 "secret")
901         msg["msDS-KeyVersionNumber"] = ldb.MessageElement(kvno,
902                                                 ldb.FLAG_MOD_REPLACE,
903                                                 "msDS-KeyVersionNumber")
904
905         secrets_ldb.modify(msg)
906     else:
907         raise ProvisioningError("Unable to find an object"
908                                 " with %s" % expression )
909
910 def search_constructed_attrs_stored(samdb, rootdn, attrs):
911     """Search a given sam DB for calculated attributes that are
912     still stored in the db.
913
914     :param samdb: An LDB object pointing to the sam
915     :param rootdn: The base DN where the search should start
916     :param attrs: A list of attributes to be searched
917     :return: A hash with attributes as key and an array of
918              array. Each array contains the dn and the associated
919              values for this attribute as they are stored in the
920              sam."""
921
922     hashAtt = {}
923     expr = construct_existor_expr(attrs)
924     if expr == "":
925         return hashAtt
926     entry = samdb.search(expression=expr, base=ldb.Dn(samdb, str(rootdn)),
927                          scope=SCOPE_SUBTREE, attrs=attrs,
928                          controls=["search_options:1:2","bypassoperational:0"])
929     if len(entry) == 0:
930         # Nothing anymore
931         return hashAtt
932
933     for ent in entry:
934         for att in attrs:
935             if ent.get(att):
936                 if hashAtt.has_key(att):
937                     hashAtt[att][str(ent.dn).lower()] = str(ent[att])
938                 else:
939                     hashAtt[att] = {}
940                     hashAtt[att][str(ent.dn).lower()] = str(ent[att])
941
942     return hashAtt
943
944 def int64range2str(value):
945     """Display the int64 range stored in value as xxx-yyy
946
947     :param value: The int64 range
948     :return: A string of the representation of the range
949     """
950
951     lvalue = long(value)
952     str = "%d-%d" % (lvalue&0xFFFFFFFF, lvalue>>32)
953     return str