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