1 # backend code for upgrading from Samba3
2 # Copyright Jelmer Vernooij 2005-2007
3 # Copyright Andrew Bartlett 2011
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 """Support code for upgrading from Samba 3 to Samba 4."""
21 __docformat__ = "restructuredText"
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 from samba import unix2nttime
39 def import_sam_policy(samdb, policy, logger):
40 """Import a Samba 3 policy.
42 :param samdb: Samba4 SAM database
43 :param policy: Samba3 account policy
44 :param logger: Logger object
47 # Following entries are used -
48 # min password length, password history, minimum password age,
49 # maximum password age, lockout duration
51 # Following entries are not used -
52 # reset count minutes, user must logon to change password,
53 # bad lockout minutes, disconnect time
56 m.dn = samdb.get_default_basedn()
57 m['a01'] = ldb.MessageElement(str(policy['min password length']),
58 ldb.FLAG_MOD_REPLACE, 'minPwdLength')
59 m['a02'] = ldb.MessageElement(str(policy['password history']),
60 ldb.FLAG_MOD_REPLACE, 'pwdHistoryLength')
62 min_pw_age_unix = policy['minimum password age']
63 min_pw_age_nt = int(-min_pw_age_unix * (1e7 * 60 * 60 * 24))
64 m['a03'] = ldb.MessageElement(str(min_pw_age_nt), ldb.FLAG_MOD_REPLACE,
67 max_pw_age_unix = policy['maximum password age']
68 if max_pw_age_unix == -1 or max_pw_age_unix == 0:
69 max_pw_age_nt = -0x8000000000000000
71 max_pw_age_nt = int(-max_pw_age_unix * (1e7 * 60 * 60 * 24))
73 m['a04'] = ldb.MessageElement(str(max_pw_age_nt), ldb.FLAG_MOD_REPLACE,
76 lockout_duration_mins = policy['lockout duration']
77 lockout_duration_nt = unix2nttime(lockout_duration_mins * 60)
79 m['a05'] = ldb.MessageElement(str(lockout_duration_nt),
80 ldb.FLAG_MOD_REPLACE, 'lockoutDuration')
84 except ldb.LdbError, e:
85 logger.warn("Could not set account policy, (%s)", str(e))
88 def add_idmap_entry(idmapdb, sid, xid, xid_type, logger):
91 :param idmapdb: Samba4 IDMAP database
92 :param sid: user/group sid
93 :param xid: user/group id
94 :param xid_type: type of id (ID_TYPE_UID/ID_TYPE_GID)
95 :param logger: Logger object
98 # First try to see if we already have this entry
100 msg = idmapdb.search(expression='objectSid=%s' % str(sid))
108 m['xidNumber'] = ldb.MessageElement(
109 str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
110 m['type'] = ldb.MessageElement(
111 xid_type, ldb.FLAG_MOD_REPLACE, 'type')
113 except ldb.LdbError, e:
115 'Could not modify idmap entry for sid=%s, id=%s, type=%s (%s)',
116 str(sid), str(xid), xid_type, str(e))
119 idmapdb.add({"dn": "CN=%s" % str(sid),
121 "objectClass": "sidMap",
122 "objectSid": ndr_pack(sid),
124 "xidNumber": str(xid)})
125 except ldb.LdbError, e:
127 'Could not add idmap entry for sid=%s, id=%s, type=%s (%s)',
128 str(sid), str(xid), xid_type, str(e))
131 def import_idmap(idmapdb, samba3, logger):
132 """Import idmap data.
134 :param idmapdb: Samba4 IDMAP database
135 :param samba3_idmap: Samba3 IDMAP database to import from
136 :param logger: Logger object
140 samba3_idmap = samba3.get_idmap_db()
142 logger.warn('Cannot open idmap database, Ignoring: %s', str(e))
145 currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm())
146 lowerbound = currentxid
150 m.dn = ldb.Dn(idmapdb, 'CN=CONFIG')
151 m['lowerbound'] = ldb.MessageElement(
152 str(lowerbound), ldb.FLAG_MOD_REPLACE, 'lowerBound')
153 m['xidNumber'] = ldb.MessageElement(
154 str(currentxid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
157 for id_type, xid in samba3_idmap.ids():
159 xid_type = 'ID_TYPE_UID'
160 elif id_type == 'GID':
161 xid_type = 'ID_TYPE_GID'
163 logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type)
166 sid = samba3_idmap.get_sid(xid, id_type)
167 add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger)
170 def add_group_from_mapping_entry(samdb, groupmap, logger):
171 """Add or modify group from group mapping entry
173 param samdb: Samba4 SAM database
174 param groupmap: Groupmap entry
175 param logger: Logger object
178 # First try to see if we already have this entry
181 base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE)
183 except ldb.LdbError, (ecode, emsg):
184 if ecode == ldb.ERR_NO_SUCH_OBJECT:
187 raise ldb.LdbError(ecode, emsg)
190 logger.warn('Group already exists sid=%s, groupname=%s existing_groupname=%s, Ignoring.',
191 str(groupmap.sid), groupmap.nt_name, msg[0]['sAMAccountName'][0])
193 if groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
194 # In a lot of Samba3 databases, aliases are marked as well known groups
195 (group_dom_sid, rid) = groupmap.sid.split()
196 if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
200 m.dn = ldb.Dn(samdb, "CN=%s,CN=Users,%s" % (groupmap.nt_name, samdb.get_default_basedn()))
201 m['cn'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn')
202 m['objectClass'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass')
203 m['objectSid'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD,
205 m['sAMAccountName'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD,
209 m['description'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD,
212 # Fix up incorrect 'well known' groups that are actually builtin (per test above) to be aliases
213 if groupmap.sid_name_use == lsa.SID_NAME_ALIAS or groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
214 m['groupType'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP),
215 ldb.FLAG_MOD_ADD, 'groupType')
218 samdb.add(m, controls=["relax:0"])
219 except ldb.LdbError, e:
220 logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e))
223 def add_users_to_group(samdb, group, members, logger):
224 """Add user/member to group/alias
226 param samdb: Samba4 SAM database
227 param group: Groupmap object
228 param members: List of member SIDs
229 param logger: Logger object
231 for member_sid in members:
233 m.dn = ldb.Dn(samdb, "<SID=%s>" % str(group.sid))
234 m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_ADD, 'member')
238 except ldb.LdbError, (ecode, emsg):
239 if ecode == ldb.ERR_ENTRY_ALREADY_EXISTS:
240 logger.debug("skipped re-adding member '%s' to group '%s': %s", member_sid, group.sid, emsg)
241 elif ecode == ldb.ERR_NO_SUCH_OBJECT:
242 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))
244 raise ProvisioningError("Could not add member '%s' to group '%s': %s" % (member_sid, group.sid, emsg))
247 def import_wins(samba4_winsdb, samba3_winsdb):
248 """Import settings from a Samba3 WINS database.
250 :param samba4_winsdb: WINS database to import to
251 :param samba3_winsdb: WINS database to import from
256 for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
259 type = int(name.split("#", 1)[1], 16)
274 if ttl > time.time():
275 rState = 0x0 # active
277 rState = 0x1 # released
279 nType = ((nb_flags & 0x60) >> 5)
281 samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
282 "type": name.split("#")[1],
283 "name": name.split("#")[0],
284 "objectClass": "winsRecord",
285 "recordType": str(rType),
286 "recordState": str(rState),
287 "nodeType": str(nType),
288 "expireTime": ldb.timestring(ttl),
290 "versionID": str(version_id),
293 samba4_winsdb.add({"dn": "cn=VERSION",
295 "objectClass": "winsMaxVersion",
296 "maxVersion": str(version_id)})
299 def enable_samba3sam(samdb, ldapurl):
300 """Enable Samba 3 LDAP URL database.
302 :param samdb: SAM Database.
303 :param ldapurl: Samba 3 LDAP URL
305 samdb.modify_ldif("""
309 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
312 samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
329 "bind interfaces only",
334 "obey pam restrictions",
342 "client NTLMv2 auth",
343 "client lanman auth",
344 "client plaintext auth",
362 "name resolve order",
371 "paranoid server security",
408 def upgrade_smbconf(oldconf, mark):
409 """Remove configuration variables not present in Samba4
411 :param oldconf: Old configuration structure
412 :param mark: Whether removed configuration variables should be
413 kept in the new configuration as "samba3:<name>"
415 data = oldconf.data()
421 for k in smbconf_keep:
422 if smbconf_keep[k] == p:
427 newconf.set(s, p, oldconf.get(s, p))
429 newconf.set(s, "samba3:" + p, oldconf.get(s, p))
433 SAMBA3_PREDEF_NAMES = {
434 'HKLM': registry.HKEY_LOCAL_MACHINE,
438 def import_registry(samba4_registry, samba3_regdb):
439 """Import a Samba 3 registry database into the Samba 4 registry.
441 :param samba4_registry: Samba 4 registry handle.
442 :param samba3_regdb: Samba 3 registry database handle.
444 def ensure_key_exists(keypath):
445 (predef_name, keypath) = keypath.split("/", 1)
446 predef_id = SAMBA3_PREDEF_NAMES[predef_name]
447 keypath = keypath.replace("/", "\\")
448 return samba4_registry.create_key(predef_id, keypath)
450 for key in samba3_regdb.keys():
451 key_handle = ensure_key_exists(key)
452 for subkey in samba3_regdb.subkeys(key):
453 ensure_key_exists(subkey)
454 for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
455 key_handle.set_value(value_name, value_type, value_data)
458 def upgrade_from_samba3(samba3, logger, targetdir, session_info=None, useeadb=False):
459 """Upgrade from samba3 database to samba4 AD database
461 :param samba3: samba3 object
462 :param logger: Logger object
463 :param targetdir: samba4 database directory
464 :param session_info: Session information
466 serverrole = samba3.lp.server_role()
468 domainname = samba3.lp.get("workgroup")
469 realm = samba3.lp.get("realm")
470 netbiosname = samba3.lp.get("netbios name")
472 if samba3.lp.get("ldapsam:trusted") is None:
473 samba3.lp.set("ldapsam:trusted", "yes")
477 secrets_db = samba3.get_secrets_db()
479 raise ProvisioningError("Could not open '%s', the Samba3 secrets database: %s. Perhaps you specified the incorrect smb.conf, --testparm or --dbdir option?" % (samba3.privatedir_path("secrets.tdb"), str(e)))
482 domainname = secrets_db.domains()[0]
483 logger.warning("No workgroup specified in smb.conf file, assuming '%s'",
487 if serverrole == "ROLE_DOMAIN_BDC" or serverrole == "ROLE_DOMAIN_PDC":
488 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.")
490 realm = domainname.upper()
491 logger.warning("No realm specified in smb.conf file, assuming '%s'",
494 # Find machine account and password
498 machinepass = secrets_db.get_machine_password(netbiosname)
502 # We must close the direct pytdb database before the C code loads it
505 # Connect to old password backend
506 passdb.set_secrets_dir(samba3.lp.get("private dir"))
507 s3db = samba3.get_sam_db()
511 domainsid = passdb.get_global_sam_sid()
513 raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
515 # Get machine account, sid, rid
517 machineacct = s3db.getsampwnam('%s$' % netbiosname)
522 machinesid, machinerid = machineacct.user_sid.split()
524 # Export account policy
525 logger.info("Exporting account policy")
526 policy = s3db.get_account_policy()
528 # Export groups from old passdb backend
529 logger.info("Exporting groups")
530 grouplist = s3db.enum_group_mapping()
532 for group in grouplist:
533 sid, rid = group.sid.split()
538 # Get members for each group/alias
539 if group.sid_name_use == lsa.SID_NAME_ALIAS:
541 members = s3db.enum_aliasmem(group.sid)
542 groupmembers[str(group.sid)] = members
543 except passdb.error, e:
544 logger.warn("Ignoring group '%s' %s listed but then not found: %s",
545 group.nt_name, group.sid, e)
547 elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
549 members = s3db.enum_group_members(group.sid)
550 groupmembers[str(group.sid)] = members
551 except passdb.error, e:
552 logger.warn("Ignoring group '%s' %s listed but then not found: %s",
553 group.nt_name, group.sid, e)
555 elif group.sid_name_use == lsa.SID_NAME_WKN_GRP:
556 (group_dom_sid, rid) = group.sid.split()
557 if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
558 logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)",
561 # A number of buggy databases mix up well known groups and aliases.
563 members = s3db.enum_aliasmem(group.sid)
564 groupmembers[str(group.sid)] = members
565 except passdb.error, e:
566 logger.warn("Ignoring group '%s' %s listed but then not found: %s",
567 group.nt_name, group.sid, e)
570 logger.warn("Ignoring group '%s' %s with sid_name_use=%d",
571 group.nt_name, group.sid, group.sid_name_use)
574 # Export users from old passdb backend
575 logger.info("Exporting users")
576 userlist = s3db.search_users(0)
580 for entry in userlist:
581 if machinerid and machinerid == entry['rid']:
583 username = entry['account_name']
584 if entry['rid'] < 1000:
585 logger.info(" Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
587 if entry['rid'] >= next_rid:
588 next_rid = entry['rid'] + 1
590 user = s3db.getsampwnam(username)
591 acct_type = (user.acct_ctrl & (samr.ACB_NORMAL|samr.ACB_WSTRUST|samr.ACB_SVRTRUST|samr.ACB_DOMTRUST))
592 if (acct_type == samr.ACB_NORMAL or acct_type == samr.ACB_WSTRUST or acct_type == samr.ACB_SVRTRUST):
594 elif acct_type == samr.ACB_DOMTRUST:
595 logger.warn(" Skipping inter-domain trust from domain %s, this trust must be re-created as an AD trust" % username[:-1])
597 elif acct_type == (samr.ACB_NORMAL|samr.ACB_WSTRUST) and username[-1] == '$':
598 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)
599 user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_NORMAL)
601 raise ProvisioningError("""Failed to upgrade due to invalid account %s, account control flags 0x%08X must have exactly one of
602 ACB_NORMAL (N, 0x%08X), ACB_WSTRUST (W 0x%08X), ACB_SVRTRUST (S 0x%08X) or ACB_DOMTRUST (D 0x%08X).
604 Please fix this account before attempting to upgrade again
606 % (user.acct_flags, username,
607 samr.ACB_NORMAL, samr.ACB_WSTRUST, samr.ACB_SVRTRUST, samr.ACB_DOMTRUST))
609 userdata[username] = user
611 uids[username] = s3db.sid_to_id(user.user_sid)[0]
614 uids[username] = pwd.getpwnam(username).pw_uid
618 if not admin_user and username.lower() == 'root':
619 admin_user = username
620 if username.lower() == 'administrator':
621 admin_user = username
624 group_memberships = s3db.enum_group_memberships(user);
625 for group in group_memberships:
626 if str(group) in groupmembers:
627 if user.user_sid not in groupmembers[str(group)]:
628 groupmembers[str(group)].append(user.user_sid)
630 groupmembers[str(group)] = [user.user_sid];
631 except passdb.error, e:
632 logger.warn("Ignoring group memberships of '%s' %s: %s",
633 username, user.user_sid, e)
636 logger.info("Next rid = %d", next_rid)
638 # Check for same username/groupname
639 group_names = set([g.nt_name for g in grouplist])
640 user_names = set([u['account_name'] for u in userlist])
641 common_names = group_names.intersection(user_names)
643 logger.error("Following names are both user names and group names:")
644 for name in common_names:
645 logger.error(" %s" % name)
646 raise ProvisioningError("Please remove common user/group names before upgrade.")
648 # Check for same user sid/group sid
649 group_sids = set([str(g.sid) for g in grouplist])
650 if len(grouplist) != len(group_sids):
651 raise ProvisioningError("Please remove duplicate group sid entries before upgrade.")
652 user_sids = set(["%s-%u" % (domainsid, u['rid']) for u in userlist])
653 if len(userlist) != len(user_sids):
654 raise ProvisioningError("Please remove duplicate user sid entries before upgrade.")
655 common_sids = group_sids.intersection(user_sids)
657 logger.error("Following sids are both user and group sids:")
658 for sid in common_sids:
659 logger.error(" %s" % str(sid))
660 raise ProvisioningError("Please remove duplicate sid entries before upgrade.")
662 if serverrole == "ROLE_DOMAIN_BDC" or serverrole == "ROLE_DOMAIN_PDC":
663 dns_backend = "BIND9_DLZ"
668 result = provision(logger, session_info, None,
669 targetdir=targetdir, realm=realm, domain=domainname,
670 domainsid=str(domainsid), next_rid=next_rid,
672 dom_for_fun_level=dsdb.DS_DOMAIN_FUNCTION_2003,
673 hostname=netbiosname.lower(), machinepass=machinepass,
674 serverrole=serverrole, samdb_fill=FILL_FULL,
675 useeadb=useeadb, dns_backend=dns_backend)
676 result.report_logger(logger)
678 # Import WINS database
679 logger.info("Importing WINS database")
683 samba3_winsdb = samba3.get_wins_db()
685 logger.warn('Cannot open wins database, Ignoring: %s', str(e))
688 import_wins(Ldb(result.paths.winsdb), samba3_winsdb)
691 logger.info("Importing Account policy")
692 import_sam_policy(result.samdb, policy, logger)
694 # Migrate IDMAP database
695 logger.info("Importing idmap database")
696 import_idmap(result.idmap, samba3, logger)
698 # Set the s3 context for samba4 configuration
699 new_lp_ctx = s3param.get_context()
700 new_lp_ctx.load(result.lp.configfile)
701 new_lp_ctx.set("private dir", result.lp.get("private dir"))
702 new_lp_ctx.set("state directory", result.lp.get("state directory"))
703 new_lp_ctx.set("lock directory", result.lp.get("lock directory"))
705 # Connect to samba4 backend
706 s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend"))
708 # Export groups to samba4 backend
709 logger.info("Importing groups")
711 # Ignore uninitialized groups (gid = -1)
713 add_idmap_entry(result.idmap, g.sid, g.gid, "ID_TYPE_GID", logger)
714 add_group_from_mapping_entry(result.samdb, g, logger)
716 # Export users to samba4 backend
717 logger.info("Importing users")
718 for username in userdata:
719 if username.lower() == 'administrator' or username.lower() == 'root':
721 s4_passdb.add_sam_account(userdata[username])
723 add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "ID_TYPE_UID", logger)
725 logger.info("Adding users to groups")
727 if str(g.sid) in groupmembers:
728 add_users_to_group(result.samdb, g, groupmembers[str(g.sid)], logger)
730 # Set password for administrator
732 logger.info("Setting password for administrator")
733 admin_userdata = s4_passdb.getsampwnam("administrator")
734 admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
735 if userdata[admin_user].lanman_passwd:
736 admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
737 admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
738 if userdata[admin_user].pw_history:
739 admin_userdata.pw_history = userdata[admin_user].pw_history
740 s4_passdb.update_sam_account(admin_userdata)
741 logger.info("Administrator password has been set to password of user '%s'", admin_user)
743 # FIXME: import_registry(registry.Registry(), samba3.get_registry())