mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-25 08:21:05 -06:00
6c9fcccfbc
Since we are authenticating against AD DC before talking to it (by using trusted domain object's credentials), we need to override krb5.conf configuration in case --server option is specified. The context is a helper which is launched out of process with the help of oddjobd. The helper takes existing trusted domain object, uses its credentials to authenticate and then runs LSA RPC calls against that trusted domain's domain controller. Previous code directed Samba bindings to use the correct domain controller. However, if a DC visible to MIT Kerberos is not reachable, we would not be able to obtain TGT and the whole process will fail. trust_add.execute() was calling out to the D-Bus helper without passing the options (e.g. --server) so there was no chance to get that option visible by the oddjob helper. Also we need to make errors in the oddjob helper more visible to error_log. Thus, move error reporting for a normal communication up from the exception catching. Resolves: https://pagure.io/freeipa/issue/7895 Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Florence Blanc-Renaud <flo@redhat.com> Reviewed-By: Sergey Orlov <sorlov@redhat.com>
332 lines
10 KiB
Python
332 lines
10 KiB
Python
#!/usr/bin/python3
|
|
|
|
from ipaserver import dcerpc
|
|
from ipaserver.install.installutils import is_ipa_configured, ScriptError
|
|
from ipapython import config, ipautil
|
|
from ipalib import api
|
|
from ipapython.dn import DN
|
|
from ipapython.dnsutil import DNSName
|
|
from ipaplatform.constants import constants
|
|
from ipaplatform.paths import paths
|
|
import io
|
|
import sys
|
|
import os
|
|
import pwd
|
|
import tempfile
|
|
import textwrap
|
|
|
|
import six
|
|
import gssapi
|
|
|
|
from ipalib.install.kinit import kinit_keytab, kinit_password
|
|
|
|
if six.PY3:
|
|
unicode = str
|
|
|
|
|
|
def parse_options():
|
|
usage = "%prog <trusted domain name>\n"
|
|
parser = config.IPAOptionParser(
|
|
usage=usage, formatter=config.IPAFormatter()
|
|
)
|
|
|
|
parser.add_option(
|
|
"-d",
|
|
"--debug",
|
|
action="store_true",
|
|
dest="debug",
|
|
help="Display debugging information",
|
|
)
|
|
parser.add_option(
|
|
"-s",
|
|
"--server",
|
|
action="store",
|
|
dest="server",
|
|
help="Domain controller for the Active Directory domain (optional)",
|
|
)
|
|
parser.add_option(
|
|
"-a",
|
|
"--admin",
|
|
action="store",
|
|
dest="admin",
|
|
help="Active Directory administrator (optional)",
|
|
)
|
|
parser.add_option(
|
|
"-p",
|
|
"--password",
|
|
action="store",
|
|
dest="password",
|
|
help="Display debugging information",
|
|
)
|
|
|
|
options, args = parser.parse_args()
|
|
safe_options = parser.get_safe_opts(options)
|
|
|
|
# We only use first argument of the passed args but as D-BUS interface
|
|
# in oddjobd cannot expose optional, we fill in empty slots from IPA side
|
|
# and filter them here.
|
|
trusted_domain = ipautil.fsdecode(args[0]).lower()
|
|
|
|
# Accept domain names that at least have two labels. We do not support
|
|
# single label Active Directory domains. This also catches empty args.
|
|
if len(DNSName(trusted_domain).labels) < 2:
|
|
# LSB status code 2: invalid or excess argument(s)
|
|
raise ScriptError("You must specify a valid trusted domain name", 2)
|
|
return safe_options, options, trusted_domain
|
|
|
|
|
|
def retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal):
|
|
getkeytab_args = [
|
|
"/usr/sbin/ipa-getkeytab",
|
|
"-s",
|
|
api.env.host,
|
|
"-p",
|
|
oneway_principal,
|
|
"-k",
|
|
oneway_keytab_name,
|
|
"-r",
|
|
]
|
|
if os.path.isfile(oneway_keytab_name):
|
|
os.unlink(oneway_keytab_name)
|
|
|
|
ipautil.run(
|
|
getkeytab_args,
|
|
env={"KRB5CCNAME": ccache_name, "LANG": "C"},
|
|
raiseonerr=False,
|
|
)
|
|
# Make sure SSSD is able to read the keytab
|
|
try:
|
|
sssd = pwd.getpwnam(constants.SSSD_USER)
|
|
os.chown(oneway_keytab_name, sssd[2], sssd[3])
|
|
except KeyError:
|
|
# If user 'sssd' does not exist, we don't need to chown from root to sssd
|
|
# because it means SSSD does not run as sssd user
|
|
pass
|
|
|
|
|
|
def get_forest_root_domain(api_instance, trusted_domain, server=None):
|
|
"""Retrieve trusted forest root domain for given domain name
|
|
|
|
:param api_instance: IPA API instance
|
|
:param trusted_domain: trusted domain name
|
|
|
|
:returns: forest root domain DNS name
|
|
"""
|
|
trustconfig_show = api_instance.Command.trustconfig_show
|
|
flatname = trustconfig_show()["result"]["ipantflatname"][0]
|
|
|
|
remote_domain = dcerpc.retrieve_remote_domain(
|
|
api_instance.env.host, flatname, trusted_domain, realm_server=server
|
|
)
|
|
|
|
return remote_domain.info["dns_forest"]
|
|
|
|
|
|
def generate_krb5_config(realm, server):
|
|
"""Generate override krb5 config file for trusted domain DC access
|
|
|
|
:param realm: realm of the trusted AD domain
|
|
:param server: server to override KDC to
|
|
|
|
:returns: tuple (temporary config file name, KRB5_CONFIG string)
|
|
"""
|
|
cfg = paths.KRB5_CONF
|
|
tcfg = None
|
|
if server:
|
|
content = textwrap.dedent(u"""
|
|
[realms]
|
|
%s = {
|
|
kdc = %s
|
|
}
|
|
""") % (
|
|
realm.upper(),
|
|
server,
|
|
)
|
|
|
|
(fd, tcfg) = tempfile.mkstemp(dir="/var/run/ipa",
|
|
prefix="krb5conf", text=True)
|
|
with io.open(fd, mode='w', encoding='utf-8') as o:
|
|
o.write(content)
|
|
cfg = ":".join([tcfg, cfg])
|
|
return (tcfg, cfg)
|
|
|
|
|
|
if not is_ipa_configured():
|
|
# LSB status code 6: program is not configured
|
|
raise ScriptError(
|
|
"IPA is not configured "
|
|
+ "(see man pages of ipa-server-install for help)",
|
|
6,
|
|
)
|
|
|
|
if not os.getegid() == 0:
|
|
# LSB status code 4: user had insufficient privilege
|
|
raise ScriptError("You must be root to run ipactl.", 4)
|
|
|
|
safe_options, options, trusted_domain = parse_options()
|
|
|
|
api.bootstrap(
|
|
in_server=True, log=None, context="server", confdir=paths.ETC_IPA
|
|
)
|
|
api.finalize()
|
|
|
|
# Only import trust plugin after api is initialized or internal imports
|
|
# within the plugin will not work
|
|
from ipaserver.plugins import trust
|
|
|
|
# We have to dance with two different credentials caches:
|
|
# ccache_name -- for cifs/ipa.master@IPA.REALM to communicate with LDAP
|
|
# oneway_ccache_name -- for IPA$@AD.REALM to communicate with AD DCs
|
|
#
|
|
# ccache_name may not exist, we'll have to initialize it from Samba's keytab
|
|
#
|
|
# oneway_ccache_name may not exist either but to initialize it, we need
|
|
# to check if oneway_keytab_name keytab exists and fetch it first otherwise.
|
|
#
|
|
# to fetch oneway_keytab_name keytab, we need to initialize ccache_name ccache first
|
|
# and retrieve our own NetBIOS domain name and use cifs/ipa.master@IPA.REALM to
|
|
# retrieve the keys to oneway_keytab_name.
|
|
|
|
keytab_name = "/etc/samba/samba.keytab"
|
|
|
|
principal = str("cifs/" + api.env.host)
|
|
|
|
oneway_ccache_name = "/var/run/ipa/krb5cc_oddjob_trusts_fetch"
|
|
ccache_name = "/var/run/ipa/krb5cc_oddjob_trusts"
|
|
|
|
# Standard sequence:
|
|
# - check if ccache exists
|
|
# - if not, initialize it from Samba's keytab
|
|
# - check if ccache contains valid TGT
|
|
# - if not, initialize it from Samba's keytab
|
|
# - refer the correct ccache object for further use
|
|
#
|
|
have_ccache = False
|
|
try:
|
|
cred = kinit_keytab(principal, keytab_name, ccache_name)
|
|
if cred.lifetime > 0:
|
|
have_ccache = True
|
|
except (gssapi.exceptions.ExpiredCredentialsError, gssapi.raw.misc.GSSError):
|
|
pass
|
|
if not have_ccache:
|
|
# delete stale ccache and try again
|
|
if os.path.exists(ccache_name):
|
|
os.unlink(ccache_name)
|
|
cred = kinit_keytab(principal, keytab_name, ccache_name)
|
|
|
|
old_ccache = os.environ.get("KRB5CCNAME")
|
|
old_config = os.environ.get("KRB5_CONFIG")
|
|
api.Backend.ldap2.connect(ccache_name)
|
|
|
|
# Retrieve own NetBIOS name and trusted forest's name.
|
|
# We use script's input to retrieve the trusted forest's name to sanitize input
|
|
# for file-level access as we might need to wipe out keytab in /var/lib/sss/keytabs
|
|
own_trust_dn = DN(
|
|
("cn", api.env.domain), ("cn", "ad"), ("cn", "etc"), api.env.basedn
|
|
)
|
|
own_trust_entry = api.Backend.ldap2.get_entry(own_trust_dn, ["ipantflatname"])
|
|
own_trust_flatname = own_trust_entry.single_value.get("ipantflatname").upper()
|
|
trusted_domain_dn = DN(
|
|
("cn", trusted_domain.lower()), api.env.container_adtrusts, api.env.basedn
|
|
)
|
|
trusted_domain_entry = api.Backend.ldap2.get_entry(trusted_domain_dn, ["cn"])
|
|
trusted_domain = trusted_domain_entry.single_value.get("cn").lower()
|
|
|
|
# At this point if we didn't find trusted forest name, an exception will be raised
|
|
# and script will quit. This is actually intended.
|
|
|
|
# Generate MIT Kerberos configuration file that potentially overlays
|
|
# the KDC to connect to for a trusted domain to allow --server option
|
|
# to take precedence.
|
|
cfg_file, cfg = generate_krb5_config(trusted_domain, options.server)
|
|
|
|
if not (options.admin and options.password):
|
|
oneway_keytab_name = "/var/lib/sss/keytabs/" + trusted_domain + ".keytab"
|
|
oneway_principal = str(
|
|
"%s$@%s" % (own_trust_flatname, trusted_domain.upper())
|
|
)
|
|
|
|
# If keytab does not exist, retrieve it
|
|
if not os.path.isfile(oneway_keytab_name):
|
|
retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
|
|
|
|
try:
|
|
have_ccache = False
|
|
try:
|
|
# The keytab may have stale key material (from older trust-add run)
|
|
cred = kinit_keytab(
|
|
oneway_principal,
|
|
oneway_keytab_name,
|
|
oneway_ccache_name,
|
|
config=cfg,
|
|
)
|
|
if cred.lifetime > 0:
|
|
have_ccache = True
|
|
except (gssapi.exceptions.ExpiredCredentialsError, gssapi.raw.misc.GSSError):
|
|
pass
|
|
if not have_ccache:
|
|
if os.path.exists(oneway_ccache_name):
|
|
os.unlink(oneway_ccache_name)
|
|
kinit_keytab(
|
|
oneway_principal,
|
|
oneway_keytab_name,
|
|
oneway_ccache_name,
|
|
config=cfg,
|
|
)
|
|
except (gssapi.exceptions.GSSError, gssapi.raw.misc.GSSError):
|
|
# If there was failure on using keytab, assume it is stale and retrieve again
|
|
retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal)
|
|
if os.path.exists(oneway_ccache_name):
|
|
os.unlink(oneway_ccache_name)
|
|
cred = kinit_keytab(
|
|
oneway_principal,
|
|
oneway_keytab_name,
|
|
oneway_ccache_name,
|
|
config=cfg,
|
|
)
|
|
else:
|
|
cred = kinit_password(
|
|
options.admin,
|
|
options.password,
|
|
oneway_ccache_name,
|
|
canonicalize=True,
|
|
enterprise=True,
|
|
config=cfg,
|
|
)
|
|
|
|
if cred and cred.lifetime > 0:
|
|
have_ccache = True
|
|
|
|
if not have_ccache:
|
|
sys.exit(1)
|
|
|
|
# We are done: we have ccache with TDO credentials and can fetch domains
|
|
ipa_domain = api.env.domain
|
|
os.environ["KRB5CCNAME"] = oneway_ccache_name
|
|
os.environ["KRB5_CONFIG"] = cfg
|
|
|
|
# retrieve the forest root domain name and contact it to retrieve trust
|
|
# topology info
|
|
forest_root = get_forest_root_domain(
|
|
api, trusted_domain, server=options.server
|
|
)
|
|
domains = dcerpc.fetch_domains(
|
|
api, ipa_domain, forest_root, creds=True, server=options.server
|
|
)
|
|
|
|
if old_ccache:
|
|
os.environ["KRB5CCNAME"] = old_ccache
|
|
|
|
if old_config:
|
|
os.environ["KRB5_CONFIG"] = old_config
|
|
|
|
if cfg_file:
|
|
os.remove(cfg_file)
|
|
|
|
trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)[
|
|
"result"
|
|
]
|
|
trust.add_new_domains_from_trust(api, None, trust_domain_object, domains)
|
|
|
|
sys.exit(0)
|