s3_upgrade: Set the administrator password on upgrade
[idra/samba.git] / source4 / scripting / python / samba / upgrade.py
1 # backend code for upgrading from Samba3
2 # Copyright Jelmer Vernooij 2005-2007
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 #
17
18 """Support code for upgrading from Samba 3 to Samba 4."""
19
20 __docformat__ = "restructuredText"
21
22 import grp
23 import ldb
24 import time
25 import pwd
26
27 from samba import Ldb, registry
28 from samba.param import LoadParm
29 from samba.provision import provision, FILL_FULL
30 from samba.samba3 import passdb
31 from samba.samba3 import param as s3param
32 from samba.dcerpc import lsa
33 from samba.dcerpc.security import dom_sid
34 from samba import dsdb
35 from samba.ndr import ndr_pack
36
37
38 def import_sam_policy(samldb, policy, dn):
39     """Import a Samba 3 policy database."""
40     samldb.modify_ldif("""
41 dn: %s
42 changetype: modify
43 replace: minPwdLength
44 minPwdLength: %d
45 pwdHistoryLength: %d
46 minPwdAge: %d
47 maxPwdAge: %d
48 lockoutDuration: %d
49 samba3ResetCountMinutes: %d
50 samba3UserMustLogonToChangePassword: %d
51 samba3BadLockoutMinutes: %d
52 samba3DisconnectTime: %d
53
54 """ % (dn, policy.min_password_length,
55     policy.password_history, policy.minimum_password_age,
56     policy.maximum_password_age, policy.lockout_duration,
57     policy.reset_count_minutes, policy.user_must_logon_to_change_password,
58     policy.bad_lockout_minutes, policy.disconnect_time))
59
60
61 def import_sam_account(samldb,acc,domaindn,domainsid):
62     """Import a Samba 3 SAM account.
63
64     :param samldb: Samba 4 SAM Database handle
65     :param acc: Samba 3 account
66     :param domaindn: Domain DN
67     :param domainsid: Domain SID."""
68     if acc.nt_username is None or acc.nt_username == "":
69         acc.nt_username = acc.username
70
71     if acc.fullname is None:
72         try:
73             acc.fullname = pwd.getpwnam(acc.username)[4].split(",")[0]
74         except KeyError:
75             pass
76
77     if acc.fullname is None:
78         acc.fullname = acc.username
79
80     assert acc.fullname is not None
81     assert acc.nt_username is not None
82
83     samldb.add({
84         "dn": "cn=%s,%s" % (acc.fullname, domaindn),
85         "objectClass": ["top", "user"],
86         "lastLogon": str(acc.logon_time),
87         "lastLogoff": str(acc.logoff_time),
88         "unixName": acc.username,
89         "sAMAccountName": acc.nt_username,
90         "cn": acc.nt_username,
91         "description": acc.acct_desc,
92         "primaryGroupID": str(acc.group_rid),
93         "badPwdcount": str(acc.bad_password_count),
94         "logonCount": str(acc.logon_count),
95         "samba3Domain": acc.domain,
96         "samba3DirDrive": acc.dir_drive,
97         "samba3MungedDial": acc.munged_dial,
98         "samba3Homedir": acc.homedir,
99         "samba3LogonScript": acc.logon_script,
100         "samba3ProfilePath": acc.profile_path,
101         "samba3Workstations": acc.workstations,
102         "samba3KickOffTime": str(acc.kickoff_time),
103         "samba3BadPwdTime": str(acc.bad_password_time),
104         "samba3PassLastSetTime": str(acc.pass_last_set_time),
105         "samba3PassCanChangeTime": str(acc.pass_can_change_time),
106         "samba3PassMustChangeTime": str(acc.pass_must_change_time),
107         "objectSid": "%s-%d" % (domainsid, acc.user_rid),
108         "lmPwdHash:": acc.lm_password,
109         "ntPwdHash:": acc.nt_password,
110         })
111
112
113 def import_sam_group(samldb, sid, gid, sid_name_use, nt_name, comment, domaindn):
114     """Upgrade a SAM group.
115
116     :param samldb: SAM database.
117     :param gid: Group GID
118     :param sid_name_use: SID name use
119     :param nt_name: NT Group Name
120     :param comment: NT Group Comment
121     :param domaindn: Domain DN
122     """
123
124     if sid_name_use == 5: # Well-known group
125         return None
126
127     if nt_name in ("Domain Guests", "Domain Users", "Domain Admins"):
128         return None
129
130     if gid == -1:
131         gr = grp.getgrnam(nt_name)
132     else:
133         gr = grp.getgrgid(gid)
134
135     if gr is None:
136         unixname = "UNKNOWN"
137     else:
138         unixname = gr.gr_name
139
140     assert unixname is not None
141
142     samldb.add({
143         "dn": "cn=%s,%s" % (nt_name, domaindn),
144         "objectClass": ["top", "group"],
145         "description": comment,
146         "cn": nt_name,
147         "objectSid": sid,
148         "unixName": unixname,
149         "samba3SidNameUse": str(sid_name_use)
150         })
151
152
153 def add_idmap_entry(idmapdb, sid, xid, xid_type, logger):
154     """Create idmap entry"""
155
156     # First try to see if we already have this entry
157     found = False
158     try:
159         msg = idmapdb.search(expression='objectSid=%s' % str(sid))
160         if msg.count == 1:
161             found = True
162     except Exception, e:
163         raise e
164
165     if found:
166         print msg.count
167         print dir(msg)
168         try:
169             m = ldb.Message()
170             m.dn = ldb.Dn(idmapdb, msg[0]['dn'])
171             m['xidNumber'] = ldb.MessageElement(str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
172             m['type'] = ldb.MessageElement(xid_type, ldb.FLAG_MOD_REPLACE, 'type')
173             idmapdb.modify(m)
174         except ldb.LdbError, e:
175             logger.warn('Could not modify idmap entry for sid=%s, id=%s, type=%s (%s)',
176                             str(sid), str(xid), xid_type, str(e))
177         except Exception, e:
178             raise e
179     else:
180         try:
181             idmapdb.add({"dn": "CN=%s" % str(sid),
182                         "cn": str(sid),
183                         "objectClass": "sidMap",
184                         "objectSid": ndr_pack(sid),
185                         "type": xid_type,
186                         "xidNumber": str(xid)})
187         except ldb.LdbError, e:
188             logger.warn('Could not add idmap entry for sid=%s, id=%s, type=%s (%s)',
189                             str(sid), str(xid), xid_type, str(e))
190         except Exception, e:
191             raise e
192
193
194 def import_idmap(idmapdb, samba3_idmap, logger):
195     """Import idmap data.
196
197     :param samba3_idmap: Samba 3 IDMAP database to import from
198     """
199
200     currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm())
201     lowerbound = currentxid
202     # FIXME: upperbound
203
204     m = ldb.Message()
205     m.dn = ldb.Dn(idmapdb, 'CN=CONFIG')
206     m['lowerbound'] = ldb.MessageElement(str(lowerbound), ldb.FLAG_MOD_REPLACE, 'lowerBound')
207     m['xidNumber'] = ldb.MessageElement(str(currentxid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
208     idmapdb.modify(m)
209
210     for id_type, xid in samba3_idmap.ids():
211         if id_type == 'UID':
212             xid_type = 'ID_TYPE_UID'
213         elif id_type == 'GID':
214             xid_type = 'ID_TYPE_GID'
215         else:
216             logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type)
217             continue
218
219         sid = samba3_idmap.get_sid(xid, id_type)
220         add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger)
221
222
223 def add_group_from_mapping_entry(samdb, groupmap, logger):
224     """Add or modify group from group mapping entry"""
225
226     # First try to see if we already have this entry
227     try:
228         msg = samdb.search(base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE)
229         found = True
230     except ldb.LdbError, (ecode, emsg):
231         if ecode == ldb.ERR_NO_SUCH_OBJECT:
232             found = False
233         else:
234             raise ldb.LdbError(ecode, emsg)
235     except Exception, e:
236         raise e
237
238     if found:
239         logger.warn('Group already exists sid=%s, groupname=%s existing_groupname=%s, Ignoring.',
240                             str(groupmap.sid), groupmap.nt_name, msg[0]['sAMAccountName'][0])
241     else:
242         if groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
243             return
244
245         m = ldb.Message()
246         m.dn = ldb.Dn(samdb, "CN=%s,CN=Users,%s" % (groupmap.nt_name, samdb.get_default_basedn()))
247         m['a01'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn')
248         m['a02'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass')
249         m['a03'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD, 'objectSid')
250         m['a04'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD, 'description')
251         m['a05'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'sAMAccountName')
252
253         if groupmap.sid_name_use == lsa.SID_NAME_ALIAS:
254             m['a06'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP), ldb.FLAG_MOD_ADD, 'groupType')
255
256         try:
257             samdb.add(m, controls=["relax:0"])
258         except ldb.LdbError, e:
259             logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e))
260         except Exception, e:
261             raise(e)
262
263
264 def add_users_to_group(samdb, group, members, logger):
265     """Add user/member to group/alias"""
266
267     for member_sid in members:
268         m = ldb.Message()
269         m.dn = ldb.Dn(samdb, "<SID=%s" % str(group.sid))
270         m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_REPLACE, 'member')
271
272         try:
273             samdb.modify(m)
274         except ldb.LdbError, e:
275             logger.warn("Could not add member to group '%s'", groupmap.nt_name)
276         except Exception, e:
277             raise(e)
278
279
280 def import_wins(samba4_winsdb, samba3_winsdb):
281     """Import settings from a Samba3 WINS database.
282
283     :param samba4_winsdb: WINS database to import to
284     :param samba3_winsdb: WINS database to import from
285     """
286     version_id = 0
287
288     for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
289         version_id+=1
290
291         type = int(name.split("#", 1)[1], 16)
292
293         if type == 0x1C:
294             rType = 0x2
295         elif type & 0x80:
296             if len(ips) > 1:
297                 rType = 0x2
298             else:
299                 rType = 0x1
300         else:
301             if len(ips) > 1:
302                 rType = 0x3
303             else:
304                 rType = 0x0
305
306         if ttl > time.time():
307             rState = 0x0 # active
308         else:
309             rState = 0x1 # released
310
311         nType = ((nb_flags & 0x60)>>5)
312
313         samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
314                            "type": name.split("#")[1],
315                            "name": name.split("#")[0],
316                            "objectClass": "winsRecord",
317                            "recordType": str(rType),
318                            "recordState": str(rState),
319                            "nodeType": str(nType),
320                            "expireTime": ldb.timestring(ttl),
321                            "isStatic": "0",
322                            "versionID": str(version_id),
323                            "address": ips})
324
325     samba4_winsdb.add({"dn": "cn=VERSION",
326                        "cn": "VERSION",
327                        "objectClass": "winsMaxVersion",
328                        "maxVersion": str(version_id)})
329
330 def enable_samba3sam(samdb, ldapurl):
331     """Enable Samba 3 LDAP URL database.
332
333     :param samdb: SAM Database.
334     :param ldapurl: Samba 3 LDAP URL
335     """
336     samdb.modify_ldif("""
337 dn: @MODULES
338 changetype: modify
339 replace: @LIST
340 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
341 """)
342
343     samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
344
345
346 smbconf_keep = [
347     "dos charset",
348     "unix charset",
349     "display charset",
350     "comment",
351     "path",
352     "directory",
353     "workgroup",
354     "realm",
355     "netbios name",
356     "netbios aliases",
357     "netbios scope",
358     "server string",
359     "interfaces",
360     "bind interfaces only",
361     "security",
362     "auth methods",
363     "encrypt passwords",
364     "null passwords",
365     "obey pam restrictions",
366     "password server",
367     "smb passwd file",
368     "private dir",
369     "passwd chat",
370     "password level",
371     "lanman auth",
372     "ntlm auth",
373     "client NTLMv2 auth",
374     "client lanman auth",
375     "client plaintext auth",
376     "read only",
377     "hosts allow",
378     "hosts deny",
379     "log level",
380     "debuglevel",
381     "log file",
382     "smb ports",
383     "large readwrite",
384     "max protocol",
385     "min protocol",
386     "unicode",
387     "read raw",
388     "write raw",
389     "disable netbios",
390     "nt status support",
391     "max mux",
392     "max xmit",
393     "name resolve order",
394     "max wins ttl",
395     "min wins ttl",
396     "time server",
397     "unix extensions",
398     "use spnego",
399     "server signing",
400     "client signing",
401     "max connections",
402     "paranoid server security",
403     "socket options",
404     "strict sync",
405     "max print jobs",
406     "printable",
407     "print ok",
408     "printer name",
409     "printer",
410     "map system",
411     "map hidden",
412     "map archive",
413     "preferred master",
414     "prefered master",
415     "local master",
416     "browseable",
417     "browsable",
418     "wins server",
419     "wins support",
420     "csc policy",
421     "strict locking",
422     "preload",
423     "auto services",
424     "lock dir",
425     "lock directory",
426     "pid directory",
427     "socket address",
428     "copy",
429     "include",
430     "available",
431     "volume",
432     "fstype",
433     "panic action",
434     "msdfs root",
435     "host msdfs",
436     "winbind separator"]
437
438 def upgrade_smbconf(oldconf,mark):
439     """Remove configuration variables not present in Samba4
440
441     :param oldconf: Old configuration structure
442     :param mark: Whether removed configuration variables should be
443         kept in the new configuration as "samba3:<name>"
444     """
445     data = oldconf.data()
446     newconf = LoadParm()
447
448     for s in data:
449         for p in data[s]:
450             keep = False
451             for k in smbconf_keep:
452                 if smbconf_keep[k] == p:
453                     keep = True
454                     break
455
456             if keep:
457                 newconf.set(s, p, oldconf.get(s, p))
458             elif mark:
459                 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
460
461     return newconf
462
463 SAMBA3_PREDEF_NAMES = {
464         'HKLM': registry.HKEY_LOCAL_MACHINE,
465 }
466
467 def import_registry(samba4_registry, samba3_regdb):
468     """Import a Samba 3 registry database into the Samba 4 registry.
469
470     :param samba4_registry: Samba 4 registry handle.
471     :param samba3_regdb: Samba 3 registry database handle.
472     """
473     def ensure_key_exists(keypath):
474         (predef_name, keypath) = keypath.split("/", 1)
475         predef_id = SAMBA3_PREDEF_NAMES[predef_name]
476         keypath = keypath.replace("/", "\\")
477         return samba4_registry.create_key(predef_id, keypath)
478
479     for key in samba3_regdb.keys():
480         key_handle = ensure_key_exists(key)
481         for subkey in samba3_regdb.subkeys(key):
482             ensure_key_exists(subkey)
483         for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
484             key_handle.set_value(value_name, value_type, value_data)
485
486
487 def upgrade_from_samba3(samba3, logger, session_info, smbconf, targetdir):
488     """Upgrade from samba3 database to samba4 AD database
489     """
490
491     # Read samba3 smb.conf
492     oldconf = s3param.get_context();
493     oldconf.load(smbconf)
494
495     if oldconf.get("domain logons"):
496         serverrole = "domain controller"
497     else:
498         if oldconf.get("security") == "user":
499             serverrole = "standalone"
500         else:
501             serverrole = "member server"
502
503     domainname = oldconf.get("workgroup")
504     realm = oldconf.get("realm")
505     netbiosname = oldconf.get("netbios name")
506
507     # secrets db
508     secrets_db = samba3.get_secrets_db()
509
510     if not domainname:
511         domainname = secrets_db.domains()[0]
512         logger.warning("No domain specified in smb.conf file, assuming '%s'",
513                 domainname)
514
515     if not realm:
516         if oldconf.get("domain logons"):
517             logger.warning("No realm specified in smb.conf file and being a DC. That upgrade path doesn't work! Please add a 'realm' directive to your old smb.conf to let us know which one you want to use (generally it's the upcased DNS domainname).")
518             return
519         else:
520             realm = domainname.upper()
521             logger.warning("No realm specified in smb.conf file, assuming '%s'",
522                     realm)
523
524     # Find machine account and password
525     machinepass = None
526     machinerid = None
527     machinesid = None
528     next_rid = 1000
529
530     try:
531         machinepass = secrets_db.get_machine_password(netbiosname)
532     except:
533         pass
534
535     # We must close the direct pytdb database before the C code loads it
536     secrets_db.close()
537
538     passdb.set_secrets_dir(samba3.privatedir)
539
540     # Get domain sid
541     try:
542         domainsid = passdb.get_global_sam_sid()
543     except:
544         raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
545
546     # Get machine account, sid, rid
547     try:
548         machineacct = old_passdb.getsampwnam('%s$' % netbiosname)
549         machinesid, machinerid = machineacct.user_sid.split()
550     except:
551         pass
552
553     # Connect to old password backend
554     old_passdb = passdb.PDB(oldconf.get('passdb backend'))
555
556     # Import groups from old passdb backend
557     logger.info("Exporting groups")
558     grouplist = old_passdb.enum_group_mapping()
559     groupmembers = {}
560     for group in grouplist:
561         sid, rid = group.sid.split()
562         if sid == domainsid:
563             if rid >= next_rid:
564                next_rid = rid + 1
565
566         # Get members for each group/alias
567         if group.sid_name_use == lsa.SID_NAME_ALIAS or group.sid_name_use == lsa.SID_NAME_WKN_GRP:
568             members = old_passdb.enum_aliasmem(group.sid)
569         elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
570             try:
571                 members = old_passdb.enum_group_members(group.sid)
572             except:
573                 continue
574         else:
575             logger.warn("Ignoring group '%s' with sid_name_use=%d",
576                         group.nt_name, group.sid_name_use)
577             continue
578         groupmembers[group.nt_name] = members
579
580
581     # Import users from old passdb backend
582     logger.info("Exporting users")
583     userlist = old_passdb.search_users(0)
584     userdata = {}
585     uids = {}
586     admin_user = None
587     for entry in userlist:
588         if machinerid and machinerid == entry['rid']:
589             continue
590         username = entry['account_name']
591         if entry['rid'] < 1000:
592             logger.info("  Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
593             continue
594         if entry['rid'] >= next_rid:
595             next_rid = entry['rid'] + 1
596         
597         userdata[username] = old_passdb.getsampwnam(username)
598         try:
599             uids[username] = old_passdb.sid_to_id(userdata[username].user_sid)[0]
600         except:
601             try:
602                 uids[username] = pwd.getpwnam(username).pw_uid
603             except:
604                 pass
605
606         if not admin_user and username.lower() == 'root':
607             admin_user = username
608         if username.lower() == 'administrator':
609             admin_user = username
610
611
612     logger.info("Next rid = %d", next_rid)
613
614     # Do full provision
615     result = provision(logger, session_info, None,
616                        targetdir=targetdir, realm=realm, domain=domainname,
617                        domainsid=str(domainsid), next_rid=next_rid,
618                        dc_rid=machinerid,
619                        hostname=netbiosname, machinepass=machinepass,
620                        serverrole=serverrole, samdb_fill=FILL_FULL)
621
622     logger.info("Import WINS")
623     import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
624
625     new_smbconf = result.lp.configfile
626     newconf = s3param.get_context()
627     newconf.load(new_smbconf)
628
629     # Migrate idmap
630     logger.info("Migrating idmap database")
631     import_idmap(result.idmap, samba3.get_idmap_db(), logger)
632
633     # Connect to samba4 backend
634     new_passdb = passdb.PDB('samba4')
635
636     # Export groups to samba4 backend
637     logger.info("Importing groups")
638     for g in grouplist:
639         # Ignore uninitialized groups (gid = -1)
640         if g.gid != 0xffffffff:
641             add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
642             add_group_from_mapping_entry(result.samdb, g, logger)
643
644     # Export users to samba4 backend
645     logger.info("Importing users")
646     for username in userdata:
647         if username.lower() == 'administrator' or username.lower() == 'root':
648             continue
649         new_passdb.add_sam_account(userdata[username])
650         if username in uids:
651             add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
652
653     logger.info("Adding users to groups")
654     for g in grouplist:
655         if g.nt_name in groupmembers:
656             add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
657
658     # Set password for administrator
659     if admin_user:
660         logger.info("Setting password for administrator")
661         admin_userdata = new_passdb.getsampwnam("administrator")
662         admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
663         if userdata[admin_user].lanman_passwd:
664             admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
665         admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
666         if userdata[admin_user].pw_history:
667             admin_userdata.pw_history = userdata[admin_user].pw_history
668         new_passdb.update_sam_account(admin_userdata)
669         logger.info("Administrator password has been set to password of user '%s'", admin_user)
670
671     # FIXME: import_registry(registry.Registry(), samba3.get_registry())