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