7386d0b4b159c9088896bd4658bcd6b3d669fa01
[amitay/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(samdb, policy, logger):
39     """Import a Samba 3 policy.
40
41     :param samdb: Samba4 SAM database
42     :param policy: Samba3 account policy
43     :param logger: Logger object
44     """
45
46     # Following entries are used -
47     #    min password length, password history, minimum password age,
48     #    maximum password age, lockout duration
49     #
50     # Following entries are not used -
51     #    reset count minutes, user must logon to change password,
52     #    bad lockout minutes, disconnect time
53
54     m = ldb.Message()
55     m.dn = samdb.get_default_basedn()
56     m['a01'] = ldb.MessageElement(str(policy['min password length']), ldb.FLAG_MOD_REPLACE,
57                             'minPwdLength')
58     m['a02'] = ldb.MessageElement(str(policy['password history']), ldb.FLAG_MOD_REPLACE,
59                             'pwdHistoryLength')
60     m['a03'] = ldb.MessageElement(str(policy['minimum password age']), ldb.FLAG_MOD_REPLACE,
61                             'minPwdAge')
62     m['a04'] = ldb.MessageElement(str(policy['maximum password age']), ldb.FLAG_MOD_REPLACE,
63                             'maxPwdAge')
64     m['a05'] = ldb.MessageElement(str(policy['lockout duration']), ldb.FLAG_MOD_REPLACE,
65                             'lockoutDuration')
66
67     try:
68         samdb.modify(m)
69     except ldb.LdbError, e:
70         logger.warn("Could not set account policy, (%s)", str(e))
71
72
73 def add_idmap_entry(idmapdb, sid, xid, xid_type, logger):
74     """Create idmap entry
75
76     :param idmapdb: Samba4 IDMAP database
77     :param sid: user/group sid
78     :param xid: user/group id
79     :param xid_type: type of id (UID/GID)
80     :param logger: Logger object
81     """
82
83     # First try to see if we already have this entry
84     found = False
85     msg = idmapdb.search(expression='objectSid=%s' % str(sid))
86     if msg.count == 1:
87         found = True
88
89     if found:
90         try:
91             m = ldb.Message()
92             m.dn = ldb.Dn(idmapdb, msg[0]['dn'])
93             m['xidNumber'] = ldb.MessageElement(str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
94             m['type'] = ldb.MessageElement(xid_type, ldb.FLAG_MOD_REPLACE, 'type')
95             idmapdb.modify(m)
96         except ldb.LdbError, e:
97             logger.warn('Could not modify idmap entry for sid=%s, id=%s, type=%s (%s)',
98                             str(sid), str(xid), xid_type, str(e))
99     else:
100         try:
101             idmapdb.add({"dn": "CN=%s" % str(sid),
102                         "cn": str(sid),
103                         "objectClass": "sidMap",
104                         "objectSid": ndr_pack(sid),
105                         "type": xid_type,
106                         "xidNumber": str(xid)})
107         except ldb.LdbError, e:
108             logger.warn('Could not add idmap entry for sid=%s, id=%s, type=%s (%s)',
109                             str(sid), str(xid), xid_type, str(e))
110
111
112 def import_idmap(idmapdb, samba3_idmap, logger):
113     """Import idmap data.
114
115     :param idmapdb: Samba4 IDMAP database
116     :param samba3_idmap: Samba3 IDMAP database to import from
117     :param logger: Logger object
118     """
119     currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm())
120     lowerbound = currentxid
121     # FIXME: upperbound
122
123     m = ldb.Message()
124     m.dn = ldb.Dn(idmapdb, 'CN=CONFIG')
125     m['lowerbound'] = ldb.MessageElement(str(lowerbound), ldb.FLAG_MOD_REPLACE, 'lowerBound')
126     m['xidNumber'] = ldb.MessageElement(str(currentxid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
127     idmapdb.modify(m)
128
129     for id_type, xid in samba3_idmap.ids():
130         if id_type == 'UID':
131             xid_type = 'ID_TYPE_UID'
132         elif id_type == 'GID':
133             xid_type = 'ID_TYPE_GID'
134         else:
135             logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type)
136             continue
137
138         sid = samba3_idmap.get_sid(xid, id_type)
139         add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger)
140
141
142 def add_group_from_mapping_entry(samdb, groupmap, logger):
143     """Add or modify group from group mapping entry
144
145     param samdb: Samba4 SAM database
146     param groupmap: Groupmap entry
147     param logger: Logger object
148     """
149
150     # First try to see if we already have this entry
151     try:
152         msg = samdb.search(base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE)
153         found = True
154     except ldb.LdbError, (ecode, emsg):
155         if ecode == ldb.ERR_NO_SUCH_OBJECT:
156             found = False
157         else:
158             raise ldb.LdbError(ecode, emsg)
159
160     if found:
161         logger.warn('Group already exists sid=%s, groupname=%s existing_groupname=%s, Ignoring.',
162                             str(groupmap.sid), groupmap.nt_name, msg[0]['sAMAccountName'][0])
163     else:
164         if groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
165             return
166
167         m = ldb.Message()
168         m.dn = ldb.Dn(samdb, "CN=%s,CN=Users,%s" % (groupmap.nt_name, samdb.get_default_basedn()))
169         m['a01'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn')
170         m['a02'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass')
171         m['a03'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD, 'objectSid')
172         m['a04'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD, 'description')
173         m['a05'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'sAMAccountName')
174
175         if groupmap.sid_name_use == lsa.SID_NAME_ALIAS:
176             m['a06'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP), ldb.FLAG_MOD_ADD, 'groupType')
177
178         try:
179             samdb.add(m, controls=["relax:0"])
180         except ldb.LdbError, e:
181             logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e))
182
183
184 def add_users_to_group(samdb, group, members, logger):
185     """Add user/member to group/alias
186
187     param samdb: Samba4 SAM database
188     param group: Groupmap object
189     param members: List of member SIDs
190     param logger: Logger object
191     """
192     for member_sid in members:
193         m = ldb.Message()
194         m.dn = ldb.Dn(samdb, "<SID=%s" % str(group.sid))
195         m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_REPLACE, 'member')
196
197         try:
198             samdb.modify(m)
199         except ldb.LdbError, e:
200             logger.warn("Could not add member to group '%s'", groupmap.nt_name)
201
202
203 def import_wins(samba4_winsdb, samba3_winsdb):
204     """Import settings from a Samba3 WINS database.
205
206     :param samba4_winsdb: WINS database to import to
207     :param samba3_winsdb: WINS database to import from
208     """
209     version_id = 0
210
211     for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
212         version_id+=1
213
214         type = int(name.split("#", 1)[1], 16)
215
216         if type == 0x1C:
217             rType = 0x2
218         elif type & 0x80:
219             if len(ips) > 1:
220                 rType = 0x2
221             else:
222                 rType = 0x1
223         else:
224             if len(ips) > 1:
225                 rType = 0x3
226             else:
227                 rType = 0x0
228
229         if ttl > time.time():
230             rState = 0x0 # active
231         else:
232             rState = 0x1 # released
233
234         nType = ((nb_flags & 0x60)>>5)
235
236         samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
237                            "type": name.split("#")[1],
238                            "name": name.split("#")[0],
239                            "objectClass": "winsRecord",
240                            "recordType": str(rType),
241                            "recordState": str(rState),
242                            "nodeType": str(nType),
243                            "expireTime": ldb.timestring(ttl),
244                            "isStatic": "0",
245                            "versionID": str(version_id),
246                            "address": ips})
247
248     samba4_winsdb.add({"dn": "cn=VERSION",
249                        "cn": "VERSION",
250                        "objectClass": "winsMaxVersion",
251                        "maxVersion": str(version_id)})
252
253 def enable_samba3sam(samdb, ldapurl):
254     """Enable Samba 3 LDAP URL database.
255
256     :param samdb: SAM Database.
257     :param ldapurl: Samba 3 LDAP URL
258     """
259     samdb.modify_ldif("""
260 dn: @MODULES
261 changetype: modify
262 replace: @LIST
263 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
264 """)
265
266     samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
267
268
269 smbconf_keep = [
270     "dos charset",
271     "unix charset",
272     "display charset",
273     "comment",
274     "path",
275     "directory",
276     "workgroup",
277     "realm",
278     "netbios name",
279     "netbios aliases",
280     "netbios scope",
281     "server string",
282     "interfaces",
283     "bind interfaces only",
284     "security",
285     "auth methods",
286     "encrypt passwords",
287     "null passwords",
288     "obey pam restrictions",
289     "password server",
290     "smb passwd file",
291     "private dir",
292     "passwd chat",
293     "password level",
294     "lanman auth",
295     "ntlm auth",
296     "client NTLMv2 auth",
297     "client lanman auth",
298     "client plaintext auth",
299     "read only",
300     "hosts allow",
301     "hosts deny",
302     "log level",
303     "debuglevel",
304     "log file",
305     "smb ports",
306     "large readwrite",
307     "max protocol",
308     "min protocol",
309     "unicode",
310     "read raw",
311     "write raw",
312     "disable netbios",
313     "nt status support",
314     "max mux",
315     "max xmit",
316     "name resolve order",
317     "max wins ttl",
318     "min wins ttl",
319     "time server",
320     "unix extensions",
321     "use spnego",
322     "server signing",
323     "client signing",
324     "max connections",
325     "paranoid server security",
326     "socket options",
327     "strict sync",
328     "max print jobs",
329     "printable",
330     "print ok",
331     "printer name",
332     "printer",
333     "map system",
334     "map hidden",
335     "map archive",
336     "preferred master",
337     "prefered master",
338     "local master",
339     "browseable",
340     "browsable",
341     "wins server",
342     "wins support",
343     "csc policy",
344     "strict locking",
345     "preload",
346     "auto services",
347     "lock dir",
348     "lock directory",
349     "pid directory",
350     "socket address",
351     "copy",
352     "include",
353     "available",
354     "volume",
355     "fstype",
356     "panic action",
357     "msdfs root",
358     "host msdfs",
359     "winbind separator"]
360
361 def upgrade_smbconf(oldconf,mark):
362     """Remove configuration variables not present in Samba4
363
364     :param oldconf: Old configuration structure
365     :param mark: Whether removed configuration variables should be
366         kept in the new configuration as "samba3:<name>"
367     """
368     data = oldconf.data()
369     newconf = LoadParm()
370
371     for s in data:
372         for p in data[s]:
373             keep = False
374             for k in smbconf_keep:
375                 if smbconf_keep[k] == p:
376                     keep = True
377                     break
378
379             if keep:
380                 newconf.set(s, p, oldconf.get(s, p))
381             elif mark:
382                 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
383
384     return newconf
385
386 SAMBA3_PREDEF_NAMES = {
387         'HKLM': registry.HKEY_LOCAL_MACHINE,
388 }
389
390 def import_registry(samba4_registry, samba3_regdb):
391     """Import a Samba 3 registry database into the Samba 4 registry.
392
393     :param samba4_registry: Samba 4 registry handle.
394     :param samba3_regdb: Samba 3 registry database handle.
395     """
396     def ensure_key_exists(keypath):
397         (predef_name, keypath) = keypath.split("/", 1)
398         predef_id = SAMBA3_PREDEF_NAMES[predef_name]
399         keypath = keypath.replace("/", "\\")
400         return samba4_registry.create_key(predef_id, keypath)
401
402     for key in samba3_regdb.keys():
403         key_handle = ensure_key_exists(key)
404         for subkey in samba3_regdb.subkeys(key):
405             ensure_key_exists(subkey)
406         for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
407             key_handle.set_value(value_name, value_type, value_data)
408
409
410 def upgrade_from_samba3(samba3, logger, targetdir, session_info=None):
411     """Upgrade from samba3 database to samba4 AD database
412
413     :param samba3: samba3 object
414     :param logger: Logger object
415     :param targetdir: samba4 database directory
416     :param session_info: Session information
417     """
418
419     if samba3.lp.get("domain logons"):
420         serverrole = "domain controller"
421     else:
422         if samba3.lp.get("security") == "user":
423             serverrole = "standalone"
424         else:
425             serverrole = "member server"
426
427     domainname = samba3.lp.get("workgroup")
428     realm = samba3.lp.get("realm")
429     netbiosname = samba3.lp.get("netbios name")
430
431     # secrets db
432     secrets_db = samba3.get_secrets_db()
433
434     if not domainname:
435         domainname = secrets_db.domains()[0]
436         logger.warning("No workgroup specified in smb.conf file, assuming '%s'",
437                 domainname)
438
439     if not realm:
440         if serverrole == "domain controller":
441             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).")
442             return
443         else:
444             realm = domainname.upper()
445             logger.warning("No realm specified in smb.conf file, assuming '%s'",
446                     realm)
447
448     # Find machine account and password
449     machinepass = None
450     machinerid = None
451     machinesid = None
452     next_rid = 1000
453
454     try:
455         machinepass = secrets_db.get_machine_password(netbiosname)
456     except:
457         pass
458
459     # We must close the direct pytdb database before the C code loads it
460     secrets_db.close()
461
462     # Connect to old password backend
463     passdb.set_secrets_dir(samba3.lp.get("private dir"))
464     s3db = samba3.get_sam_db()
465
466     # Get domain sid
467     try:
468         domainsid = passdb.get_global_sam_sid()
469     except passdb.error:
470         raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
471
472     # Get machine account, sid, rid
473     try:
474         machineacct = s3db.getsampwnam('%s$' % netbiosname)
475         machinesid, machinerid = machineacct.user_sid.split()
476     except:
477         pass
478
479     # Export account policy
480     logger.info("Exporting account policy")
481     policy = s3db.get_account_policy()
482
483     # Export groups from old passdb backend
484     logger.info("Exporting groups")
485     grouplist = s3db.enum_group_mapping()
486     groupmembers = {}
487     for group in grouplist:
488         sid, rid = group.sid.split()
489         if sid == domainsid:
490             if rid >= next_rid:
491                next_rid = rid + 1
492
493         # Get members for each group/alias
494         if group.sid_name_use == lsa.SID_NAME_ALIAS or group.sid_name_use == lsa.SID_NAME_WKN_GRP:
495             members = s3db.enum_aliasmem(group.sid)
496         elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
497             try:
498                 members = s3db.enum_group_members(group.sid)
499             except:
500                 continue
501         else:
502             logger.warn("Ignoring group '%s' with sid_name_use=%d",
503                         group.nt_name, group.sid_name_use)
504             continue
505         groupmembers[group.nt_name] = members
506
507
508     # Export users from old passdb backend
509     logger.info("Exporting users")
510     userlist = s3db.search_users(0)
511     userdata = {}
512     uids = {}
513     admin_user = None
514     for entry in userlist:
515         if machinerid and machinerid == entry['rid']:
516             continue
517         username = entry['account_name']
518         if entry['rid'] < 1000:
519             logger.info("  Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
520             continue
521         if entry['rid'] >= next_rid:
522             next_rid = entry['rid'] + 1
523         
524         userdata[username] = s3db.getsampwnam(username)
525         try:
526             uids[username] = s3db.sid_to_id(userdata[username].user_sid)[0]
527         except:
528             try:
529                 uids[username] = pwd.getpwnam(username).pw_uid
530             except:
531                 pass
532
533         if not admin_user and username.lower() == 'root':
534             admin_user = username
535         if username.lower() == 'administrator':
536             admin_user = username
537
538     logger.info("Next rid = %d", next_rid)
539
540     # Do full provision
541     result = provision(logger, session_info, None,
542                        targetdir=targetdir, realm=realm, domain=domainname,
543                        domainsid=str(domainsid), next_rid=next_rid,
544                        dc_rid=machinerid,
545                        hostname=netbiosname, machinepass=machinepass,
546                        serverrole=serverrole, samdb_fill=FILL_FULL)
547
548     # Import WINS database
549     logger.info("Importing WINS database")
550     import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
551
552     # Set Account policy
553     logger.info("Importing Account policy")
554     import_sam_policy(result.samdb, policy, logger)
555
556     # Migrate IDMAP database
557     logger.info("Importing idmap database")
558     import_idmap(result.idmap, samba3.get_idmap_db(), logger)
559
560     # Set the s3 context for samba4 configuration
561     new_lp_ctx = s3param.get_context()
562     new_lp_ctx.load(result.lp.configfile)
563     new_lp_ctx.set("private dir", result.lp.get("private dir"))
564     new_lp_ctx.set("state directory", result.lp.get("state directory"))
565     new_lp_ctx.set("lock directory", result.lp.get("lock directory"))
566
567     # Connect to samba4 backend
568     s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend"))
569
570     # Export groups to samba4 backend
571     logger.info("Importing groups")
572     for g in grouplist:
573         # Ignore uninitialized groups (gid = -1)
574         if g.gid != 0xffffffff:
575             add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
576             add_group_from_mapping_entry(result.samdb, g, logger)
577
578     # Export users to samba4 backend
579     logger.info("Importing users")
580     for username in userdata:
581         if username.lower() == 'administrator' or username.lower() == 'root':
582             continue
583         s4_passdb.add_sam_account(userdata[username])
584         if username in uids:
585             add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
586
587     logger.info("Adding users to groups")
588     for g in grouplist:
589         if g.nt_name in groupmembers:
590             add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
591
592     # Set password for administrator
593     if admin_user:
594         logger.info("Setting password for administrator")
595         admin_userdata = s4_passdb.getsampwnam("administrator")
596         admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
597         if userdata[admin_user].lanman_passwd:
598             admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
599         admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
600         if userdata[admin_user].pw_history:
601             admin_userdata.pw_history = userdata[admin_user].pw_history
602         s4_passdb.update_sam_account(admin_userdata)
603         logger.info("Administrator password has been set to password of user '%s'", admin_user)
604
605     # FIXME: import_registry(registry.Registry(), samba3.get_registry())
606     # FIXME: shares