066fbe469ceb3223be4d5ee0baa408f805bc16e8
[samba.git] / source4 / scripting / bin / upgradeprovision
1 #!/usr/bin/env python
2 # vim: expandtab
3 #
4 # Copyright (C) Matthieu Patou <mat@matws.net> 2009
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
25 import shutil
26 import optparse
27 import os
28 import sys
29 import tempfile
30 # Allow to run from s4 source directory (without installing samba)
31 sys.path.insert(0, "bin/python")
32
33 import samba
34 import samba.getopt as options
35 from samba.credentials import DONT_USE_KERBEROS
36 from samba.auth import system_session, admin_session
37 from samba import Ldb, version
38 from ldb import SCOPE_SUBTREE, SCOPE_BASE, \
39                 FLAG_MOD_REPLACE, FLAG_MOD_ADD, FLAG_MOD_DELETE,\
40                 MessageElement, Message, Dn
41 from samba import param
42 from samba.misc import messageEltFlagToString
43 from samba.provision import find_setup_dir, get_domain_descriptor, get_config_descriptor, secretsdb_self_join,set_gpo_acl,getpolicypath,create_gpo_struct, ProvisioningError
44 from samba.schema import get_linked_attributes, Schema, get_schema_descriptor
45 from samba.dcerpc import security
46 from samba.ndr import ndr_unpack
47 from samba.dcerpc.misc import SEC_CHAN_BDC
48 from samba.upgradehelpers import dn_sort, get_paths, newprovision, find_provision_key_parameters
49
50 never=0
51 replace=2^FLAG_MOD_REPLACE
52 add=2^FLAG_MOD_ADD
53 delete=2^FLAG_MOD_DELETE
54
55 #Errors are always logged
56 ERROR =     -1
57 SIMPLE =     0x00
58 CHANGE =     0x01
59 CHANGESD =     0x02
60 GUESS =     0x04
61 PROVISION =    0x08
62 CHANGEALL =    0xff
63
64 __docformat__ = "restructuredText"
65
66 # Attributes that are never copied from the reference provision (even if they
67 # do not exist in the destination object).
68 # This is most probably because they are populated automatcally when object is
69 # created
70 # This also apply to imported object from reference provision
71 hashAttrNotCopied = {     "dn": 1, "whenCreated": 1, "whenChanged": 1, "objectGUID": 1, "replPropertyMetaData": 1, "uSNChanged": 1,
72                         "uSNCreated": 1, "parentGUID": 1, "objectCategory": 1, "distinguishedName": 1,
73                         "showInAdvancedViewOnly": 1, "instanceType": 1, "cn": 1, "msDS-Behavior-Version":1, "nextRid":1,
74                         "nTMixedDomain": 1, "versionNumber":1, "lmPwdHistory":1, "pwdLastSet": 1, "ntPwdHistory":1, "unicodePwd":1,
75                         "dBCSPwd":1, "supplementalCredentials":1, "gPCUserExtensionNames":1, "gPCMachineExtensionNames":1,
76                         "maxPwdAge":1, "mail":1, "secret":1, "possibleInferiors":1, "sAMAccountType":1}
77
78 # Usually for an object that already exists we do not overwrite attributes as
79 # they might have been changed for good reasons. Anyway for a few of them it's
80 # mandatory to replace them otherwise the provision will be broken somehow.
81 hashOverwrittenAtt = {    "prefixMap": replace, "systemMayContain": replace, "systemOnly":replace, "searchFlags":replace,
82                         "mayContain":replace,  "systemFlags":replace, "description":replace,
83                         "oEMInformation":never, "operatingSystemVersion":replace, "adminPropertyPages":replace,
84                         "defaultSecurityDescriptor": replace, "wellKnownObjects":replace, "privilege":delete, "groupType":replace,
85                         "rIDAvailablePool": never}
86
87
88 backlinked = []
89 dn_syntax_att = []
90 def define_what_to_log(opts):
91     what = 0
92     if opts.debugchange:
93         what = what | CHANGE
94     if opts.debugchangesd:
95         what = what | CHANGESD
96     if opts.debugguess:
97         what = what | GUESS
98     if opts.debugprovision:
99         what = what | PROVISION
100     if opts.debugall:
101         what = what | CHANGEALL
102     return what
103
104
105 parser = optparse.OptionParser("provision [options]")
106 sambaopts = options.SambaOptions(parser)
107 parser.add_option_group(sambaopts)
108 parser.add_option_group(options.VersionOptions(parser))
109 credopts = options.CredentialsOptions(parser)
110 parser.add_option_group(credopts)
111 parser.add_option("--setupdir", type="string", metavar="DIR",
112                     help="directory with setup files")
113 parser.add_option("--debugprovision", help="Debug provision", action="store_true")
114 parser.add_option("--debugguess", help="Print information on what is different but won't be changed", action="store_true")
115 parser.add_option("--debugchange", help="Print information on what is different but won't be changed", action="store_true")
116 parser.add_option("--debugchangesd", help="Print information security descriptors differences", action="store_true")
117 parser.add_option("--debugall", help="Print all available information (very verbose)", action="store_true")
118 parser.add_option("--full", help="Perform full upgrade of the samdb (schema, configuration, new objects, ...", action="store_true")
119
120 opts = parser.parse_args()[0]
121
122 whatToLog = define_what_to_log(opts)
123
124 def messageprovision(text):
125     """Print a message if quiet is not set
126
127     :param text: Message to print """
128     if opts.debugprovision or opts.debugall:
129         print text
130
131 def message(what,text):
132     """Print a message if this message type has been selected to be printed
133
134     :param what: Category of the message
135     :param text: Message to print """
136     if (whatToLog & what) or what <= 0:
137         print text
138
139 if len(sys.argv) == 1:
140     opts.interactive = True
141 lp = sambaopts.get_loadparm()
142 smbconf = lp.configfile
143
144 creds = credopts.get_credentials(lp)
145 creds.set_kerberos_state(DONT_USE_KERBEROS)
146 setup_dir = opts.setupdir
147 if setup_dir is None:
148     setup_dir = find_setup_dir()
149
150 session = system_session()
151
152 def identic_rename(ldbobj,dn):
153     """Perform a back and forth rename to trigger renaming on attribute that can't be directly modified.
154
155     :param lbdobj: An Ldb Object
156     :param dn: DN of the object to manipulate """
157     (before,sep,after)=str(dn).partition('=')
158     ldbobj.rename(dn,Dn(ldbobj,"%s=foo%s"%(before,after)))
159     ldbobj.rename(Dn(ldbobj,"%s=foo%s"%(before,after)),dn)
160
161
162 def populate_backlink(newpaths,creds,session,schemadn):
163     """Populate an array with all the back linked attributes
164
165     This attributes that are modified automaticaly when
166     front attibutes are changed
167
168     :param newpaths: a list of paths for different provision objects
169     :param creds: credential for the authentification
170     :param session: session for connexion
171     :param schemadn: DN of the schema for the partition"""
172     newsam_ldb = Ldb(newpaths.samdb, session_info=session, credentials=creds,lp=lp)
173     linkedAttHash = get_linked_attributes(Dn(newsam_ldb,str(schemadn)),newsam_ldb)
174     backlinked.extend(linkedAttHash.values())
175
176 def populate_dnsyntax(newpaths,creds,session,schemadn):
177     """Populate an array with all the attributes that have DN synthax (oid 2.5.5.1)
178
179     :param newpaths: a list of paths for different provision objects
180     :param creds: credential for the authentification
181     :param session: session for connexion
182     :param schemadn: DN of the schema for the partition"""
183     newsam_ldb = Ldb(newpaths.samdb, session_info=session, credentials=creds,lp=lp)
184     res = newsam_ldb.search(expression="(attributeSyntax=2.5.5.1)",base=Dn(newsam_ldb,str(schemadn)),
185                             scope=SCOPE_SUBTREE, attrs=["lDAPDisplayName"])
186     for elem in res:
187         dn_syntax_att.append(elem["lDAPDisplayName"])
188
189
190 def sanitychecks(credentials,session_info,names,paths):
191     """Populate an array with all the attributes that have DN synthax (oid 2.5.5.1)
192
193     :param creds: credential for the authentification
194     :param session_info: session for connexion
195     :param names: list of key provision parameters
196     :param paths: list of path to provision object
197     :return: Status of check (1 for Ok, 0 for not Ok) """
198     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp,options=["modules:samba_dsdb"])
199
200     sam_ldb.set_session_info(session)
201     res = sam_ldb.search(expression="objectClass=ntdsdsa",base=str(names.configdn),
202                          scope=SCOPE_SUBTREE,attrs=["dn"],controls=["search_options:1:2"])
203     if len(res) == 0:
204         print "No DC found, your provision is most probalby hardly broken !"
205         return False
206     elif len(res) != 1:
207         print "Found %d domain controllers, for the moment upgradeprovision is not able to handle upgrade on \
208 domain with more than one DC, please demote the other(s) DC(s) before upgrading"%len(res)
209         return False
210     else:
211         return True
212
213
214 def print_provision_key_parameters(names):
215     """Do a a pretty print of provision parameters
216
217     :param names: list of key provision parameters """
218     message(GUESS, "rootdn      :"+str(names.rootdn))
219     message(GUESS, "configdn    :"+str(names.configdn))
220     message(GUESS, "schemadn    :"+str(names.schemadn))
221     message(GUESS, "serverdn    :"+str(names.serverdn))
222     message(GUESS, "netbiosname :"+names.netbiosname)
223     message(GUESS, "defaultsite :"+names.sitename)
224     message(GUESS, "dnsdomain   :"+names.dnsdomain)
225     message(GUESS, "hostname    :"+names.hostname)
226     message(GUESS, "domain      :"+names.domain)
227     message(GUESS, "realm       :"+names.realm)
228     message(GUESS, "invocationid:"+names.invocation)
229     message(GUESS, "policyguid  :"+names.policyid)
230     message(GUESS, "policyguiddc:"+str(names.policyid_dc))
231     message(GUESS, "domainsid   :"+str(names.domainsid))
232     message(GUESS, "domainguid  :"+names.domainguid)
233     message(GUESS, "ntdsguid    :"+names.ntdsguid)
234     message(GUESS, "domainlevel :"+str(names.domainlevel))
235
236
237 def handle_security_desc(ischema, att, msgElt, hashallSD, old, new):
238     """Check if the security descriptor has been modified.
239
240     This function also populate a hash used for the upgrade process.
241     :param ischema: Boolean that indicate if it's the schema that is updated
242     :param att: Name of the attribute
243     :param msgElt: MessageElement object
244     :param hashallSD: Hash table with DN as key and the old SD as value
245     :param old: The updated LDAP object
246     :param new: The reference LDAP object
247     :return: 1 to indicate that the attribute should be kept, 0 for discarding it
248     """
249     if ischema == 1 and att == "defaultSecurityDescriptor"  and msgElt.flags() == FLAG_MOD_REPLACE:
250         hashSD = {}
251         hashSD["oldSD"] = old[0][att]
252         hashSD["newSD"] = new[0][att]
253         hashallSD[str(old[0].dn)] = hashSD
254         return True
255     if att == "nTSecurityDescriptor"  and msgElt.flags() == FLAG_MOD_REPLACE:
256         if ischema == 0:
257             hashSD = {}
258             hashSD["oldSD"] = ndr_unpack(security.descriptor, str(old[0][att]))
259             hashSD["newSD"] = ndr_unpack(security.descriptor, str(new[0][att]))
260             hashallSD[str(old[0].dn)] = hashSD
261         return False
262     return False
263
264
265 def handle_special_case(att, delta, new, old, ischema):
266     """Define more complicate update rules for some attributes
267
268     :param att: The attribute to be updated
269     :param delta: A messageElement object that correspond to the difference between the updated object and the reference one
270     :param new: The reference object
271     :param old: The Updated object
272     :param ischema: A boolean that indicate that the attribute is part of a schema object
273     :return: Tru to indicate that the attribute should be kept, False for discarding it
274     """
275     flag = delta.get(att).flags()
276     if (att == "gPLink" or att == "gPCFileSysPath") and \
277         flag == FLAG_MOD_REPLACE and str(new[0].dn).lower() == str(old[0].dn).lower():
278         delta.remove(att)
279         return True
280     if att == "forceLogoff":
281         ref=0x8000000000000000
282         oldval=int(old[0][att][0])
283         newval=int(new[0][att][0])
284         ref == old and ref == abs(new)
285         return True
286     if (att == "adminDisplayName" or att == "adminDescription") and ischema:
287         return True
288
289     if (str(old[0].dn) == "CN=Samba4-Local-Domain,%s" % (str(names.schemadn))\
290         and att == "defaultObjectCategory" and flag == FLAG_MOD_REPLACE):
291         return True
292
293     if (str(old[0].dn) == "CN=Title,%s"%(str(names.schemadn)) and att == "rangeUpper" and flag == FLAG_MOD_REPLACE):
294         return True
295
296     if ((att == "member" or att == "servicePrincipalName") and flag == FLAG_MOD_REPLACE):
297         hash = {}
298         newval = []
299         changeDelta=0
300         for elem in old[0][att]:
301             hash[str(elem)]=1
302             newval.append(str(elem))
303
304         for elem in new[0][att]:
305             if not hash.has_key(str(elem)):
306                 changeDelta=1
307                 newval.append(str(elem))
308         if changeDelta == 1:
309             delta[att] = MessageElement(newval, FLAG_MOD_REPLACE, att)
310         else:
311             delta.remove(att)
312         return True
313
314     if (str(old[0].dn) == "%s"%(str(names.rootdn)) and att == "subRefs" and flag == FLAG_MOD_REPLACE):
315         return True
316     if str(delta.dn).endswith("CN=DisplaySpecifiers,%s"%names.configdn):
317         return True
318     return False
319
320 def update_secrets(newpaths, paths, creds, session):
321     """Update secrets.ldb
322
323     :param newpaths: a list of paths for different provision objects from the
324         reference provision
325     :param paths: a list of paths for different provision objects from the
326         upgraded provision
327     :param creds: credential for the authentification
328     :param session: session for connexion"""
329
330     message(SIMPLE,"update secrets.ldb")
331     newsecrets_ldb = Ldb(newpaths.secrets, session_info=session,
332         credentials=creds,lp=lp)
333     secrets_ldb = Ldb(paths.secrets, session_info=session,
334         credentials=creds,lp=lp, options=["modules:samba_secrets"])
335     reference = newsecrets_ldb.search(expression="dn=@MODULES",base="",
336         scope=SCOPE_SUBTREE)
337     current = secrets_ldb.search(expression="dn=@MODULES",base="",
338         scope=SCOPE_SUBTREE)
339     delta = secrets_ldb.msg_diff(current[0],reference[0])
340     delta.dn = current[0].dn
341     secrets_ldb.modify(delta)
342
343     newsecrets_ldb = Ldb(newpaths.secrets, session_info=session, credentials=creds,lp=lp)
344     secrets_ldb = Ldb(paths.secrets, session_info=session, credentials=creds,lp=lp)
345     reference = newsecrets_ldb.search(expression="objectClass=top",base="", scope=SCOPE_SUBTREE,attrs=["dn"])
346     current = secrets_ldb.search(expression="objectClass=top",base="", scope=SCOPE_SUBTREE,attrs=["dn"])
347     hash_new = {}
348     hash = {}
349     listMissing = []
350     listPresent = []
351
352     empty = Message()
353     for i in range(0,len(reference)):
354         hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"]
355
356     # Create a hash for speeding the search of existing object in the
357     # current provision
358     for i in range(0,len(current)):
359         hash[str(current[i]["dn"]).lower()] = current[i]["dn"]
360
361     for k in hash_new.keys():
362         if not hash.has_key(k):
363             listMissing.append(hash_new[k])
364         else:
365             listPresent.append(hash_new[k])
366
367     for entry in listMissing:
368         reference = newsecrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
369         current = secrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
370         delta = secrets_ldb.msg_diff(empty,reference[0])
371         for att in hashAttrNotCopied.keys():
372             delta.remove(att)
373         message(CHANGE,"Entry %s is missing from secrets.ldb"%reference[0].dn)
374         for att in delta:
375             message(CHANGE," Adding attribute %s"%att)
376         delta.dn = reference[0].dn
377         secrets_ldb.add(delta)
378
379     for entry in listPresent:
380         reference = newsecrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
381         current = secrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
382         delta = secrets_ldb.msg_diff(current[0],reference[0])
383         for att in hashAttrNotCopied.keys():
384             delta.remove(att)
385         for att in delta:
386             if att == "name":
387                 message(CHANGE,"Found attribute name on  %s, must rename the DN "%(current[0].dn))
388                 identic_rename(secrets_ldb,reference[0].dn)
389             else:
390                 delta.remove(att)
391
392     for entry in listPresent:
393         reference = newsecrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
394         current = secrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
395         delta = secrets_ldb.msg_diff(current[0],reference[0])
396         for att in hashAttrNotCopied.keys():
397             delta.remove(att)
398         for att in delta:
399             if att != "dn":
400                 message(CHANGE," Adding/Changing attribute %s to %s"%(att,current[0].dn))
401
402         delta.dn = current[0].dn
403         secrets_ldb.modify(delta)
404
405
406 def dump_denied_change(dn,att,flagtxt,current,reference):
407     """Print detailed information about why a changed is denied
408
409     :param dn: DN of the object which attribute is denied
410     :param att: Attribute that was supposed to be upgraded
411     :param flagtxt: Type of the update that should be performed (add, change, remove, ...)
412     :param current: Value(s) of the current attribute
413     :param reference: Value(s) of the reference attribute"""
414
415     message(CHANGE, "dn= "+str(dn)+" "+att+" with flag "+flagtxt+" is not allowed to be changed/removed, I discard this change ...")
416     if att != "objectSid" :
417         i = 0
418         for e in range(0,len(current)):
419             message(CHANGE,"old %d : %s"%(i,str(current[e])))
420             i+=1
421         if reference != None:
422             i = 0
423             for e in range(0,len(reference)):
424                 message(CHANGE,"new %d : %s"%(i,str(reference[e])))
425                 i+=1
426     else:
427         message(CHANGE,"old : %s"%str(ndr_unpack( security.dom_sid,current[0])))
428         message(CHANGE,"new : %s"%str(ndr_unpack( security.dom_sid,reference[0])))
429
430
431 def handle_special_add(sam_ldb,dn,names):
432     """Handle special operation (like remove) on some object needed during upgrade
433
434     This is mostly due to wrong creation of the object in previous provision.
435     :param sam_ldb: An Ldb object representing the SAM database
436     :param dn: DN of the object to inspect
437     :param names: list of key provision parameters"""
438     dntoremove = None
439     if str(dn).lower() == ("CN=Certificate Service DCOM Access,CN=Builtin,%s"%names.rootdn).lower():
440         #This entry was misplaced lets remove it if it exists
441         dntoremove = "CN=Certificate Service DCOM Access,CN=Users,%s"%names.rootdn
442
443     if str(dn).lower() == ("CN=Cryptographic Operators,CN=Builtin,%s"%names.rootdn).lower():
444         #This entry was misplaced lets remove it if it exists
445         dntoremove = "CN=Cryptographic Operators,CN=Users,%s"%names.rootdn
446
447     if str(dn).lower() == ("CN=Event Log Readers,CN=Builtin,%s"%names.rootdn).lower():
448         #This entry was misplaced lets remove it if it exists
449         dntoremove = "CN=Event Log Readers,CN=Users,%s"%names.rootdn
450
451     if dntoremove != None:
452         res = sam_ldb.search(expression="objectClass=*",base=dntoremove, scope=SCOPE_BASE,attrs=["dn"],controls=["search_options:1:2"])
453         if len(res) > 0:
454             message(CHANGE,"Existing object %s must be replaced by %s, removing old object"%(dntoremove,str(dn)))
455             sam_ldb.delete(res[0]["dn"])
456
457
458 def check_dn_nottobecreated(hash, index, listdn):
459     """Check if one of the DN present in the list has a creation order greater than the current.
460
461     Hash is indexed by dn to be created, with each key is associated the creation order
462     First dn to be created has the creation order 0, second has 1, ...
463     Index contain the current creation order
464
465     :param hash: Hash holding the different DN of the object to be created as key
466     :param index: Current creation order
467     :param listdn: List of DNs on which the current DN depends on
468     :return: None if the current object do not depend on other object or if all object have been
469     created before."""
470     if listdn == None:
471         return None
472     for dn in listdn:
473         key = str(dn).lower()
474         if hash.has_key(key) and hash[key] > index:
475             return str(dn)
476     return None
477
478
479 def add_missing_object(newsam_ldb, sam_ldb, dn, names, basedn, hash, index):
480     """Add a new object if the dependencies are satisfied
481
482     The function add the object if the object on which it depends are already created
483     :param newsam_ldb: Ldb object representing the SAM db of the reference provision
484     :param sam_ldb: Ldb object representing the SAM db of the upgraded provision
485     :param dn: DN of the object to be added
486     :param names: List of key provision parameters
487     :param basedn: DN of the partition to be updated
488     :param hash: Hash holding the different DN of the object to be created as key
489     :param index: Current creation order
490     :return: True if the object was created False otherwise"""
491     handle_special_add(sam_ldb,dn,names)
492     reference = newsam_ldb.search(expression="dn=%s"%(str(dn)),base=basedn,
493                     scope=SCOPE_SUBTREE,controls=["search_options:1:2"])
494     empty = Message()
495     delta = sam_ldb.msg_diff(empty,reference[0])
496     for att in hashAttrNotCopied.keys():
497         delta.remove(att)
498     for att in backlinked:
499         delta.remove(att)
500     depend_on_yettobecreated = None
501     for att in dn_syntax_att:
502         depend_on_yet_tobecreated = check_dn_nottobecreated(hash,index,delta.get(str(att)))
503         if depend_on_yet_tobecreated != None:
504             message(CHANGE,"Object %s depends on %s in attribute %s, delaying the creation"
505                             %(str(dn),depend_on_yet_tobecreated,str(att)))
506             return False
507     delta.dn = dn
508     message(CHANGE,"Object %s will be added"%dn)
509     sam_ldb.add(delta,["relax:0"])
510     return True
511
512
513 def gen_dn_index_hash(listMissing):
514     """Generate a hash associating the DN to its creation order
515
516     :param listMissing: List of DN
517     :return: Hash with DN as keys and creation order as values"""
518     hash = {}
519     for i in range(0,len(listMissing)):
520         hash[str(listMissing[i]).lower()] = i
521     return hash
522
523
524 def add_missing_entries(newsam_ldb, sam_ldb, names, basedn,list):
525     """Add the missing object whose DN is the list
526
527     The function add the object if the object on which it depends are already created
528     :param newsam_ldb: Ldb object representing the SAM db of the reference provision
529     :param sam_ldb: Ldb object representing the SAM db of the upgraded provision
530     :param dn: DN of the object to be added
531     :param names: List of key provision parameters
532     :param basedn: DN of the partition to be updated
533     :param list: List of DN to be added in the upgraded provision"""
534     listMissing = []
535     listDefered = list
536
537     while(len(listDefered) != len(listMissing) and len(listDefered) > 0):
538         index = 0
539         listMissing = listDefered
540         listDefered = []
541         hashMissing = gen_dn_index_hash(listMissing)
542         for dn in listMissing:
543             ret = add_missing_object(newsam_ldb,sam_ldb,dn,names,basedn,hashMissing,index)
544             index = index + 1
545             if ret == 0:
546                 #DN can't be created because it depends on some other DN in the list
547                 listDefered.append(dn)
548     if len(listDefered) != 0:
549         raise ProvisioningError("Unable to insert missing elements: circular references")
550
551
552 def check_diff_name(newpaths, paths, creds, session, basedn, names, ischema):
553     """Check differences between the reference provision and the upgraded one.
554
555     It looks for all objects which base DN is name. If ischema is "false" then
556     the scan is done in cross partition mode.
557     If "ischema" is true, then special handling is done for dealing with schema
558
559     This function will also add the missing object and update existing object to add
560     or remove attributes that were missing.
561     :param newpaths: List of paths for different provision objects from the reference provision
562     :param paths: List of paths for different provision objects from the upgraded provision
563     :param creds: Credential for the authentification
564     :param session: Session for connexion
565     :param basedn: DN of the partition to update
566     :param names: List of key provision parameters
567     :param ischema: Boolean indicating if the update is about the schema only
568     :return: Hash of security descriptor to update"""
569
570     hash_new = {}
571     hash = {}
572     hashallSD = {}
573     listMissing = []
574     listPresent = []
575     reference = []
576     current = []
577     # Connect to the reference provision and get all the attribute in the
578     # partition referred by name
579     newsam_ldb = Ldb(newpaths.samdb, session_info=session, credentials=creds,lp=lp)
580     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp, options=["modules:samba_dsdb"])
581     sam_ldb.transaction_start()
582     if ischema:
583         reference = newsam_ldb.search(expression="objectClass=*",base=basedn, scope=SCOPE_SUBTREE,attrs=["dn"])
584         current = sam_ldb.search(expression="objectClass=*",base=basedn, scope=SCOPE_SUBTREE,attrs=["dn"])
585     else:
586         reference = newsam_ldb.search(expression="objectClass=*",base=basedn, scope=SCOPE_SUBTREE,attrs=["dn"],controls=["search_options:1:2"])
587         current = sam_ldb.search(expression="objectClass=*",base=basedn, scope=SCOPE_SUBTREE,attrs=["dn"],controls=["search_options:1:2"])
588
589     sam_ldb.transaction_commit()
590     # Create a hash for speeding the search of new object
591     for i in range(0,len(reference)):
592         hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"]
593
594     # Create a hash for speeding the search of existing object in the
595     # current provision
596     for i in range(0,len(current)):
597         hash[str(current[i]["dn"]).lower()] = current[i]["dn"]
598
599     for k in hash_new.keys():
600         if not hash.has_key(k):
601             print hash_new[k]
602             listMissing.append(hash_new[k])
603         else:
604             listPresent.append(hash_new[k])
605
606     # Sort the missing object in order to have object of the lowest level
607     # first (which can be containers for higher level objects)
608     listMissing.sort(dn_sort)
609     listPresent.sort(dn_sort)
610
611     if ischema:
612         # The following lines (up to the for loop) is to load the up to
613         # date schema into our current LDB
614         # a complete schema is needed as the insertion of attributes
615         # and class is done against it
616         # and the schema is self validated
617         # The double ldb open and schema validation is taken from the
618         # initial provision script
619         # it's not certain that it is really needed ....
620         sam_ldb = Ldb(session_info=session, credentials=creds, lp=lp)
621         schema = Schema(setup_path, names.domainsid, schemadn=basedn, serverdn=str(names.serverdn))
622         # Load the schema from the one we computed earlier
623         sam_ldb.set_schema_from_ldb(schema.ldb)
624         # And now we can connect to the DB - the schema won't be loaded
625         # from the DB
626         sam_ldb.connect(paths.samdb)
627     else:
628         sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp, options=["modules:samba_dsdb"])
629
630     sam_ldb.transaction_start()
631     # XXX: This needs to be wrapped in try/except so we
632     # abort on exceptions.
633     message(SIMPLE,"There are %d missing objects"%(len(listMissing)))
634     add_missing_entries(newsam_ldb,sam_ldb,names,basedn,listMissing)
635     changed = 0
636     for dn in listPresent:
637         reference = newsam_ldb.search(expression="dn=%s"%(str(dn)),base=basedn, scope=SCOPE_SUBTREE,controls=["search_options:1:2"])
638         current = sam_ldb.search(expression="dn=%s"%(str(dn)),base=basedn, scope=SCOPE_SUBTREE,controls=["search_options:1:2"])
639         if ((str(current[0].dn) != str(reference[0].dn)) and (str(current[0].dn).upper() == str(reference[0].dn).upper())):
640             message(CHANGE,"Name are the same but case change, let's rename %s to %s"%(str(current[0].dn),str(reference[0].dn)))
641             identic_rename(sam_ldb,reference[0].dn)
642             current = sam_ldb.search(expression="dn=%s"%(str(dn)),base=basedn, scope=SCOPE_SUBTREE,controls=["search_options:1:2"])
643
644         delta = sam_ldb.msg_diff(current[0],reference[0])
645         for att in hashAttrNotCopied.keys():
646             delta.remove(att)
647         for att in backlinked:
648             delta.remove(att)
649         delta.remove("parentGUID")
650         nb = 0
651         
652         for att in delta:
653             msgElt = delta.get(att)
654             if att == "dn":
655                 continue
656             if att == "name":
657                 delta.remove(att)
658                 continue
659             if not handle_security_desc(ischema,att,msgElt,hashallSD,current,reference):
660                 delta.remove(att)
661                 continue
662             if (not hashOverwrittenAtt.has_key(att) or not (hashOverwrittenAtt.get(att)&2^msgElt.flags())):
663                 if  hashOverwrittenAtt.has_key(att) and hashOverwrittenAtt.get(att)==never:
664                     delta.remove(att)
665                     continue
666                 if not handle_special_case(att,delta,reference,current,ischema) and msgElt.flags()!=FLAG_MOD_ADD:
667                     if opts.debugchange or opts.debugall:
668                         try:
669                             dump_denied_change(dn,att,messageEltFlagToString(msgElt.flags()),current[0][att],reference[0][att])
670                         except:
671                             # FIXME: Should catch an explicit exception here
672                             dump_denied_change(dn,att,messageEltFlagToString(msgElt.flags()),current[0][att],None)
673                     delta.remove(att)
674         delta.dn = dn
675         if len(delta.items()) >1:
676             attributes=",".join(delta.keys())
677             message(CHANGE,"%s is different from the reference one, changed attributes: %s"%(dn,attributes))
678             changed = changed + 1
679             sam_ldb.modify(delta)
680
681     sam_ldb.transaction_commit()
682     message(SIMPLE,"There are %d changed objects"%(changed))
683     return hashallSD
684
685
686 def check_updated_sd(newpaths, paths, creds, session, names):
687     """Check if the security descriptor in the upgraded provision are the same as the reference
688
689     :param newpaths: List of paths for different provision objects from the reference provision
690     :param paths: List of paths for different provision objects from the upgraded provision
691     :param creds: Credential for the authentification
692     :param session: Session for connexion
693     :param basedn: DN of the partition to update
694     :param names: List of key provision parameters"""
695     newsam_ldb = Ldb(newpaths.samdb, session_info=session, credentials=creds,lp=lp)
696     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp)
697     reference = newsam_ldb.search(expression="objectClass=*",base=str(names.rootdn), scope=SCOPE_SUBTREE,attrs=["dn","nTSecurityDescriptor"],controls=["search_options:1:2"])
698     current = sam_ldb.search(expression="objectClass=*",base=str(names.rootdn), scope=SCOPE_SUBTREE,attrs=["dn","nTSecurityDescriptor"],controls=["search_options:1:2"])
699     hash_new = {}
700     for i in range(0,len(reference)):
701         hash_new[str(reference[i]["dn"]).lower()] = ndr_unpack(security.descriptor,str(reference[i]["nTSecurityDescriptor"])).as_sddl(names.domainsid)
702
703     for i in range(0,len(current)):
704         key = str(current[i]["dn"]).lower()
705         if hash_new.has_key(key):
706             sddl = ndr_unpack(security.descriptor,str(current[i]["nTSecurityDescriptor"])).as_sddl(names.domainsid)
707             if sddl != hash_new[key]:
708                 print "%s new sddl/sddl in ref"%key
709                 print "%s\n%s"%(sddl,hash_new[key])
710
711
712 def update_sd(paths, creds, session, names):
713     """Update security descriptor of the current provision
714
715     During the different pre release of samba4 security descriptors (SD) were notarly broken (up to alpha11 included)
716     This function allow to get them back in order, this function make the assumption that nobody has modified manualy an SD
717     and so SD can be safely recalculated from scratch to get them right.
718
719     :param paths: List of paths for different provision objects from the upgraded provision
720     :param creds: Credential for the authentification
721     :param session: Session for connexion
722     :param names: List of key provision parameters"""
723
724     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp,options=["modules:samba_dsdb"])
725     sam_ldb.transaction_start()
726     # First update the SD for the rootdn
727     sam_ldb.set_session_info(session)
728     res = sam_ldb.search(expression="objectClass=*", base=str(names.rootdn), scope=SCOPE_BASE,\
729                          attrs=["dn", "whenCreated"], controls=["search_options:1:2"])
730     delta = Message()
731     delta.dn = Dn(sam_ldb,str(res[0]["dn"]))
732     descr = get_domain_descriptor(names.domainsid)
733     delta["nTSecurityDescriptor"] = MessageElement(descr, FLAG_MOD_REPLACE, "nTSecurityDescriptor")
734     sam_ldb.modify(delta,["recalculate_sd:0"])
735     # Then the config dn
736     res = sam_ldb.search(expression="objectClass=*",base=str(names.configdn), scope=SCOPE_BASE,attrs=["dn","whenCreated"],controls=["search_options:1:2"])
737     delta = Message()
738     delta.dn = Dn(sam_ldb,str(res[0]["dn"]))
739     descr = get_config_descriptor(names.domainsid)
740     delta["nTSecurityDescriptor"] = MessageElement(descr, FLAG_MOD_REPLACE, "nTSecurityDescriptor" )
741     sam_ldb.modify(delta,["recalculate_sd:0"])
742     # Then the schema dn
743     res = sam_ldb.search(expression="objectClass=*",base=str(names.schemadn), scope=SCOPE_BASE,attrs=["dn","whenCreated"],controls=["search_options:1:2"])
744     delta = Message()
745     delta.dn = Dn(sam_ldb,str(res[0]["dn"]))
746     descr = get_schema_descriptor(names.domainsid)
747     delta["nTSecurityDescriptor"] = MessageElement(descr, FLAG_MOD_REPLACE, "nTSecurityDescriptor" )
748     sam_ldb.modify(delta,["recalculate_sd:0"])
749
750     # Then the rest
751     hash = {}
752     res = sam_ldb.search(expression="objectClass=*",base=str(names.rootdn), scope=SCOPE_SUBTREE,attrs=["dn","whenCreated"],controls=["search_options:1:2"])
753     for obj in res:
754         if not (str(obj["dn"]) == str(names.rootdn) or
755             str(obj["dn"]) == str(names.configdn) or \
756             str(obj["dn"]) == str(names.schemadn)):
757             hash[str(obj["dn"])] = obj["whenCreated"]
758
759     listkeys = hash.keys()
760     listkeys.sort(dn_sort)
761
762     for key in listkeys:
763         try:
764             delta = Message()
765             delta.dn = Dn(sam_ldb,key)
766             delta["whenCreated"] = MessageElement(hash[key], FLAG_MOD_REPLACE, "whenCreated" )
767             sam_ldb.modify(delta,["recalculate_sd:0"])
768         except:
769             # XXX: We should always catch an explicit exception.
770             # What could go wrong here?
771             sam_ldb.transaction_cancel()
772             res = sam_ldb.search(expression="objectClass=*", base=str(names.rootdn), scope=SCOPE_SUBTREE,\
773                                  attrs=["dn","nTSecurityDescriptor"], controls=["search_options:1:2"])
774             print "bad stuff" +ndr_unpack(security.descriptor,str(res[0]["nTSecurityDescriptor"])).as_sddl(names.domainsid)
775             return
776     sam_ldb.transaction_commit()
777
778
779 def update_basesamdb(newpaths, paths, names):
780     """Update the provision container db: sam.ldb
781
782     :param newpaths: List of paths for different provision objects from the reference provision
783     :param paths: List of paths for different provision objects from the upgraded provision
784     :param names: List of key provision parameters"""
785
786     message(SIMPLE,"Copy samdb")
787     shutil.copy(newpaths.samdb,paths.samdb)
788
789     message(SIMPLE,"Update partitions filename if needed")
790     schemaldb = os.path.join(paths.private_dir, "schema.ldb")
791     configldb = os.path.join(paths.private_dir, "configuration.ldb")
792     usersldb = os.path.join(paths.private_dir, "users.ldb")
793     samldbdir = os.path.join(paths.private_dir, "sam.ldb.d")
794
795     if not os.path.isdir(samldbdir):
796         os.mkdir(samldbdir)
797         os.chmod(samldbdir,0700)
798     if os.path.isfile(schemaldb):
799         shutil.copy(schemaldb, os.path.join(samldbdir, "%s.ldb" % str(names.schemadn).upper()))
800         os.remove(schemaldb)
801     if os.path.isfile(usersldb):
802         shutil.copy(usersldb, os.path.join(samldbdir, "%s.ldb" % str(names.rootdn).upper()))
803         os.remove(usersldb)
804     if os.path.isfile(configldb):
805         shutil.copy(configldb, os.path.join(samldbdir, "%s.ldb" % str(names.configdn).upper()))
806         os.remove(configldb)
807
808
809 def update_privilege(newpaths, paths):
810     """Update the privilege database
811
812     :param newpaths: List of paths for different provision objects from the reference provision
813     :param paths: List of paths for different provision objects from the upgraded provision"""
814     message(SIMPLE, "Copy privilege")
815     shutil.copy(os.path.join(newpaths.private_dir, "privilege.ldb"), 
816                 os.path.join(paths.private_dir, "privilege.ldb"))
817
818
819 def update_samdb(newpaths, paths, creds, session, names):
820     """Upgrade the SAM DB contents for all the provision
821
822     :param newpaths: List of paths for different provision objects from the reference provision
823     :param paths: List of paths for different provision objects from the upgraded provision
824     :param creds: Credential for the authentification
825     :param session: Session for connexion
826     :param names: List of key provision parameters"""
827
828     message(SIMPLE, "Doing schema update")
829     hashdef = check_diff_name(newpaths,paths,creds,session,str(names.schemadn),names,1)
830     message(SIMPLE,"Done with schema update")
831     message(SIMPLE,"Scanning whole provision for updates and additions")
832     hashSD = check_diff_name(newpaths,paths,creds,session,str(names.rootdn),names,0)
833     message(SIMPLE,"Done with scanning")
834
835
836 def update_machine_account_password(paths, creds, session, names):
837     """Update (change) the password of the current DC both in the SAM db and in secret one
838
839     :param paths: List of paths for different provision objects from the upgraded provision
840     :param creds: Credential for the authentification
841     :param session: Session for connexion
842     :param names: List of key provision parameters"""
843
844     secrets_ldb = Ldb(paths.secrets, session_info=session,
845         credentials=creds,lp=lp)
846     secrets_ldb.transaction_start()
847     secrets_msg = secrets_ldb.search(expression=("samAccountName=%s$" % names.netbiosname), attrs=["secureChannelType"])
848     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp)
849     sam_ldb.transaction_start()
850     if int(secrets_msg[0]["secureChannelType"][0]) == SEC_CHAN_BDC:
851         res = sam_ldb.search(expression=("samAccountName=%s$" % names.netbiosname), attrs=[])
852         assert(len(res) == 1)
853
854         msg = Message(res[0].dn)
855         machinepass = samba.generate_random_password(128, 255)
856         msg["userPassword"] = MessageElement(machinepass, FLAG_MOD_REPLACE, "userPassword")
857         sam_ldb.modify(msg)
858
859         res = sam_ldb.search(expression=("samAccountName=%s$" % names.netbiosname),
860                      attrs=["msDs-keyVersionNumber"])
861         assert(len(res) == 1)
862         kvno = int(str(res[0]["msDs-keyVersionNumber"]))
863
864         secretsdb_self_join(secrets_ldb, domain=names.domain,
865                     realm=names.realm or sambaopts._lp.get('realm'),
866                     domainsid=names.domainsid,
867                     dnsdomain=names.dnsdomain,
868                     netbiosname=names.netbiosname,
869                     machinepass=machinepass,
870                     key_version_number=kvno,
871                     secure_channel_type=int(secrets_msg[0]["secureChannelType"][0]))
872         sam_ldb.transaction_prepare_commit()
873         secrets_ldb.transaction_prepare_commit()
874         sam_ldb.transaction_commit()
875         secrets_ldb.transaction_commit()
876     else:
877         secrets_ldb.transaction_cancel()
878
879
880 def update_gpo(paths,creds,session,names):
881     """Create missing GPO file object if needed
882
883     Set ACL correctly also.
884     """
885     dir = getpolicypath(paths.sysvol,names.dnsdomain,names.policyid)
886     if not os.path.isdir(dir):
887         create_gpo_struct(dir)
888
889     dir = getpolicypath(paths.sysvol,names.dnsdomain,names.policyid_dc)
890     if not os.path.isdir(dir):
891         create_gpo_struct(dir)
892     samdb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp)
893     set_gpo_acl(paths.sysvol, names.dnsdomain, names.domainsid,
894         names.domaindn, samdb, lp)
895
896
897 def updateOEMInfo(paths, creds, session,names):
898     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds, lp=lp,
899         options=["modules:samba_dsdb"])
900     res = sam_ldb.search(expression="(objectClass=*)",base=str(names.rootdn),
901                             scope=SCOPE_BASE, attrs=["dn","oEMInformation"])
902     if len(res) > 0:
903         info = res[0]["oEMInformation"]
904         info = "%s, upgrade to %s"%(info,version)
905         delta = Message()
906         delta.dn = Dn(sam_ldb,str(res[0]["dn"]))
907         descr = get_schema_descriptor(names.domainsid)
908         delta["oEMInformation"] = MessageElement(info, FLAG_MOD_REPLACE,
909             "oEMInformation" )
910         sam_ldb.modify(delta)
911
912
913 def setup_path(file):
914     return os.path.join(setup_dir, file)
915
916
917 if __name__ == '__main__':
918     # From here start the big steps of the program
919     # First get files paths
920     paths=get_paths(param,smbconf=smbconf)
921     paths.setup = setup_dir
922     # Guess all the needed names (variables in fact) from the current
923     # provision.
924
925     names = find_provision_key_parameters(param, creds, session, paths, smbconf)
926     if not sanitychecks(creds,session,names,paths):
927         message(SIMPLE,"Sanity checks for the upgrade fails, checks messages and correct it before rerunning upgradeprovision")
928         sys.exit(1)
929     # Let's see them
930     print_provision_key_parameters(names)
931     # With all this information let's create a fresh new provision used as reference
932     message(SIMPLE,"Creating a reference provision")
933     provisiondir = tempfile.mkdtemp(dir=paths.private_dir, prefix="referenceprovision")
934     newprovision(names, setup_dir, creds, session, smbconf, provisiondir, messageprovision)
935     # Get file paths of this new provision
936     newpaths = get_paths(param, targetdir=provisiondir)
937     populate_backlink(newpaths, creds, session,names.schemadn)
938     populate_dnsyntax(newpaths, creds, session,names.schemadn)
939     # Check the difference
940     update_basesamdb(newpaths, paths, names)
941
942     if opts.full:
943         update_samdb(newpaths, paths, creds, session, names)
944     update_secrets(newpaths, paths, creds, session)
945     update_privilege(newpaths, paths)
946     update_machine_account_password(paths, creds, session, names)
947     # SD should be created with admin but as some previous acl were so wrong that admin can't modify them we have first
948     # to recreate them with the good form but with system account and then give the ownership to admin ...
949     admin_session_info = admin_session(lp, str(names.domainsid))
950     message(SIMPLE, "Updating SD")
951     update_sd(paths, creds, session,names)
952     update_sd(paths, creds, admin_session_info, names)
953     check_updated_sd(newpaths, paths, creds, session, names)
954     updateOEMInfo(paths,creds,session,names)
955     message(SIMPLE, "Upgrade finished !")
956     # remove reference provision now that everything is done !
957     shutil.rmtree(provisiondir)