s4-provision cope with SID_NAME_WKN_GRP mappings in upgrade.py
[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:
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         elif group.sid_name_use == lsa.SID_NAME_WKN_GRP:
502             logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)",
503                         group.nt_name, group.sid_name_use)
504             continue
505         else:
506             logger.warn("Ignoring group '%s' with sid_name_use=%d",
507                         group.nt_name, group.sid_name_use)
508             continue
509         groupmembers[group.nt_name] = members
510
511
512     # Export users from old passdb backend
513     logger.info("Exporting users")
514     userlist = s3db.search_users(0)
515     userdata = {}
516     uids = {}
517     admin_user = None
518     for entry in userlist:
519         if machinerid and machinerid == entry['rid']:
520             continue
521         username = entry['account_name']
522         if entry['rid'] < 1000:
523             logger.info("  Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
524             continue
525         if entry['rid'] >= next_rid:
526             next_rid = entry['rid'] + 1
527         
528         userdata[username] = s3db.getsampwnam(username)
529         try:
530             uids[username] = s3db.sid_to_id(userdata[username].user_sid)[0]
531         except:
532             try:
533                 uids[username] = pwd.getpwnam(username).pw_uid
534             except:
535                 pass
536
537         if not admin_user and username.lower() == 'root':
538             admin_user = username
539         if username.lower() == 'administrator':
540             admin_user = username
541
542     logger.info("Next rid = %d", next_rid)
543
544     # Do full provision
545     result = provision(logger, session_info, None,
546                        targetdir=targetdir, realm=realm, domain=domainname,
547                        domainsid=str(domainsid), next_rid=next_rid,
548                        dc_rid=machinerid,
549                        hostname=netbiosname, machinepass=machinepass,
550                        serverrole=serverrole, samdb_fill=FILL_FULL)
551
552     # Import WINS database
553     logger.info("Importing WINS database")
554     import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
555
556     # Set Account policy
557     logger.info("Importing Account policy")
558     import_sam_policy(result.samdb, policy, logger)
559
560     # Migrate IDMAP database
561     logger.info("Importing idmap database")
562     import_idmap(result.idmap, samba3.get_idmap_db(), logger)
563
564     # Set the s3 context for samba4 configuration
565     new_lp_ctx = s3param.get_context()
566     new_lp_ctx.load(result.lp.configfile)
567     new_lp_ctx.set("private dir", result.lp.get("private dir"))
568     new_lp_ctx.set("state directory", result.lp.get("state directory"))
569     new_lp_ctx.set("lock directory", result.lp.get("lock directory"))
570
571     # Connect to samba4 backend
572     s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend"))
573
574     # Export groups to samba4 backend
575     logger.info("Importing groups")
576     for g in grouplist:
577         # Ignore uninitialized groups (gid = -1)
578         if g.gid != 0xffffffff:
579             add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
580             add_group_from_mapping_entry(result.samdb, g, logger)
581
582     # Export users to samba4 backend
583     logger.info("Importing users")
584     for username in userdata:
585         if username.lower() == 'administrator' or username.lower() == 'root':
586             continue
587         s4_passdb.add_sam_account(userdata[username])
588         if username in uids:
589             add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
590
591     logger.info("Adding users to groups")
592     for g in grouplist:
593         if g.nt_name in groupmembers:
594             add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
595
596     # Set password for administrator
597     if admin_user:
598         logger.info("Setting password for administrator")
599         admin_userdata = s4_passdb.getsampwnam("administrator")
600         admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
601         if userdata[admin_user].lanman_passwd:
602             admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
603         admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
604         if userdata[admin_user].pw_history:
605             admin_userdata.pw_history = userdata[admin_user].pw_history
606         s4_passdb.update_sam_account(admin_userdata)
607         logger.info("Administrator password has been set to password of user '%s'", admin_user)
608
609     # FIXME: import_registry(registry.Registry(), samba3.get_registry())
610     # FIXME: shares