1 # backend code for upgrading from Samba3
2 # Copyright Jelmer Vernooij 2005-2007
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.
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.
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/>.
18 """Support code for upgrading from Samba 3 to Samba 4."""
20 __docformat__ = "restructuredText"
26 from samba import Ldb, registry
27 from samba.param import LoadParm
28 from samba.provision import provision, FILL_FULL, ProvisioningError
29 from samba.samba3 import passdb
30 from samba.samba3 import param as s3param
31 from samba.dcerpc import lsa, samr, security
32 from samba.dcerpc.security import dom_sid
33 from samba import dsdb
34 from samba.ndr import ndr_pack
35 from samba import unix2nttime
38 def import_sam_policy(samdb, policy, logger):
39 """Import a Samba 3 policy.
41 :param samdb: Samba4 SAM database
42 :param policy: Samba3 account policy
43 :param logger: Logger object
46 # Following entries are used -
47 # min password length, password history, minimum password age,
48 # maximum password age, lockout duration
50 # Following entries are not used -
51 # reset count minutes, user must logon to change password,
52 # bad lockout minutes, disconnect time
55 m.dn = samdb.get_default_basedn()
56 m['a01'] = ldb.MessageElement(str(policy['min password length']),
57 ldb.FLAG_MOD_REPLACE, 'minPwdLength')
58 m['a02'] = ldb.MessageElement(str(policy['password history']),
59 ldb.FLAG_MOD_REPLACE, 'pwdHistoryLength')
61 min_pw_age_unix = policy['minimum password age']
62 min_pw_age_nt = 0 - unix2nttime(min_pw_age_unix)
63 m['a03'] = ldb.MessageElement(str(min_pw_age_nt), ldb.FLAG_MOD_REPLACE,
66 max_pw_age_unix = policy['maximum password age']
67 if (max_pw_age_unix == 0xFFFFFFFF):
70 max_pw_age_nt = unix2nttime(max_pw_age_unix)
72 m['a04'] = ldb.MessageElement(str(max_pw_age_nt), ldb.FLAG_MOD_REPLACE,
75 lockout_duration_mins = policy['lockout duration']
76 lockout_duration_nt = unix2nttime(lockout_duration_mins * 60)
78 m['a05'] = ldb.MessageElement(str(lockout_duration_nt),
79 ldb.FLAG_MOD_REPLACE, 'lockoutDuration')
83 except ldb.LdbError, e:
84 logger.warn("Could not set account policy, (%s)", str(e))
87 def add_idmap_entry(idmapdb, sid, xid, xid_type, logger):
90 :param idmapdb: Samba4 IDMAP database
91 :param sid: user/group sid
92 :param xid: user/group id
93 :param xid_type: type of id (UID/GID)
94 :param logger: Logger object
97 # First try to see if we already have this entry
99 msg = idmapdb.search(expression='objectSid=%s' % str(sid))
107 m['xidNumber'] = ldb.MessageElement(
108 str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
109 m['type'] = ldb.MessageElement(
110 xid_type, ldb.FLAG_MOD_REPLACE, 'type')
112 except ldb.LdbError, e:
114 'Could not modify idmap entry for sid=%s, id=%s, type=%s (%s)',
115 str(sid), str(xid), xid_type, str(e))
118 idmapdb.add({"dn": "CN=%s" % str(sid),
120 "objectClass": "sidMap",
121 "objectSid": ndr_pack(sid),
123 "xidNumber": str(xid)})
124 except ldb.LdbError, e:
126 'Could not add idmap entry for sid=%s, id=%s, type=%s (%s)',
127 str(sid), str(xid), xid_type, str(e))
130 def import_idmap(idmapdb, samba3, logger):
131 """Import idmap data.
133 :param idmapdb: Samba4 IDMAP database
134 :param samba3_idmap: Samba3 IDMAP database to import from
135 :param logger: Logger object
139 samba3_idmap = samba3.get_idmap_db()
141 logger.warn('Cannot open idmap database, Ignoring: %s', str(e))
144 currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm())
145 lowerbound = currentxid
149 m.dn = ldb.Dn(idmapdb, 'CN=CONFIG')
150 m['lowerbound'] = ldb.MessageElement(
151 str(lowerbound), ldb.FLAG_MOD_REPLACE, 'lowerBound')
152 m['xidNumber'] = ldb.MessageElement(
153 str(currentxid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
156 for id_type, xid in samba3_idmap.ids():
158 xid_type = 'ID_TYPE_UID'
159 elif id_type == 'GID':
160 xid_type = 'ID_TYPE_GID'
162 logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type)
165 sid = samba3_idmap.get_sid(xid, id_type)
166 add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger)
169 def add_group_from_mapping_entry(samdb, groupmap, logger):
170 """Add or modify group from group mapping entry
172 param samdb: Samba4 SAM database
173 param groupmap: Groupmap entry
174 param logger: Logger object
177 # First try to see if we already have this entry
180 base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE)
182 except ldb.LdbError, (ecode, emsg):
183 if ecode == ldb.ERR_NO_SUCH_OBJECT:
186 raise ldb.LdbError(ecode, emsg)
189 logger.warn('Group already exists sid=%s, groupname=%s existing_groupname=%s, Ignoring.',
190 str(groupmap.sid), groupmap.nt_name, msg[0]['sAMAccountName'][0])
192 if groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
193 # In a lot of Samba3 databases, aliases are marked as well known groups
194 (group_dom_sid, rid) = groupmap.sid.split()
195 if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
199 m.dn = ldb.Dn(samdb, "CN=%s,CN=Users,%s" % (groupmap.nt_name, samdb.get_default_basedn()))
200 m['a01'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn')
201 m['a02'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass')
202 m['a03'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD, 'objectSid')
203 m['a04'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD, 'description')
204 m['a05'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'sAMAccountName')
206 # Fix up incorrect 'well known' groups that are actually builtin (per test above) to be aliases
207 if groupmap.sid_name_use == lsa.SID_NAME_ALIAS or groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
208 m['a06'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP), ldb.FLAG_MOD_ADD, 'groupType')
211 samdb.add(m, controls=["relax:0"])
212 except ldb.LdbError, e:
213 logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e))
216 def add_users_to_group(samdb, group, members, logger):
217 """Add user/member to group/alias
219 param samdb: Samba4 SAM database
220 param group: Groupmap object
221 param members: List of member SIDs
222 param logger: Logger object
224 for member_sid in members:
226 m.dn = ldb.Dn(samdb, "<SID=%s>" % str(group.sid))
227 m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_ADD, 'member')
231 except ldb.LdbError, (ecode, emsg):
232 if ecode == ldb.ERR_ENTRY_ALREADY_EXISTS:
233 logger.info("skipped re-adding member '%s' to group '%s': %s", member_sid, group.sid, emsg)
234 elif ecode == ldb.ERR_NO_SUCH_OBJECT:
235 raise ProvisioningError("Could not add member '%s' to group '%s' as either group or user record doesn't exist: %s" % (member_sid, group.sid, emsg))
237 raise ProvisioningError("Could not add member '%s' to group '%s': %s" % (member_sid, group.sid, emsg))
240 def import_wins(samba4_winsdb, samba3_winsdb):
241 """Import settings from a Samba3 WINS database.
243 :param samba4_winsdb: WINS database to import to
244 :param samba3_winsdb: WINS database to import from
249 for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
252 type = int(name.split("#", 1)[1], 16)
267 if ttl > time.time():
268 rState = 0x0 # active
270 rState = 0x1 # released
272 nType = ((nb_flags & 0x60) >> 5)
274 samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
275 "type": name.split("#")[1],
276 "name": name.split("#")[0],
277 "objectClass": "winsRecord",
278 "recordType": str(rType),
279 "recordState": str(rState),
280 "nodeType": str(nType),
281 "expireTime": ldb.timestring(ttl),
283 "versionID": str(version_id),
286 samba4_winsdb.add({"dn": "cn=VERSION",
288 "objectClass": "winsMaxVersion",
289 "maxVersion": str(version_id)})
292 def enable_samba3sam(samdb, ldapurl):
293 """Enable Samba 3 LDAP URL database.
295 :param samdb: SAM Database.
296 :param ldapurl: Samba 3 LDAP URL
298 samdb.modify_ldif("""
302 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
305 samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
322 "bind interfaces only",
327 "obey pam restrictions",
335 "client NTLMv2 auth",
336 "client lanman auth",
337 "client plaintext auth",
355 "name resolve order",
364 "paranoid server security",
401 def upgrade_smbconf(oldconf, mark):
402 """Remove configuration variables not present in Samba4
404 :param oldconf: Old configuration structure
405 :param mark: Whether removed configuration variables should be
406 kept in the new configuration as "samba3:<name>"
408 data = oldconf.data()
414 for k in smbconf_keep:
415 if smbconf_keep[k] == p:
420 newconf.set(s, p, oldconf.get(s, p))
422 newconf.set(s, "samba3:" + p, oldconf.get(s, p))
426 SAMBA3_PREDEF_NAMES = {
427 'HKLM': registry.HKEY_LOCAL_MACHINE,
431 def import_registry(samba4_registry, samba3_regdb):
432 """Import a Samba 3 registry database into the Samba 4 registry.
434 :param samba4_registry: Samba 4 registry handle.
435 :param samba3_regdb: Samba 3 registry database handle.
437 def ensure_key_exists(keypath):
438 (predef_name, keypath) = keypath.split("/", 1)
439 predef_id = SAMBA3_PREDEF_NAMES[predef_name]
440 keypath = keypath.replace("/", "\\")
441 return samba4_registry.create_key(predef_id, keypath)
443 for key in samba3_regdb.keys():
444 key_handle = ensure_key_exists(key)
445 for subkey in samba3_regdb.subkeys(key):
446 ensure_key_exists(subkey)
447 for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
448 key_handle.set_value(value_name, value_type, value_data)
451 def upgrade_from_samba3(samba3, logger, targetdir, session_info=None, useeadb=False):
452 """Upgrade from samba3 database to samba4 AD database
454 :param samba3: samba3 object
455 :param logger: Logger object
456 :param targetdir: samba4 database directory
457 :param session_info: Session information
460 if samba3.lp.get("domain logons"):
461 serverrole = "domain controller"
463 if samba3.lp.get("security") == "user":
464 serverrole = "standalone"
466 serverrole = "member server"
468 domainname = samba3.lp.get("workgroup")
469 realm = samba3.lp.get("realm")
470 netbiosname = samba3.lp.get("netbios name")
474 secrets_db = samba3.get_secrets_db()
476 raise ProvisioningError("Could not open '%s', the Samba3 secrets database: %s. Perhaps you specified the incorrect smb.conf, --testparm or --libdir option?" % samba3.privatedir_path("secrets.tdb"), str(e))
479 domainname = secrets_db.domains()[0]
480 logger.warning("No workgroup specified in smb.conf file, assuming '%s'",
484 if serverrole == "domain controller":
485 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.")
487 realm = domainname.upper()
488 logger.warning("No realm specified in smb.conf file, assuming '%s'",
491 # Find machine account and password
495 machinepass = secrets_db.get_machine_password(netbiosname)
499 # We must close the direct pytdb database before the C code loads it
502 # Connect to old password backend
503 passdb.set_secrets_dir(samba3.lp.get("private dir"))
504 s3db = samba3.get_sam_db()
508 domainsid = passdb.get_global_sam_sid()
510 raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
512 # Get machine account, sid, rid
514 machineacct = s3db.getsampwnam('%s$' % netbiosname)
519 machinesid, machinerid = machineacct.user_sid.split()
521 # Export account policy
522 logger.info("Exporting account policy")
523 policy = s3db.get_account_policy()
525 # Export groups from old passdb backend
526 logger.info("Exporting groups")
527 grouplist = s3db.enum_group_mapping()
529 for group in grouplist:
530 sid, rid = group.sid.split()
535 # Get members for each group/alias
536 if group.sid_name_use == lsa.SID_NAME_ALIAS:
537 members = s3db.enum_aliasmem(group.sid)
538 elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
540 members = s3db.enum_group_members(group.sid)
543 groupmembers[group.nt_name] = members
544 elif group.sid_name_use == lsa.SID_NAME_WKN_GRP:
545 (group_dom_sid, rid) = group.sid.split()
546 if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
547 logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)",
550 # A number of buggy databases mix up well known groups and aliases.
551 members = s3db.enum_aliasmem(group.sid)
553 logger.warn("Ignoring group '%s' with sid_name_use=%d",
554 group.nt_name, group.sid_name_use)
557 # Export users from old passdb backend
558 logger.info("Exporting users")
559 userlist = s3db.search_users(0)
563 for entry in userlist:
564 if machinerid and machinerid == entry['rid']:
566 username = entry['account_name']
567 if entry['rid'] < 1000:
568 logger.info(" Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
570 if entry['rid'] >= next_rid:
571 next_rid = entry['rid'] + 1
573 user = s3db.getsampwnam(username)
574 acct_type = (user.acct_ctrl & (samr.ACB_NORMAL|samr.ACB_WSTRUST|samr.ACB_SVRTRUST|samr.ACB_DOMTRUST))
575 if (acct_type == samr.ACB_NORMAL or acct_type == samr.ACB_WSTRUST or acct_type == samr.ACB_SVRTRUST):
577 elif acct_type == samr.ACB_DOMTRUST:
578 logger.warn(" Skipping inter-domain trust from domain %s, this trust must be re-created as an AD trust" % username[:-1])
580 elif acct_type == (samr.ACB_NORMAL|samr.ACB_WSTRUST) and username[-1] == '$':
581 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)
582 user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_NORMAL)
584 raise ProvisioningError("""Failed to upgrade due to invalid account %s, account control flags 0x%08X must have exactly one of
585 ACB_NORMAL (N, 0x%08X), ACB_WSTRUST (W 0x%08X), ACB_SVRTRUST (S 0x%08X) or ACB_DOMTRUST (D 0x%08X).
587 Please fix this account before attempting to upgrade again
589 % (user.acct_flags, username,
590 samr.ACB_NORMAL, samr.ACB_WSTRUST, samr.ACB_SVRTRUST, samr.ACB_DOMTRUST))
592 userdata[username] = user
594 uids[username] = s3db.sid_to_id(user.user_sid)[0]
597 uids[username] = pwd.getpwnam(username).pw_uid
601 if not admin_user and username.lower() == 'root':
602 admin_user = username
603 if username.lower() == 'administrator':
604 admin_user = username
606 logger.info("Next rid = %d", next_rid)
608 # Check for same username/groupname
609 group_names = set([g.nt_name for g in grouplist])
610 user_names = set([u['account_name'] for u in userlist])
611 common_names = group_names.intersection(user_names)
613 logger.error("Following names are both user names and group names:")
614 for name in common_names:
615 logger.error(" %s" % name)
616 raise ProvisioningError("Please remove common user/group names before upgrade.")
618 # Check for same user sid/group sid
619 group_sids = set([str(g.sid) for g in grouplist])
620 user_sids = set(["%s-%u" % (domainsid, u['rid']) for u in userlist])
621 common_sids = group_sids.intersection(user_sids)
623 logger.error("Following sids are both user and group sids:")
624 for sid in common_sids:
625 logger.error(" %s" % str(sid))
626 raise ProvisioningError("Please remove duplicate sid entries before upgrade.")
628 if serverrole == "domain controller":
629 dns_backend = "BIND9_FLATFILE"
634 result = provision(logger, session_info, None,
635 targetdir=targetdir, realm=realm, domain=domainname,
636 domainsid=str(domainsid), next_rid=next_rid,
638 dom_for_fun_level=dsdb.DS_DOMAIN_FUNCTION_2003,
639 hostname=netbiosname, machinepass=machinepass,
640 serverrole=serverrole, samdb_fill=FILL_FULL,
641 useeadb=useeadb, dns_backend=dns_backend)
643 # Import WINS database
644 logger.info("Importing WINS database")
648 samba3_winsdb = samba3.get_wins_db()
650 logger.warn('Cannot open wins database, Ignoring: %s', str(e))
653 import_wins(Ldb(result.paths.winsdb), samba3_winsdb)
656 logger.info("Importing Account policy")
657 import_sam_policy(result.samdb, policy, logger)
659 # Migrate IDMAP database
660 logger.info("Importing idmap database")
661 import_idmap(result.idmap, samba3, logger)
663 # Set the s3 context for samba4 configuration
664 new_lp_ctx = s3param.get_context()
665 new_lp_ctx.load(result.lp.configfile)
666 new_lp_ctx.set("private dir", result.lp.get("private dir"))
667 new_lp_ctx.set("state directory", result.lp.get("state directory"))
668 new_lp_ctx.set("lock directory", result.lp.get("lock directory"))
670 # Connect to samba4 backend
671 s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend"))
673 # Export groups to samba4 backend
674 logger.info("Importing groups")
676 # Ignore uninitialized groups (gid = -1)
677 if g.gid != 0xffffffff:
678 add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
679 add_group_from_mapping_entry(result.samdb, g, logger)
681 # Export users to samba4 backend
682 logger.info("Importing users")
683 for username in userdata:
684 if username.lower() == 'administrator' or username.lower() == 'root':
686 s4_passdb.add_sam_account(userdata[username])
688 add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
690 logger.info("Adding users to groups")
692 if g.nt_name in groupmembers:
693 add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
695 # Set password for administrator
697 logger.info("Setting password for administrator")
698 admin_userdata = s4_passdb.getsampwnam("administrator")
699 admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
700 if userdata[admin_user].lanman_passwd:
701 admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
702 admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
703 if userdata[admin_user].pw_history:
704 admin_userdata.pw_history = userdata[admin_user].pw_history
705 s4_passdb.update_sam_account(admin_userdata)
706 logger.info("Administrator password has been set to password of user '%s'", admin_user)
708 # FIXME: import_registry(registry.Registry(), samba3.get_registry())