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"
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 = 0 - unix2nttime(min_pw_age_unix)
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 == 0xFFFFFFFF):
71 max_pw_age_nt = unix2nttime(max_pw_age_unix)
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 (UID/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['a01'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'cn')
202 m['a02'] = ldb.MessageElement('group', ldb.FLAG_MOD_ADD, 'objectClass')
203 m['a03'] = ldb.MessageElement(ndr_pack(groupmap.sid), ldb.FLAG_MOD_ADD, 'objectSid')
204 m['a04'] = ldb.MessageElement(groupmap.comment, ldb.FLAG_MOD_ADD, 'description')
205 m['a05'] = ldb.MessageElement(groupmap.nt_name, ldb.FLAG_MOD_ADD, 'sAMAccountName')
207 # Fix up incorrect 'well known' groups that are actually builtin (per test above) to be aliases
208 if groupmap.sid_name_use == lsa.SID_NAME_ALIAS or groupmap.sid_name_use == lsa.SID_NAME_WKN_GRP:
209 m['a06'] = ldb.MessageElement(str(dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP), ldb.FLAG_MOD_ADD, 'groupType')
212 samdb.add(m, controls=["relax:0"])
213 except ldb.LdbError, e:
214 logger.warn('Could not add group name=%s (%s)', groupmap.nt_name, str(e))
217 def add_users_to_group(samdb, group, members, logger):
218 """Add user/member to group/alias
220 param samdb: Samba4 SAM database
221 param group: Groupmap object
222 param members: List of member SIDs
223 param logger: Logger object
225 for member_sid in members:
227 m.dn = ldb.Dn(samdb, "<SID=%s>" % str(group.sid))
228 m['a01'] = ldb.MessageElement("<SID=%s>" % str(member_sid), ldb.FLAG_MOD_ADD, 'member')
232 except ldb.LdbError, (ecode, emsg):
233 if ecode == ldb.ERR_ENTRY_ALREADY_EXISTS:
234 logger.info("skipped re-adding member '%s' to group '%s': %s", member_sid, group.sid, emsg)
235 elif ecode == ldb.ERR_NO_SUCH_OBJECT:
236 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))
238 raise ProvisioningError("Could not add member '%s' to group '%s': %s" % (member_sid, group.sid, emsg))
241 def import_wins(samba4_winsdb, samba3_winsdb):
242 """Import settings from a Samba3 WINS database.
244 :param samba4_winsdb: WINS database to import to
245 :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")
473 secrets_db = samba3.get_secrets_db()
476 domainname = secrets_db.domains()[0]
477 logger.warning("No workgroup specified in smb.conf file, assuming '%s'",
481 if serverrole == "domain controller":
482 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.")
484 realm = domainname.upper()
485 logger.warning("No realm specified in smb.conf file, assuming '%s'",
488 # 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)
515 machinesid, machinerid = machineacct.user_sid.split()
519 # Export account policy
520 logger.info("Exporting account policy")
521 policy = s3db.get_account_policy()
523 # Export groups from old passdb backend
524 logger.info("Exporting groups")
525 grouplist = s3db.enum_group_mapping()
527 for group in grouplist:
528 sid, rid = group.sid.split()
533 # Get members for each group/alias
534 if group.sid_name_use == lsa.SID_NAME_ALIAS:
535 members = s3db.enum_aliasmem(group.sid)
536 elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
538 members = s3db.enum_group_members(group.sid)
541 groupmembers[group.nt_name] = members
542 elif group.sid_name_use == lsa.SID_NAME_WKN_GRP:
543 (group_dom_sid, rid) = group.sid.split()
544 if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
545 logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)",
548 # A number of buggy databases mix up well known groups and aliases.
549 members = s3db.enum_aliasmem(group.sid)
551 logger.warn("Ignoring group '%s' with sid_name_use=%d",
552 group.nt_name, group.sid_name_use)
555 # Export users from old passdb backend
556 logger.info("Exporting users")
557 userlist = s3db.search_users(0)
561 for entry in userlist:
562 if machinerid and machinerid == entry['rid']:
564 username = entry['account_name']
565 if entry['rid'] < 1000:
566 logger.info(" Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
568 if entry['rid'] >= next_rid:
569 next_rid = entry['rid'] + 1
571 user = s3db.getsampwnam(username)
572 acct_type = (user.acct_ctrl & (samr.ACB_NORMAL|samr.ACB_WSTRUST|samr.ACB_SVRTRUST|samr.ACB_DOMTRUST))
573 if (acct_type == samr.ACB_NORMAL or acct_type == samr.ACB_WSTRUST or acct_type == samr.ACB_SVRTRUST):
575 elif acct_type == samr.ACB_DOMTRUST:
576 logger.warn(" Skipping inter-domain trust from domain %s, this trust must be re-created as an AD trust" % username[:-1])
578 elif acct_type == (samr.ACB_NORMAL|samr.ACB_WSTRUST) and username[-1] == '$':
579 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)
580 user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_NORMAL)
582 raise ProvisioningError("""Failed to upgrade due to invalid account %s, account control flags 0x%08X must have exactly one of
583 ACB_NORMAL (N, 0x%08X), ACB_WSTRUST (W 0x%08X), ACB_SVRTRUST (S 0x%08X) or ACB_DOMTRUST (D 0x%08X).
585 Please fix this account before attempting to upgrade again
587 % (user.acct_flags, username,
588 samr.ACB_NORMAL, samr.ACB_WSTRUST, samr.ACB_SVRTRUST, samr.ACB_DOMTRUST))
590 userdata[username] = user
592 uids[username] = s3db.sid_to_id(user.user_sid)[0]
595 uids[username] = pwd.getpwnam(username).pw_uid
599 if not admin_user and username.lower() == 'root':
600 admin_user = username
601 if username.lower() == 'administrator':
602 admin_user = username
604 logger.info("Next rid = %d", next_rid)
606 # Check for same username/groupname
607 group_names = set(map(lambda g: g.nt_name, grouplist))
608 user_names = set(map(lambda u: u['account_name'], userlist))
609 common_names = group_names.intersection(user_names)
611 logger.error("Following names are both user names and group names:")
612 for name in common_names:
613 logger.error(" %s" % name)
614 raise ProvisioningError("Please remove common user/group names before upgrade.")
616 # Check for same user sid/group sid
617 group_sids = set(map(lambda g: str(g.sid), grouplist))
618 user_sids = set(map(lambda u: "%s-%u" % (domainsid, u['rid']), userlist))
619 common_sids = group_sids.intersection(user_sids)
621 logger.error("Following sids are both user and group sids:")
622 for sid in common_sids:
623 logger.error(" %s" % str(sid))
624 raise ProvisioningError("Please remove duplicate sid entries before upgrade.")
627 result = provision(logger, session_info, None,
628 targetdir=targetdir, realm=realm, domain=domainname,
629 domainsid=str(domainsid), next_rid=next_rid,
631 hostname=netbiosname, machinepass=machinepass,
632 serverrole=serverrole, samdb_fill=FILL_FULL,
635 # Import WINS database
636 logger.info("Importing WINS database")
637 import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
640 logger.info("Importing Account policy")
641 import_sam_policy(result.samdb, policy, logger)
643 # Migrate IDMAP database
644 logger.info("Importing idmap database")
645 import_idmap(result.idmap, samba3, logger)
647 # Set the s3 context for samba4 configuration
648 new_lp_ctx = s3param.get_context()
649 new_lp_ctx.load(result.lp.configfile)
650 new_lp_ctx.set("private dir", result.lp.get("private dir"))
651 new_lp_ctx.set("state directory", result.lp.get("state directory"))
652 new_lp_ctx.set("lock directory", result.lp.get("lock directory"))
654 # Connect to samba4 backend
655 s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend"))
657 # Export groups to samba4 backend
658 logger.info("Importing groups")
660 # Ignore uninitialized groups (gid = -1)
661 if g.gid != 0xffffffff:
662 add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
663 add_group_from_mapping_entry(result.samdb, g, logger)
665 # Export users to samba4 backend
666 logger.info("Importing users")
667 for username in userdata:
668 if username.lower() == 'administrator' or username.lower() == 'root':
670 s4_passdb.add_sam_account(userdata[username])
672 add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
674 logger.info("Adding users to groups")
676 if g.nt_name in groupmembers:
677 add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
679 # Set password for administrator
681 logger.info("Setting password for administrator")
682 admin_userdata = s4_passdb.getsampwnam("administrator")
683 admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
684 if userdata[admin_user].lanman_passwd:
685 admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
686 admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
687 if userdata[admin_user].pw_history:
688 admin_userdata.pw_history = userdata[admin_user].pw_history
689 s4_passdb.update_sam_account(admin_userdata)
690 logger.info("Administrator password has been set to password of user '%s'", admin_user)
692 # FIXME: import_registry(registry.Registry(), samba3.get_registry())