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