# Unix SMB/CIFS implementation.
-# backend code for provisioning a Samba4 server
+# backend code for provisioning a Samba AD server
# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2012
# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008-2009
__docformat__ = "restructuredText"
from base64 import b64encode
+import errno
import os
+import stat
import re
import pwd
import grp
substitute_var,
valid_netbios_name,
version,
+ is_heimdal_built,
)
from samba.dcerpc import security, misc
from samba.dcerpc.misc import (
get_dns_partition_descriptor,
get_dns_forest_microsoft_dns_descriptor,
get_dns_domain_microsoft_dns_descriptor,
+ get_managed_service_accounts_descriptor,
)
from samba.provision.common import (
setup_path,
from samba.schema import Schema
from samba.samdb import SamDB
from samba.dbchecker import dbcheck
-from samba.provision.kerberos import make_kdcconf
+from samba.provision.kerberos import create_kdc_conf
+from samba.samdb import get_default_backend_store
DEFAULT_POLICY_GUID = "31B2F340-016D-11D2-945F-00C04FB984F9"
DEFAULT_DC_POLICY_GUID = "6AC1786C-016F-11D2-945F-00C04FB984F9"
DEFAULTSITE = "Default-First-Site-Name"
LAST_PROVISION_USN_ATTRIBUTE = "lastProvisionUSN"
+DEFAULT_MIN_PWD_LENGTH = 7
+
class ProvisionPaths(object):
self.dns = None
self.winsdb = None
self.private_dir = None
+ self.binddns_dir = None
self.state_dir = None
entry = sam.search(expression="%s=*" % LAST_PROVISION_USN_ATTRIBUTE,
base="@PROVISION", scope=ldb.SCOPE_BASE,
attrs=[LAST_PROVISION_USN_ATTRIBUTE, "provisionnerID"])
- except ldb.LdbError, (ecode, emsg):
+ except ldb.LdbError as e1:
+ (ecode, emsg) = e1.args
if ecode == ldb.ERR_NO_SUCH_OBJECT:
return None
raise
def report_logger(self, logger):
"""Report this provision result to a logger."""
logger.info(
- "Once the above files are installed, your Samba4 server will "
+ "Once the above files are installed, your Samba AD server will "
"be ready to use")
if self.adminpass_generated:
logger.info("Admin password: %s", self.adminpass)
"""
paths = ProvisionPaths()
paths.private_dir = lp.get("private dir")
+ paths.binddns_dir = lp.get("binddns dir")
paths.state_dir = lp.get("state directory")
# This is stored without path prefix for the "privateKeytab" attribute in
paths.idmapdb = os.path.join(paths.private_dir, "idmap.ldb")
paths.secrets = os.path.join(paths.private_dir, "secrets.ldb")
paths.privilege = os.path.join(paths.private_dir, "privilege.ldb")
- paths.dns = os.path.join(paths.private_dir, "dns", dnsdomain + ".zone")
paths.dns_update_list = os.path.join(paths.private_dir, "dns_update_list")
paths.spn_update_list = os.path.join(paths.private_dir, "spn_update_list")
- paths.namedconf = os.path.join(paths.private_dir, "named.conf")
- paths.namedconf_update = os.path.join(paths.private_dir, "named.conf.update")
- paths.namedtxt = os.path.join(paths.private_dir, "named.txt")
paths.krb5conf = os.path.join(paths.private_dir, "krb5.conf")
+ paths.kdcconf = os.path.join(paths.private_dir, "kdc.conf")
paths.winsdb = os.path.join(paths.private_dir, "wins.ldb")
paths.s4_ldapi_path = os.path.join(paths.private_dir, "ldapi")
+ paths.encrypted_secrets_key_path = os.path.join(
+ paths.private_dir,
+ "encrypted_secrets.key")
+
+ paths.dns = os.path.join(paths.binddns_dir, "dns", dnsdomain + ".zone")
+ paths.namedconf = os.path.join(paths.binddns_dir, "named.conf")
+ paths.namedconf_update = os.path.join(paths.binddns_dir, "named.conf.update")
+ paths.namedtxt = os.path.join(paths.binddns_dir, "named.txt")
+
paths.hklm = "hklm.ldb"
paths.hkcr = "hkcr.ldb"
paths.hkcu = "hkcu.ldb"
if domain == realm and not domain_names_forced:
raise ProvisioningError("guess_names: Realm '%s' must not be equal to short domain name '%s'!" % (realm, domain))
+ if serverrole != "active directory domain controller":
+ #
+ # This is the code path for a domain member
+ # where we provision the database as if we where
+ # on a domain controller, so we should not use
+ # the same dnsdomain as the domain controllers
+ # of our primary domain.
+ #
+ # This will be important if we start doing
+ # SID/name filtering and reject the local
+ # sid and names if they come from a domain
+ # controller.
+ #
+ realm = netbiosname
+ dnsdomain = netbiosname.lower()
+
if rootdn is None:
rootdn = domaindn
def make_smbconf(smbconf, hostname, domain, realm, targetdir,
serverrole=None, eadb=False, use_ntvfs=False, lp=None,
- global_param=None, kdcconfdir=None):
+ global_param=None):
"""Create a new smb.conf file based on a couple of basic settings.
"""
assert smbconf is not None
statedir = lp.get("state directory")
lp.set("xattr_tdb:file", os.path.abspath(os.path.join(statedir, "xattr.tdb")))
- make_kdcconf(realm, domain, kdcconfdir, os.path.dirname(lp.get("log file")))
- if kdcconfdir is not None:
- kdcconf = "%s/kdc.conf" % kdcconfdir
- lp.set("mit kdc config", kdcconf)
-
shares = {}
if serverrole == "active directory domain controller":
shares["sysvol"] = os.path.join(lp.get("state directory"), "sysvol")
f = open(smbconf, 'w')
try:
f.write("[globals]\n")
- for key, val in global_settings.iteritems():
+ for key, val in global_settings.items():
f.write("\t%s = %s\n" % (key, val))
f.write("\n")
- for name, path in shares.iteritems():
+ for name, path in shares.items():
f.write("[%s]\n" % name)
f.write("\tpath = %s\n" % path)
f.write("\tread only = no\n")
def setup_samdb_partitions(samdb_path, logger, lp, session_info,
provision_backend, names, serverrole,
- erase=False):
+ erase=False, plaintext_secrets=False,
+ backend_store=None):
"""Setup the partitions for the SAM database.
Alternatively, provision() may call this, and then populate the database.
if provision_backend.type != "ldb":
ldap_backend_line = "ldapBackend: %s" % provision_backend.ldap_uri
+ required_features = None
+ if not plaintext_secrets:
+ required_features = "requiredFeatures: encryptedSecrets"
+
+ if backend_store is None:
+ backend_store = get_default_backend_store()
+ backend_store_line = "backendStore: %s" % backend_store
+
+ if backend_store == "mdb":
+ if required_features is not None:
+ required_features += "\n"
+ else:
+ required_features = ""
+ required_features += "requiredFeatures: lmdbLevelOne"
+
+ if required_features is None:
+ required_features = "# No required features"
+
samdb.transaction_start()
try:
logger.info("Setting up sam.ldb partitions and settings")
setup_add_ldif(samdb, setup_path("provision_partitions.ldif"), {
- "LDAP_BACKEND_LINE": ldap_backend_line
+ "LDAP_BACKEND_LINE": ldap_backend_line,
+ "BACKEND_STORE": backend_store_line
})
setup_add_ldif(samdb, setup_path("provision_init.ldif"), {
"BACKEND_TYPE": provision_backend.type,
- "SERVER_ROLE": serverrole
+ "SERVER_ROLE": serverrole,
+ "REQUIRED_FEATURES": required_features
})
logger.info("Setting up sam.ldb rootDSE")
if len(res) == 1:
msg["priorSecret"] = [res[0]["secret"][0]]
- msg["priorWhenChanged"] = [res[0]["whenChanged"][0]]
+ try:
+ msg["priorWhenChanged"] = [res[0]["whenChanged"][0]]
+ except KeyError:
+ pass
try:
msg["privateKeytab"] = [res[0]["privateKeytab"][0]]
if os.path.exists(keytab_path):
os.unlink(keytab_path)
+ bind_dns_keytab_path = os.path.join(paths.binddns_dir, paths.dns_keytab)
+ if os.path.exists(bind_dns_keytab_path):
+ os.unlink(bind_dns_keytab_path)
+
dns_keytab_path = os.path.join(paths.private_dir, paths.dns_keytab)
if os.path.exists(dns_keytab_path):
os.unlink(dns_keytab_path)
privilege_ldb.erase()
privilege_ldb.load_ldif_file_add(setup_path("provision_privilege.ldif"))
+def setup_encrypted_secrets_key(path):
+ """Setup the encrypted secrets key file.
+
+ Any existing key file will be deleted and a new random key generated.
+
+ :param path: Path to the secrets key file.
+
+ """
+ if os.path.exists(path):
+ os.unlink(path)
+
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
+ mode = stat.S_IRUSR | stat.S_IWUSR
+
+ umask_original = os.umask(0)
+ try:
+ fd = os.open(path, flags, mode)
+ finally:
+ os.umask(umask_original)
+
+ with os.fdopen(fd, 'w') as f:
+ key = samba.generate_random_bytes(16)
+ f.write(key)
+
def setup_registry(path, session_info, lp):
"""Setup the registry.
def create_gpo_struct(policy_path):
if not os.path.exists(policy_path):
- os.makedirs(policy_path, 0775)
+ os.makedirs(policy_path, 0o775)
f = open(os.path.join(policy_path, "GPT.INI"), 'w')
try:
f.write("[General]\r\nVersion=0")
f.close()
p = os.path.join(policy_path, "MACHINE")
if not os.path.exists(p):
- os.makedirs(p, 0775)
+ os.makedirs(p, 0o775)
p = os.path.join(policy_path, "USER")
if not os.path.exists(p):
- os.makedirs(p, 0775)
+ os.makedirs(p, 0o775)
def create_default_gpo(sysvolpath, dnsdomain, policyguid, policyguid_dc):
def setup_samdb(path, session_info, provision_backend, lp, names,
- logger, fill, serverrole, schema, am_rodc=False):
+ logger, fill, serverrole, schema, am_rodc=False,
+ plaintext_secrets=False, backend_store=None):
"""Setup a complete SAM Database.
:note: This will wipe the main SAM database file!
# Also wipes the database
setup_samdb_partitions(path, logger=logger, lp=lp,
provision_backend=provision_backend, session_info=session_info,
- names=names, serverrole=serverrole)
+ names=names, serverrole=serverrole, plaintext_secrets=plaintext_secrets,
+ backend_store=backend_store)
# Load the database, but don's load the global schema and don't connect
# quite yet
# DB
try:
samdb.connect(path)
- except ldb.LdbError, (num, string_error):
+ except ldb.LdbError as e2:
+ (num, string_error) = e2.args
if (num == ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS):
raise ProvisioningError("Permission denied connecting to %s, are you running as root?" % path)
else:
def fill_samdb(samdb, lp, names, logger, policyguid,
policyguid_dc, fill, adminpass, krbtgtpass, machinepass, dns_backend,
dnspass, invocationid, ntdsguid, serverrole, am_rodc=False,
- dom_for_fun_level=None, schema=None, next_rid=None, dc_rid=None):
+ dom_for_fun_level=None, schema=None, next_rid=None, dc_rid=None,
+ backend_store=None):
if next_rid is None:
next_rid = 1000
"CONFIGDN": names.configdn,
"POLICYGUID": policyguid,
"DOMAIN_FUNCTIONALITY": str(domainFunctionality),
- "SAMBA_VERSION_STRING": version
+ "SAMBA_VERSION_STRING": version,
+ "MIN_PWD_LENGTH": str(DEFAULT_MIN_PWD_LENGTH)
})
# If we are setting up a subdomain, then this has been replicated in, so we don't need to add it
protected1wd_descr = b64encode(get_config_delete_protected1wd_descriptor(names.domainsid))
protected2_descr = b64encode(get_config_delete_protected2_descriptor(names.domainsid))
+ if "2008" in schema.base_schema:
+ # exclude 2012-specific changes if we're using a 2008 schema
+ incl_2012 = "#"
+ else:
+ incl_2012 = ""
+
setup_add_ldif(samdb, setup_path("provision_configuration.ldif"), {
"CONFIGDN": names.configdn,
"NETBIOSNAME": names.netbiosname,
"SITES_DESCRIPTOR": sites_descr,
})
+ setup_add_ldif(samdb, setup_path("extended-rights.ldif"), {
+ "CONFIGDN": names.configdn,
+ "INC2012" : incl_2012,
+ })
+
logger.info("Setting up display specifiers")
display_specifiers_ldif = read_ms_ldif(
setup_path('display-specifiers/DisplaySpecifiers-Win2k8R2.txt'))
check_all_substituted(display_specifiers_ldif)
samdb.add_ldif(display_specifiers_ldif)
- logger.info("Modifying display specifiers")
+ logger.info("Modifying display specifiers and extended rights")
setup_modify_ldif(samdb,
setup_path("provision_configuration_modify.ldif"), {
"CONFIGDN": names.configdn,
# If we are setting up a subdomain, then this has been replicated in, so we don't need to add it
if fill == FILL_FULL:
+ managedservice_descr = b64encode(get_managed_service_accounts_descriptor(names.domainsid))
setup_modify_ldif(samdb,
setup_path("provision_configuration_references.ldif"), {
"CONFIGDN": names.configdn,
setup_add_ldif(samdb, setup_path("provision_well_known_sec_princ.ldif"), {
"CONFIGDN": names.configdn,
"WELLKNOWNPRINCIPALS_DESCRIPTOR": protected1wd_descr,
- })
+ }, controls=["relax:0", "provision:0"])
if fill == FILL_FULL or fill == FILL_SUBDOMAIN:
setup_modify_ldif(samdb,
- setup_path("provision_basedn_references.ldif"),
- {"DOMAINDN": names.domaindn})
+ setup_path("provision_basedn_references.ldif"), {
+ "DOMAINDN": names.domaindn,
+ "MANAGEDSERVICE_DESCRIPTOR": managedservice_descr
+ })
logger.info("Setting up sam.ldb users and groups")
setup_add_ldif(samdb, setup_path("provision_users.ldif"), {
"DOMAINSID": str(names.domainsid),
"ADMINPASS_B64": b64encode(adminpass.encode('utf-16-le')),
"KRBTGTPASS_B64": b64encode(krbtgtpass.encode('utf-16-le'))
- })
+ }, controls=["relax:0", "provision:0"])
logger.info("Setting up self join")
setup_self_join(samdb, admin_session_info, names=names, fill=fill,
file = tempfile.NamedTemporaryFile(dir=os.path.abspath(sysvol))
try:
try:
- smbd.set_simple_acl(file.name, 0755, gid)
+ smbd.set_simple_acl(file.name, 0o755, gid)
except OSError:
if not smbd.have_posix_acls():
# This clue is only strictly correct for RPM and
invocationid=None, machinepass=None, ntdsguid=None,
dns_backend=None, dnspass=None,
serverrole=None, dom_for_fun_level=None,
- am_rodc=False, lp=None, use_ntvfs=False, skip_sysvolacl=False):
+ am_rodc=False, lp=None, use_ntvfs=False,
+ skip_sysvolacl=False, backend_store=None):
# create/adapt the group policy GUIDs
# Default GUID for default policy are described at
# "How Core Group Policy Works"
dns_backend=dns_backend, dnspass=dnspass,
ntdsguid=ntdsguid, serverrole=serverrole,
dom_for_fun_level=dom_for_fun_level, am_rodc=am_rodc,
- next_rid=next_rid, dc_rid=dc_rid)
+ next_rid=next_rid, dc_rid=dc_rid,
+ backend_store=backend_store)
# Set up group policies (domain policy and domain controller
# policy)
elements=kerberos_enctypes, flags=ldb.FLAG_MOD_REPLACE,
name="msDS-SupportedEncryptionTypes")
samdb.modify(msg)
- except ldb.LdbError, (enum, estr):
+ except ldb.LdbError as e:
+ (enum, estr) = e.args
if enum != ldb.ERR_NO_SUCH_ATTRIBUTE:
# It might be that this attribute does not exist in this schema
raise
setup_ad_dns(samdb, secrets_ldb, names, paths, lp, logger,
hostip=hostip, hostip6=hostip6, dns_backend=dns_backend,
dnspass=dnspass, os_level=dom_for_fun_level,
- targetdir=targetdir, fill_level=samdb_fill)
+ targetdir=targetdir, fill_level=samdb_fill,
+ backend_store=backend_store)
domainguid = samdb.searchone(basedn=samdb.get_default_basedn(),
attribute="objectGUID")
else:
samdb.transaction_commit()
+def directory_create_or_exists(path, mode=0o755):
+ if not os.path.exists(path):
+ try:
+ os.mkdir(path, mode)
+ except OSError as e:
+ if e.errno in [errno.EEXIST]:
+ pass
+ else:
+ raise ProvisioningError("Failed to create directory %s: %s" % (path, e.strerror))
-def provision(logger, session_info, smbconf=None, kdcconfdir=None,
+def provision(logger, session_info, smbconf=None,
targetdir=None, samdb_fill=FILL_FULL, realm=None, rootdn=None,
domaindn=None, schemadn=None, configdn=None, serverdn=None,
domain=None, hostname=None, hostip=None, hostip6=None, domainsid=None,
sitename=None, ol_mmr_urls=None, ol_olc=None, slapd_path=None,
useeadb=False, am_rodc=False, lp=None, use_ntvfs=False,
use_rfc2307=False, maxuid=None, maxgid=None, skip_sysvolacl=True,
- ldap_backend_forced_uri=None, nosync=False, ldap_dryrun_mode=False, ldap_backend_extra_port=None):
+ ldap_backend_forced_uri=None, nosync=False, ldap_dryrun_mode=False,
+ ldap_backend_extra_port=None, base_schema=None,
+ plaintext_secrets=False, backend_store=None):
"""Provision samba4
:note: caution, this wipes all existing data!
if backend_type is None:
backend_type = "ldb"
+ if backend_store is None:
+ backend_store = get_default_backend_store()
if domainsid is None:
domainsid = security.random_sid()
make_smbconf(smbconf, hostname, domain, realm,
targetdir, serverrole=serverrole,
eadb=useeadb, use_ntvfs=use_ntvfs,
- lp=lp, global_param=global_param,
- kdcconfdir=kdcconfdir)
+ lp=lp, global_param=global_param)
else:
make_smbconf(smbconf, hostname, domain, realm, targetdir,
serverrole=serverrole,
- eadb=useeadb, use_ntvfs=use_ntvfs, lp=lp, global_param=global_param,
- kdcconfdir=kdcconfdir)
+ eadb=useeadb, use_ntvfs=use_ntvfs, lp=lp, global_param=global_param)
if lp is None:
lp = samba.param.LoadParm()
if serverrole is None:
serverrole = lp.get("server role")
- if not os.path.exists(paths.private_dir):
- os.mkdir(paths.private_dir)
- if not os.path.exists(os.path.join(paths.private_dir, "tls")):
- os.makedirs(os.path.join(paths.private_dir, "tls"), 0700)
- if not os.path.exists(paths.state_dir):
- os.mkdir(paths.state_dir)
+ directory_create_or_exists(paths.private_dir, 0o700)
+ directory_create_or_exists(paths.binddns_dir, 0o770)
+ directory_create_or_exists(os.path.join(paths.private_dir, "tls"))
+ directory_create_or_exists(paths.state_dir)
+ if not plaintext_secrets:
+ setup_encrypted_secrets_key(paths.encrypted_secrets_key_path)
if paths.sysvol and not os.path.exists(paths.sysvol):
- os.makedirs(paths.sysvol, 0775)
+ os.makedirs(paths.sysvol, 0o775)
ldapi_url = "ldapi://%s" % urllib.quote(paths.s4_ldapi_path, safe="")
schema = Schema(domainsid, invocationid=invocationid,
- schemadn=names.schemadn)
+ schemadn=names.schemadn, base_schema=base_schema)
if backend_type == "ldb":
provision_backend = LDBBackend(backend_type, paths=paths,
samdb = setup_samdb(paths.samdb, session_info,
provision_backend, lp, names, logger=logger,
serverrole=serverrole,
- schema=schema, fill=samdb_fill, am_rodc=am_rodc)
+ schema=schema, fill=samdb_fill, am_rodc=am_rodc,
+ plaintext_secrets=plaintext_secrets,
+ backend_store=backend_store)
if serverrole == "active directory domain controller":
if paths.netlogon is None:
raise MissingShareError("sysvol", paths.smbconf)
if not os.path.isdir(paths.netlogon):
- os.makedirs(paths.netlogon, 0755)
+ os.makedirs(paths.netlogon, 0o755)
if adminpass is None:
adminpass = samba.generate_random_password(12, 32)
dnspass=dnspass, serverrole=serverrole,
dom_for_fun_level=dom_for_fun_level, am_rodc=am_rodc,
lp=lp, use_ntvfs=use_ntvfs,
- skip_sysvolacl=skip_sysvolacl)
+ skip_sysvolacl=skip_sysvolacl,
+ backend_store=backend_store)
+
+ if not is_heimdal_built():
+ create_kdc_conf(paths.kdcconf, realm, domain, os.path.dirname(lp.get("log file")))
+ logger.info("The Kerberos KDC configuration for Samba AD is "
+ "located at %s", paths.kdcconf)
create_krb5_conf(paths.krb5conf,
dnsdomain=names.dnsdomain, hostname=names.hostname,
realm=names.realm)
logger.info("A Kerberos configuration suitable for Samba AD has been "
"generated at %s", paths.krb5conf)
+ logger.info("Merge the contents of this file with your system "
+ "krb5.conf or replace it with this one. Do not create a "
+ "symlink!")
if serverrole == "active directory domain controller":
create_dns_update_list(lp, logger, paths)
# Now commit the secrets.ldb to disk
secrets_ldb.transaction_commit()
- # the commit creates the dns.keytab, now chown it
- dns_keytab_path = os.path.join(paths.private_dir, paths.dns_keytab)
- if os.path.isfile(dns_keytab_path) and paths.bind_gid is not None:
+ # the commit creates the dns.keytab in the private directory
+ private_dns_keytab_path = os.path.join(paths.private_dir, paths.dns_keytab)
+ bind_dns_keytab_path = os.path.join(paths.binddns_dir, paths.dns_keytab)
+
+ if os.path.isfile(private_dns_keytab_path):
+ if os.path.isfile(bind_dns_keytab_path):
+ try:
+ os.unlink(bind_dns_keytab_path)
+ except OSError as e:
+ logger.error("Failed to remove %s: %s" %
+ (bind_dns_keytab_path, e.strerror))
+
+ # link the dns.keytab to the bind-dns directory
try:
- os.chmod(dns_keytab_path, 0640)
- os.chown(dns_keytab_path, -1, paths.bind_gid)
- except OSError:
- if not os.environ.has_key('SAMBA_SELFTEST'):
- logger.info("Failed to chown %s to bind gid %u",
- dns_keytab_path, paths.bind_gid)
+ os.link(private_dns_keytab_path, bind_dns_keytab_path)
+ except OSError as e:
+ logger.error("Failed to create link %s -> %s: %s" %
+ (private_dns_keytab_path, bind_dns_keytab_path, e.strerror))
+
+ # chown the dns.keytab in the bind-dns directory
+ if paths.bind_gid is not None:
+ try:
+ os.chmod(paths.binddns_dir, 0o770)
+ os.chown(paths.binddns_dir, -1, paths.bind_gid)
+ except OSError:
+ if not os.environ.has_key('SAMBA_SELFTEST'):
+ logger.info("Failed to chown %s to bind gid %u",
+ paths.binddns_dir, paths.bind_gid)
+
+ try:
+ os.chmod(bind_dns_keytab_path, 0o640)
+ os.chown(bind_dns_keytab_path, -1, paths.bind_gid)
+ except OSError:
+ if not os.environ.has_key('SAMBA_SELFTEST'):
+ logger.info("Failed to chown %s to bind gid %u",
+ bind_dns_keytab_path, paths.bind_gid)
result = ProvisionResult()
result.server_role = serverrole