s4-provision Fix type error on existing idmap entries in s3 upgrade
[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, ProvisioningError
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 = 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, useeadb=False):
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             raise ProvisioningError("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 (it is the DNS name of the AD domain you wish to create.")
449         else:
450             realm = domainname.upper()
451             logger.warning("No realm specified in smb.conf file, assuming '%s'",
452                     realm)
453
454     # Find machine account and password
455     machinepass = None
456     machinerid = None
457     machinesid = None
458     next_rid = 1000
459
460     try:
461         machinepass = secrets_db.get_machine_password(netbiosname)
462     except:
463         pass
464
465     # We must close the direct pytdb database before the C code loads it
466     secrets_db.close()
467
468     # Connect to old password backend
469     passdb.set_secrets_dir(samba3.lp.get("private dir"))
470     s3db = samba3.get_sam_db()
471
472     # Get domain sid
473     try:
474         domainsid = passdb.get_global_sam_sid()
475     except passdb.error:
476         raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
477
478     # Get machine account, sid, rid
479     try:
480         machineacct = s3db.getsampwnam('%s$' % netbiosname)
481         machinesid, machinerid = machineacct.user_sid.split()
482     except:
483         pass
484
485     # Export account policy
486     logger.info("Exporting account policy")
487     policy = s3db.get_account_policy()
488
489     # Export groups from old passdb backend
490     logger.info("Exporting groups")
491     grouplist = s3db.enum_group_mapping()
492     groupmembers = {}
493     for group in grouplist:
494         sid, rid = group.sid.split()
495         if sid == domainsid:
496             if rid >= next_rid:
497                next_rid = rid + 1
498
499         # Get members for each group/alias
500         if group.sid_name_use == lsa.SID_NAME_ALIAS:
501             members = s3db.enum_aliasmem(group.sid)
502         elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
503             try:
504                 members = s3db.enum_group_members(group.sid)
505             except:
506                 continue
507         elif group.sid_name_use == lsa.SID_NAME_WKN_GRP:
508             logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)",
509                         group.nt_name, group.sid_name_use)
510             continue
511         else:
512             logger.warn("Ignoring group '%s' with sid_name_use=%d",
513                         group.nt_name, group.sid_name_use)
514             continue
515         groupmembers[group.nt_name] = members
516
517
518     # Export users from old passdb backend
519     logger.info("Exporting users")
520     userlist = s3db.search_users(0)
521     userdata = {}
522     uids = {}
523     admin_user = None
524     for entry in userlist:
525         if machinerid and machinerid == entry['rid']:
526             continue
527         username = entry['account_name']
528         if entry['rid'] < 1000:
529             logger.info("  Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
530             continue
531         if entry['rid'] >= next_rid:
532             next_rid = entry['rid'] + 1
533         
534         userdata[username] = s3db.getsampwnam(username)
535         try:
536             uids[username] = s3db.sid_to_id(userdata[username].user_sid)[0]
537         except:
538             try:
539                 uids[username] = pwd.getpwnam(username).pw_uid
540             except:
541                 pass
542
543         if not admin_user and username.lower() == 'root':
544             admin_user = username
545         if username.lower() == 'administrator':
546             admin_user = username
547
548     logger.info("Next rid = %d", next_rid)
549
550     # Do full provision
551     result = provision(logger, session_info, None,
552                        targetdir=targetdir, realm=realm, domain=domainname,
553                        domainsid=str(domainsid), next_rid=next_rid,
554                        dc_rid=machinerid,
555                        hostname=netbiosname, machinepass=machinepass,
556                        serverrole=serverrole, samdb_fill=FILL_FULL,
557                        useeadb=useeadb)
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