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
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']), ldb.FLAG_MOD_REPLACE,
58 m['a02'] = ldb.MessageElement(str(policy['password history']), ldb.FLAG_MOD_REPLACE,
60 m['a03'] = ldb.MessageElement(str(policy['minimum password age']), ldb.FLAG_MOD_REPLACE,
62 m['a04'] = ldb.MessageElement(str(policy['maximum password age']), ldb.FLAG_MOD_REPLACE,
64 m['a05'] = ldb.MessageElement(str(policy['lockout duration']), ldb.FLAG_MOD_REPLACE,
69 except ldb.LdbError, e:
70 logger.warn("Could not set account policy, (%s)", str(e))
73 def add_idmap_entry(idmapdb, sid, xid, xid_type, logger):
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
83 # First try to see if we already have this entry
85 msg = idmapdb.search(expression='objectSid=%s' % str(sid))
93 m['xidNumber'] = ldb.MessageElement(str(xid), ldb.FLAG_MOD_REPLACE, 'xidNumber')
94 m['type'] = ldb.MessageElement(xid_type, ldb.FLAG_MOD_REPLACE, 'type')
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))
101 idmapdb.add({"dn": "CN=%s" % str(sid),
103 "objectClass": "sidMap",
104 "objectSid": ndr_pack(sid),
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))
112 def import_idmap(idmapdb, samba3, logger):
113 """Import idmap data.
115 :param idmapdb: Samba4 IDMAP database
116 :param samba3_idmap: Samba3 IDMAP database to import from
117 :param logger: Logger object
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))
126 currentxid = max(samba3_idmap.get_user_hwm(), samba3_idmap.get_group_hwm())
127 lowerbound = currentxid
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')
136 for id_type, xid in samba3_idmap.ids():
138 xid_type = 'ID_TYPE_UID'
139 elif id_type == 'GID':
140 xid_type = 'ID_TYPE_GID'
142 logger.warn('Wrong type of entry in idmap (%s), Ignoring', id_type)
145 sid = samba3_idmap.get_sid(xid, id_type)
146 add_idmap_entry(idmapdb, dom_sid(sid), xid, xid_type, logger)
149 def add_group_from_mapping_entry(samdb, groupmap, logger):
150 """Add or modify group from group mapping entry
152 param samdb: Samba4 SAM database
153 param groupmap: Groupmap entry
154 param logger: Logger object
157 # First try to see if we already have this entry
159 msg = samdb.search(base='<SID=%s>' % str(groupmap.sid), scope=ldb.SCOPE_BASE)
161 except ldb.LdbError, (ecode, emsg):
162 if ecode == ldb.ERR_NO_SUCH_OBJECT:
165 raise ldb.LdbError(ecode, emsg)
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])
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)):
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')
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')
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))
195 def add_users_to_group(samdb, group, members, logger):
196 """Add user/member to group/alias
198 param samdb: Samba4 SAM database
199 param group: Groupmap object
200 param members: List of member SIDs
201 param logger: Logger object
203 for member_sid in members:
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')
210 except ldb.LdbError, (ecode, emsg):
211 if ecode == ldb.ERR_ENTRY_ALREADY_EXISTS:
212 logger.info("skipped re-adding member '%s' to group '%s': %s", member_sid, group.sid, emsg)
213 elif ecode == ldb.ERR_NO_SUCH_OBJECT:
214 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))
216 raise ProvisioningError("Could not add member '%s' to group '%s': %s" % (member_sid, group.sid, emsg))
219 def import_wins(samba4_winsdb, samba3_winsdb):
220 """Import settings from a Samba3 WINS database.
222 :param samba4_winsdb: WINS database to import to
223 :param samba3_winsdb: WINS database to import from
227 for (name, (ttl, ips, nb_flags)) in samba3_winsdb.items():
230 type = int(name.split("#", 1)[1], 16)
245 if ttl > time.time():
246 rState = 0x0 # active
248 rState = 0x1 # released
250 nType = ((nb_flags & 0x60)>>5)
252 samba4_winsdb.add({"dn": "name=%s,type=0x%s" % tuple(name.split("#")),
253 "type": name.split("#")[1],
254 "name": name.split("#")[0],
255 "objectClass": "winsRecord",
256 "recordType": str(rType),
257 "recordState": str(rState),
258 "nodeType": str(nType),
259 "expireTime": ldb.timestring(ttl),
261 "versionID": str(version_id),
264 samba4_winsdb.add({"dn": "cn=VERSION",
266 "objectClass": "winsMaxVersion",
267 "maxVersion": str(version_id)})
269 def enable_samba3sam(samdb, ldapurl):
270 """Enable Samba 3 LDAP URL database.
272 :param samdb: SAM Database.
273 :param ldapurl: Samba 3 LDAP URL
275 samdb.modify_ldif("""
279 @LIST: samldb,operational,objectguid,rdn_name,samba3sam
282 samdb.add({"dn": "@MAP=samba3sam", "@MAP_URL": ldapurl})
299 "bind interfaces only",
304 "obey pam restrictions",
312 "client NTLMv2 auth",
313 "client lanman auth",
314 "client plaintext auth",
332 "name resolve order",
341 "paranoid server security",
377 def upgrade_smbconf(oldconf,mark):
378 """Remove configuration variables not present in Samba4
380 :param oldconf: Old configuration structure
381 :param mark: Whether removed configuration variables should be
382 kept in the new configuration as "samba3:<name>"
384 data = oldconf.data()
390 for k in smbconf_keep:
391 if smbconf_keep[k] == p:
396 newconf.set(s, p, oldconf.get(s, p))
398 newconf.set(s, "samba3:"+p, oldconf.get(s,p))
402 SAMBA3_PREDEF_NAMES = {
403 'HKLM': registry.HKEY_LOCAL_MACHINE,
406 def import_registry(samba4_registry, samba3_regdb):
407 """Import a Samba 3 registry database into the Samba 4 registry.
409 :param samba4_registry: Samba 4 registry handle.
410 :param samba3_regdb: Samba 3 registry database handle.
412 def ensure_key_exists(keypath):
413 (predef_name, keypath) = keypath.split("/", 1)
414 predef_id = SAMBA3_PREDEF_NAMES[predef_name]
415 keypath = keypath.replace("/", "\\")
416 return samba4_registry.create_key(predef_id, keypath)
418 for key in samba3_regdb.keys():
419 key_handle = ensure_key_exists(key)
420 for subkey in samba3_regdb.subkeys(key):
421 ensure_key_exists(subkey)
422 for (value_name, (value_type, value_data)) in samba3_regdb.values(key).items():
423 key_handle.set_value(value_name, value_type, value_data)
426 def upgrade_from_samba3(samba3, logger, targetdir, session_info=None, useeadb=False):
427 """Upgrade from samba3 database to samba4 AD database
429 :param samba3: samba3 object
430 :param logger: Logger object
431 :param targetdir: samba4 database directory
432 :param session_info: Session information
435 if samba3.lp.get("domain logons"):
436 serverrole = "domain controller"
438 if samba3.lp.get("security") == "user":
439 serverrole = "standalone"
441 serverrole = "member server"
443 domainname = samba3.lp.get("workgroup")
444 realm = samba3.lp.get("realm")
445 netbiosname = samba3.lp.get("netbios name")
448 secrets_db = samba3.get_secrets_db()
451 domainname = secrets_db.domains()[0]
452 logger.warning("No workgroup specified in smb.conf file, assuming '%s'",
456 if serverrole == "domain controller":
457 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.")
459 realm = domainname.upper()
460 logger.warning("No realm specified in smb.conf file, assuming '%s'",
463 # Find machine account and password
470 machinepass = secrets_db.get_machine_password(netbiosname)
474 # We must close the direct pytdb database before the C code loads it
477 # Connect to old password backend
478 passdb.set_secrets_dir(samba3.lp.get("private dir"))
479 s3db = samba3.get_sam_db()
483 domainsid = passdb.get_global_sam_sid()
485 raise Exception("Can't find domain sid for '%s', Exiting." % domainname)
487 # Get machine account, sid, rid
489 machineacct = s3db.getsampwnam('%s$' % netbiosname)
490 machinesid, machinerid = machineacct.user_sid.split()
494 # Export account policy
495 logger.info("Exporting account policy")
496 policy = s3db.get_account_policy()
498 # Export groups from old passdb backend
499 logger.info("Exporting groups")
500 grouplist = s3db.enum_group_mapping()
502 for group in grouplist:
503 sid, rid = group.sid.split()
508 # Get members for each group/alias
509 if group.sid_name_use == lsa.SID_NAME_ALIAS:
510 members = s3db.enum_aliasmem(group.sid)
511 elif group.sid_name_use == lsa.SID_NAME_DOM_GRP:
513 members = s3db.enum_group_members(group.sid)
516 groupmembers[group.nt_name] = members
517 elif group.sid_name_use == lsa.SID_NAME_WKN_GRP:
518 (group_dom_sid, rid) = group.sid.split()
519 if (group_dom_sid != security.dom_sid(security.SID_BUILTIN)):
520 logger.warn("Ignoring 'well known' group '%s' (should already be in AD, and have no members)",
523 # A number of buggy databases mix up well known groups and aliases.
524 members = s3db.enum_aliasmem(group.sid)
526 logger.warn("Ignoring group '%s' with sid_name_use=%d",
527 group.nt_name, group.sid_name_use)
531 # Export users from old passdb backend
532 logger.info("Exporting users")
533 userlist = s3db.search_users(0)
537 for entry in userlist:
538 if machinerid and machinerid == entry['rid']:
540 username = entry['account_name']
541 if entry['rid'] < 1000:
542 logger.info(" Skipping wellknown rid=%d (for username=%s)", entry['rid'], username)
544 if entry['rid'] >= next_rid:
545 next_rid = entry['rid'] + 1
547 user = s3db.getsampwnam(username)
548 acct_type = (user.acct_ctrl & (samr.ACB_NORMAL|samr.ACB_WSTRUST|samr.ACB_SVRTRUST|samr.ACB_DOMTRUST))
549 if (acct_type == samr.ACB_NORMAL or acct_type == samr.ACB_WSTRUST or acct_type == samr.ACB_SVRTRUST):
551 elif acct_type == samr.ACB_DOMTRUST:
552 logger.warn(" Skipping inter-domain trust from domain %s, this trust must be re-created as an AD trust" % username[:-1])
554 elif acct_type == (samr.ACB_NORMAL|samr.ACB_WSTRUST) and username[-1] == '$':
555 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)
556 user.acct_ctrl = (user.acct_ctrl & ~samr.ACB_NORMAL)
558 raise ProvisioningError("""Failed to upgrade due to invalid account %s, account control flags 0x%08X must have exactly one of
559 ACB_NORMAL (N, 0x%08X), ACB_WSTRUST (W 0x%08X), ACB_SVRTRUST (S 0x%08X) or ACB_DOMTRUST (D 0x%08X).
561 Please fix this account before attempting to upgrade again
563 % (user.acct_flags, username,
564 samr.ACB_NORMAL, samr.ACB_WSTRUST, samr.ACB_SVRTRUST, samr.ACB_DOMTRUST))
566 userdata[username] = user
568 uids[username] = s3db.sid_to_id(user.user_sid)[0]
571 uids[username] = pwd.getpwnam(username).pw_uid
575 if not admin_user and username.lower() == 'root':
576 admin_user = username
577 if username.lower() == 'administrator':
578 admin_user = username
580 logger.info("Next rid = %d", next_rid)
583 result = provision(logger, session_info, None,
584 targetdir=targetdir, realm=realm, domain=domainname,
585 domainsid=str(domainsid), next_rid=next_rid,
587 hostname=netbiosname, machinepass=machinepass,
588 serverrole=serverrole, samdb_fill=FILL_FULL,
591 # Import WINS database
592 logger.info("Importing WINS database")
593 import_wins(Ldb(result.paths.winsdb), samba3.get_wins_db())
596 logger.info("Importing Account policy")
597 import_sam_policy(result.samdb, policy, logger)
599 # Migrate IDMAP database
600 logger.info("Importing idmap database")
601 import_idmap(result.idmap, samba3, logger)
603 # Set the s3 context for samba4 configuration
604 new_lp_ctx = s3param.get_context()
605 new_lp_ctx.load(result.lp.configfile)
606 new_lp_ctx.set("private dir", result.lp.get("private dir"))
607 new_lp_ctx.set("state directory", result.lp.get("state directory"))
608 new_lp_ctx.set("lock directory", result.lp.get("lock directory"))
610 # Connect to samba4 backend
611 s4_passdb = passdb.PDB(new_lp_ctx.get("passdb backend"))
613 # Export groups to samba4 backend
614 logger.info("Importing groups")
616 # Ignore uninitialized groups (gid = -1)
617 if g.gid != 0xffffffff:
618 add_idmap_entry(result.idmap, g.sid, g.gid, "GID", logger)
619 add_group_from_mapping_entry(result.samdb, g, logger)
621 # Export users to samba4 backend
622 logger.info("Importing users")
623 for username in userdata:
624 if username.lower() == 'administrator' or username.lower() == 'root':
626 s4_passdb.add_sam_account(userdata[username])
628 add_idmap_entry(result.idmap, userdata[username].user_sid, uids[username], "UID", logger)
630 logger.info("Adding users to groups")
632 if g.nt_name in groupmembers:
633 add_users_to_group(result.samdb, g, groupmembers[g.nt_name], logger)
635 # Set password for administrator
637 logger.info("Setting password for administrator")
638 admin_userdata = s4_passdb.getsampwnam("administrator")
639 admin_userdata.nt_passwd = userdata[admin_user].nt_passwd
640 if userdata[admin_user].lanman_passwd:
641 admin_userdata.lanman_passwd = userdata[admin_user].lanman_passwd
642 admin_userdata.pass_last_set_time = userdata[admin_user].pass_last_set_time
643 if userdata[admin_user].pw_history:
644 admin_userdata.pw_history = userdata[admin_user].pw_history
645 s4_passdb.update_sam_account(admin_userdata)
646 logger.info("Administrator password has been set to password of user '%s'", admin_user)
648 # FIXME: import_registry(registry.Registry(), samba3.get_registry())