Imported Upstream version 4.7.2

This commit is contained in:
Mario Fetka
2021-08-09 20:54:00 +02:00
parent 3bfaa6e020
commit a791de49a2
2175 changed files with 1764288 additions and 331861 deletions

View File

@@ -17,12 +17,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import print_function, absolute_import
from contextlib import contextmanager
import logging
import os
from ipalib import api
from ipalib.errors import ValidationError
from ipapython import admintool
from textwrap import wrap
from ipapython.ipa_log_manager import log_mgr
from ipalib import api
from ipalib.plugable import Plugin, API
from ipalib.errors import ValidationError
from ipaplatform.paths import paths
from ipapython import admintool
from ipapython.ipa_log_manager import Filter
from ipaserver.install import installutils
"""
@@ -35,6 +43,9 @@ The class can run any arbitrary code or IPA command via api.Command['command']()
calls. It needs to override get_info() method, which returns the formatted
advice string.
Important! Do not forget to register the class to the API.
>>> @register()
>>> class sample_advice(Advice):
>>> description = 'Instructions for machine with SSSD 1.0 setup.'
@@ -65,12 +76,327 @@ As a result, you can redirect the advice's output directly to a script file.
# ipa-advise sample-advice > script.sh
# ./script.sh
Important! Do not forget to register the class to the API.
>>> api.register(sample_advice)
"""
DEFAULT_INDENTATION_INCREMENT = 2
class _IndentationTracker(object):
"""
A simple wrapper that tracks the indentation level of the generated bash
commands
"""
def __init__(self, spaces_per_indent=0):
if spaces_per_indent <= 0:
raise ValueError(
"Indentation increments cannot be zero or negative")
self.spaces_per_indent = spaces_per_indent
self._indentation_stack = []
self._total_indentation_level = 0
@property
def indentation_string(self):
"""
return a string containing number of spaces corresponding to
indentation level
"""
return " " * self._total_indentation_level
def indent(self):
"""
track a single indentation of the generated code
"""
self._indentation_stack.append(self.spaces_per_indent)
self._recompute_indentation_level()
def _recompute_indentation_level(self):
"""
Track total indentation level of the generated code
"""
self._total_indentation_level = sum(self._indentation_stack)
def dedent(self):
"""
track a single dedentation of the generated code
dedents that would result in zero or negative indentation level will be
ignored
"""
try:
self._indentation_stack.pop()
except IndexError:
# can not dedent any further
pass
self._recompute_indentation_level()
class CompoundStatement(object):
"""
Wrapper around indented blocks of Bash statements.
Override `begin_statement` and `end_statement` methods to issue
opening/closing commands using the passed in _AdviceOutput instance
"""
def __init__(self, advice_output):
self.advice_output = advice_output
def __enter__(self):
self.begin_statement()
self.advice_output.indent()
def begin_statement(self):
pass
def __exit__(self, exc_type, exc_value, traceback):
self.advice_output.dedent()
self.end_statement()
def end_statement(self):
pass
class IfBranch(CompoundStatement):
"""
Base wrapper around `if` branch. The closing statement is empty so it
leaves trailing block that can be closed off or continued by else branches
"""
def __init__(self, advice_output, conditional):
super(IfBranch, self).__init__(advice_output)
self.conditional = conditional
def begin_statement(self):
self.advice_output.command('if {}'.format(self.conditional))
self.advice_output.command('then')
class ElseIfBranch(CompoundStatement):
"""
Wrapper for `else if <CONDITIONAL>`
"""
def __init__(self, advice_output, alternative_conditional):
super(ElseIfBranch, self).__init__(advice_output)
self.alternative_conditional = alternative_conditional
def begin_statement(self):
command = 'else if {}'.format(self.alternative_conditional)
self.advice_output.command(command)
class ElseBranch(CompoundStatement):
"""
Wrapper for final `else` block
"""
def begin_statement(self):
self.advice_output.command('else')
def end_statement(self):
self.advice_output.command('fi')
class UnbranchedIfStatement(IfBranch):
"""
Plain `if` without branches
"""
def end_statement(self):
self.advice_output.command('fi')
class ForLoop(CompoundStatement):
"""
Wrapper around the for loop
"""
def __init__(self, advice_output, loop_variable, iterable):
super(ForLoop, self).__init__(advice_output)
self.loop_variable = loop_variable
self.iterable = iterable
def begin_statement(self):
self.advice_output.command(
'for {} in {}'.format(self.loop_variable, self.iterable))
self.advice_output.command('do')
def end_statement(self):
self.advice_output.command('done')
class _AdviceOutput(object):
def __init__(self):
self.content = []
self.prefix = '# '
self.options = None
self._indentation_tracker = _IndentationTracker(
spaces_per_indent=DEFAULT_INDENTATION_INCREMENT)
def indent(self):
"""
Indent the statements by one level
"""
self._indentation_tracker.indent()
def dedent(self):
"""
Dedent the statements by one level
"""
self._indentation_tracker.dedent()
@contextmanager
def indented_block(self):
self.indent()
try:
yield
finally:
self.dedent()
def comment(self, line, wrapped=True):
if wrapped:
self.append_wrapped_and_indented_comment(line)
else:
self.append_comment(line)
def append_wrapped_and_indented_comment(self, line, character_limit=70):
"""
append wrapped and indented comment to the output
"""
for wrapped_indented_line in wrap(
self.indent_statement(line), character_limit):
self.append_comment(wrapped_indented_line)
def append_comment(self, line):
self.append_statement(self.prefix + line)
def append_statement(self, statement):
"""
Append a line to the generated content indenting it by tracked number
of spaces
"""
self.content.append(self.indent_statement(statement))
def indent_statement(self, statement):
return '{indent}{statement}'.format(
indent=self._indentation_tracker.indentation_string,
statement=statement)
def debug(self, line):
if self.options.verbose:
self.comment('DEBUG: ' + line)
def command(self, line):
self.append_statement(line)
def echo_error(self, error_message):
self.command(self._format_error(error_message))
def _format_error(self, error_message):
return 'echo "{}" >&2'.format(error_message)
def exit_on_failed_command(self, command_to_run,
error_message_lines):
self.command(command_to_run)
self.exit_on_predicate(
'[ "$?" -ne "0" ]',
error_message_lines)
def exit_on_nonroot_euid(self):
self.exit_on_predicate(
'[ "$(id -u)" -ne "0" ]',
["This script has to be run as root user"]
)
def exit_on_predicate(self, predicate, error_message_lines):
with self.unbranched_if(predicate):
for error_message_line in error_message_lines:
self.command(self._format_error(error_message_line))
self.command('exit 1')
@contextmanager
def unbranched_if(self, predicate):
with self._compound_statement(UnbranchedIfStatement, predicate):
yield
@contextmanager
def _compound_statement(self, statement_cls, *args):
with statement_cls(self, *args):
yield
def commands_on_predicate(self, predicate, commands_to_run_when_true,
commands_to_run_when_false=None):
if commands_to_run_when_false is not None:
if_statement = self.if_branch
else:
if_statement = self.unbranched_if
with if_statement(predicate):
for command_to_run_when_true in commands_to_run_when_true:
self.command(
command_to_run_when_true)
if commands_to_run_when_false is not None:
with self.else_branch():
for command_to_run_when_false in commands_to_run_when_false:
self.command(command_to_run_when_false)
@contextmanager
def if_branch(self, predicate):
with self._compound_statement(IfBranch, predicate):
yield
@contextmanager
def else_branch(self):
with self._compound_statement(ElseBranch):
yield
@contextmanager
def else_if_branch(self, predicate):
with self._compound_statement(ElseIfBranch, predicate):
yield
@contextmanager
def for_loop(self, loop_variable, iterable):
with self._compound_statement(ForLoop, loop_variable, iterable):
yield
class Advice(Plugin):
"""
Base class for advices, plugins for ipa-advise.
"""
options = None
require_root = False
description = ''
def __init__(self, api):
super(Advice, self).__init__(api)
self.log = _AdviceOutput()
def set_options(self, options):
self.options = options
self.log.options = options
def get_info(self):
"""
This method should be overridden by child Advices.
Returns a string with instructions.
"""
raise NotImplementedError
class AdviseAPI(API):
bases = (Advice,)
@property
def packages(self):
import ipaserver.advise.plugins
return (ipaserver.advise.plugins,)
advise_api = AdviseAPI()
class IpaAdvise(admintool.AdminTool):
"""
@@ -93,6 +419,7 @@ class IpaAdvise(admintool.AdminTool):
def validate_options(self):
super(IpaAdvise, self).validate_options(needs_root=False)
installutils.check_server_configuration()
if len(self.args) > 1:
raise self.option_parser.error("You can only provide one "
@@ -104,12 +431,12 @@ class IpaAdvise(admintool.AdminTool):
def print_config_list(self):
self.print_header('List of available advices')
max_keyword_len = max((len(keyword) for keyword in api.Advice))
max_keyword_len = max(
(len(advice.name) for advice in advise_api.Advice))
for keyword in api.Advice:
advice = getattr(api.Advice, keyword, '')
for advice in advise_api.Advice:
description = getattr(advice, 'description', '')
keyword = keyword.replace('_', '-')
keyword = advice.name.replace('_', '-')
# Compute the number of spaces needed for the table to be aligned
offset = max_keyword_len - len(keyword)
@@ -117,11 +444,11 @@ class IpaAdvise(admintool.AdminTool):
wrapped_description = wrap(description, 80 - len(prefix))
# Print the first line with the prefix (keyword)
print prefix + wrapped_description[0]
print(prefix + wrapped_description[0])
# Print the rest wrapped behind the colon
for line in wrapped_description[1:]:
print "{off}{line}".format(off=' ' * len(prefix), line=line)
print("{off}{line}".format(off=' ' * len(prefix), line=line))
def print_header(self, header, print_shell=False):
header_size = len(header)
@@ -129,17 +456,17 @@ class IpaAdvise(admintool.AdminTool):
prefix = ''
if print_shell:
prefix = '# '
print '#!/bin/sh'
print('#!/bin/sh')
# Do not print out empty header
if header_size > 0:
print(prefix + '-' * 70)
print((prefix + '-' * 70))
for line in wrap(header, 70):
print(prefix + line)
print(prefix + '-' * 70)
print((prefix + line))
print((prefix + '-' * 70))
def print_advice(self, keyword):
advice = getattr(api.Advice, keyword, None)
advice = getattr(advise_api.Advice, keyword, None)
# Ensure that Configuration class for given --setup option value exists
if advice is None:
@@ -167,17 +494,24 @@ class IpaAdvise(admintool.AdminTool):
advice.get_info()
api.Backend.rpcclient.disconnect()
for line in advice.log.content:
print line
print(line)
def run(self):
super(IpaAdvise, self).run()
api.bootstrap(in_server=False, context='advise')
api.bootstrap(in_server=False,
context='cli',
confdir=paths.ETC_IPA)
api.finalize()
advise_api.bootstrap(in_server=False,
context='cli',
confdir=paths.ETC_IPA)
advise_api.finalize()
if not self.options.verbose:
# Do not print connection information by default
logger_name = r'ipa\.ipalib\.plugins\.rpcclient'
log_mgr.configure(dict(logger_regexps=[(logger_name, 'warning')]))
logger_name = r'ipalib\.rpc'
root_logger = logging.getLogger()
root_logger.addFilter(Filter(logger_name, logging.WARNING))
# With no argument, print the list out and exit
if not self.args:

View File

@@ -0,0 +1,63 @@
#
# Copyright (C) 2018 FreeIPA Contributors see COPYING for license
#
from __future__ import absolute_import
from ipalib.plugable import Registry
from ipaserver.advise.base import Advice
register = Registry()
@register()
class enable_admins_sudo(Advice):
"""
Configures HBAC and SUDO for members of the admins group
"""
description = ("Instructions for enabling HBAC and unauthenticated "
"SUDO for members of the admins group.")
def check_ccache_not_empty(self):
self.log.comment('Check whether the credential cache is not empty')
self.log.exit_on_failed_command(
'klist',
[
"Credential cache is empty",
'Use kinit as privileged user to obtain Kerberos credentials'
])
def create_hbac_rule(self):
self.log.comment('Create the HBAC rule for sudo')
self.log.exit_on_failed_command(
'err=$(ipa hbacrule-add --hostcat=all --desc "Allow admins '
'to run sudo on all hosts" admins_sudo 2>&1)',
['Failed to add hbac rule: ${err}'])
self.log.command('ipa hbacrule-add-user --groups=admins admins_sudo')
self.log.command(
'ipa hbacrule-add-service --hbacsvcs=sudo admins_sudo'
)
def create_sudo_rule(self):
self.log.comment('Create the SUDO rule for the admins group')
self.log.exit_on_failed_command(
'err=$(ipa sudorule-add --desc "Allow admins to run any command '
'on any host" --hostcat=all --cmdcat=all admins_all '
'2>&1)',
['Failed to add sudo rule: ${err}'])
self.log.command('ipa sudorule-add-user --groups=admins admins_all')
def get_info(self):
self.check_ccache_not_empty()
with self.log.if_branch(
'ipa hbacrule-show admins_sudo > /dev/null 2>&1'):
self.log.command('echo HBAC rule admins_sudo already exists')
with self.log.else_branch():
self.create_hbac_rule()
with self.log.if_branch(
'ipa sudorule-show admins_all > /dev/null 2>&1'):
self.log.command('echo SUDO rule admins_all already exists')
with self.log.else_branch():
self.create_sudo_rule()

View File

@@ -1,41 +0,0 @@
# Authors: Tomas Babej <tbabej@redhat.com>
#
# Copyright (C) 2013 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ipalib import api
from ipalib.frontend import Advice
class config_fedora_authconfig(Advice):
"""
Provides client configuration instructions using authconfig.
"""
description = 'Authconfig instructions for configuring Fedora 18/19 '\
'client with IPA server without use of SSSD.'
def get_info(self):
self.log.debug("Hostname obtained via api.env.host")
self.log.comment("Run the following command as a root:")
template = "/sbin/authconfig --enableldap --ldapserver={server} "\
"--enablerfc2307bis --enablekrb5"
advice = template.format(server=api.env.host)
self.log.command(advice)
api.register(config_fedora_authconfig)

View File

@@ -16,11 +16,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import absolute_import
import os
from ipalib import api
from ipalib.frontend import Advice
from ipapython.ipautil import template_file, SHARE_DIR
from ipalib.plugable import Registry
from ipaplatform.paths import paths
from ipaserver.advise.base import Advice
from ipapython.ipautil import template_file
register = Registry()
CACERTDIR_REHASH_URL = ('https://pagure.io/authconfig/raw/master/f/'
'cacertdir_rehash')
class config_base_legacy_client(Advice):
@@ -44,17 +53,16 @@ class config_base_legacy_client(Advice):
'location. If this value is different on your system '
'the script needs to be modified accordingly.\n')
cacertdir_rehash = ('https://fedorahosted.org/authconfig/browser/'
'cacertdir_rehash?format=txt')
self.log.comment('Download the CA certificate of the IPA server')
self.log.command('mkdir -p -m 755 /etc/openldap/cacerts')
self.log.command('wget http://%s/ipa/config/ca.crt -O '
self.log.command('curl http://%s/ipa/config/ca.crt -o '
'/etc/openldap/cacerts/ipa.crt\n' % api.env.host)
self.log.comment('Generate hashes for the openldap library')
self.log.command('command -v cacertdir_rehash')
self.log.command('if [ $? -ne 0 ] ; then')
self.log.command(' wget "%s" -O cacertdir_rehash ;' % cacertdir_rehash)
self.log.command(' curl "%s" -o cacertdir_rehash ;' %
CACERTDIR_REHASH_URL)
self.log.command(' chmod 755 ./cacertdir_rehash ;')
self.log.command(' ./cacertdir_rehash /etc/openldap/cacerts/ ;')
self.log.command('else')
@@ -64,7 +72,7 @@ class config_base_legacy_client(Advice):
def configure_and_start_sssd(self):
uri, base = self.get_uri_and_base()
template = os.path.join(
SHARE_DIR,
paths.USR_SHARE_IPA_DIR,
'advise',
'legacy',
'sssd.conf.template'
@@ -80,6 +88,7 @@ class config_base_legacy_client(Advice):
self.log.command('service sssd start')
@register()
class config_redhat_sssd_before_1_9(config_base_legacy_client):
"""
Legacy client configuration for Red Hat based systems, using SSSD.
@@ -94,7 +103,7 @@ class config_redhat_sssd_before_1_9(config_base_legacy_client):
self.check_compat_plugin()
self.log.comment('Install required packages via yum')
self.log.command('yum install -y sssd authconfig wget openssl\n')
self.log.command('yum install -y sssd authconfig curl openssl\n')
self.configure_ca_cert()
@@ -113,9 +122,7 @@ class config_redhat_sssd_before_1_9(config_base_legacy_client):
super(config_redhat_sssd_before_1_9, self).configure_ca_cert()
api.register(config_redhat_sssd_before_1_9)
@register()
class config_generic_linux_sssd_before_1_9(config_base_legacy_client):
"""
Legacy client configuration for non Red Hat based linux systems,
@@ -130,7 +137,7 @@ class config_generic_linux_sssd_before_1_9(config_base_legacy_client):
self.check_compat_plugin()
with open(os.path.join(
SHARE_DIR,
paths.USR_SHARE_IPA_DIR,
'advise',
'legacy',
'pam.conf.sssd.template')) as fd:
@@ -138,7 +145,7 @@ class config_generic_linux_sssd_before_1_9(config_base_legacy_client):
self.log.comment('Install required packages using your system\'s '
'package manager. E.g:')
self.log.command('apt-get -y install sssd wget openssl\n')
self.log.command('apt-get -y install sssd curl openssl\n')
self.configure_ca_cert()
@@ -170,9 +177,7 @@ class config_generic_linux_sssd_before_1_9(config_base_legacy_client):
'/etc/ldap/ldap.conf\n')
api.register(config_generic_linux_sssd_before_1_9)
@register()
class config_redhat_nss_pam_ldapd(config_base_legacy_client):
"""
Legacy client configuration for Red Hat based systems,
@@ -188,14 +193,14 @@ class config_redhat_nss_pam_ldapd(config_base_legacy_client):
self.check_compat_plugin()
self.log.comment('Install required packages via yum')
self.log.command('yum install -y wget openssl nss-pam-ldapd pam_ldap '
self.log.command('yum install -y curl openssl nss-pam-ldapd pam_ldap '
'authconfig\n')
self.configure_ca_cert()
self.log.comment('Use the authconfig to configure nsswitch.conf '
'and the PAM stack')
self.log.command('authconfig --updateall --enableldap '
self.log.command('authconfig --updateall --enableldap --enableldaptls '
'--enableldapauth --ldapserver=%s --ldapbasedn=%s\n'
% (uri, base))
@@ -207,9 +212,7 @@ class config_redhat_nss_pam_ldapd(config_base_legacy_client):
super(config_redhat_nss_pam_ldapd, self).configure_ca_cert()
api.register(config_redhat_nss_pam_ldapd)
@register()
class config_generic_linux_nss_pam_ldapd(config_base_legacy_client):
"""
Legacy client configuration for non Red Hat based linux systems,
@@ -224,7 +227,7 @@ class config_generic_linux_nss_pam_ldapd(config_base_legacy_client):
self.check_compat_plugin()
with open(os.path.join(
SHARE_DIR,
paths.USR_SHARE_IPA_DIR,
'advise',
'legacy',
'pam.conf.nss_pam_ldapd.template')) as fd:
@@ -234,7 +237,7 @@ class config_generic_linux_nss_pam_ldapd(config_base_legacy_client):
self.log.comment('Install required packages using your system\'s '
'package manager. E.g:')
self.log.command('apt-get -y install wget openssl libnss-ldapd '
self.log.command('apt-get -y install curl openssl libnss-ldapd '
'libpam-ldapd nslcd\n')
self.configure_ca_cert()
@@ -276,9 +279,7 @@ class config_generic_linux_nss_pam_ldapd(config_base_legacy_client):
'/etc/ldap/ldap.conf\n')
api.register(config_generic_linux_nss_pam_ldapd)
@register()
class config_freebsd_nss_pam_ldapd(config_base_legacy_client):
"""
Legacy client configuration for FreeBSD, using nss-pam-ldapd.
@@ -293,7 +294,7 @@ class config_freebsd_nss_pam_ldapd(config_base_legacy_client):
self.check_compat_plugin()
with open(os.path.join(
SHARE_DIR,
paths.USR_SHARE_IPA_DIR,
'advise',
'legacy',
'pam_conf_sshd.template')) as fd:
@@ -343,9 +344,8 @@ class config_freebsd_nss_pam_ldapd(config_base_legacy_client):
self.log.command('curl -k https://%s/ipa/config/ca.crt > '
'%s' % (api.env.host, cacrt))
api.register(config_freebsd_nss_pam_ldapd)
@register()
class config_redhat_nss_ldap(config_base_legacy_client):
"""
Legacy client configuration for Red Hat based systems,
@@ -361,14 +361,14 @@ class config_redhat_nss_ldap(config_base_legacy_client):
self.check_compat_plugin()
self.log.comment('Install required packages via yum')
self.log.command('yum install -y wget openssl nss_ldap '
self.log.command('yum install -y curl openssl nss_ldap '
'authconfig\n')
self.configure_ca_cert()
self.log.comment('Use the authconfig to configure nsswitch.conf '
'and the PAM stack')
self.log.command('authconfig --updateall --enableldap '
self.log.command('authconfig --updateall --enableldap --enableldaptls '
'--enableldapauth --ldapserver=%s --ldapbasedn=%s\n'
% (uri, base))
@@ -378,5 +378,3 @@ class config_redhat_nss_ldap(config_base_legacy_client):
'Therefore, clients older than RHEL5.2 will not be '
'able to interoperate with IPA server 3.x.')
super(config_redhat_nss_ldap, self).configure_ca_cert()
api.register(config_redhat_nss_ldap)

View File

@@ -0,0 +1,321 @@
#
# Copyright (C) 2017 FreeIPA Contributors see COPYING for license
#
from __future__ import absolute_import
import sys
from ipalib.plugable import Registry
from ipaplatform import services
from ipaplatform.paths import paths
from ipaserver.advise.base import Advice
from ipaserver.install.httpinstance import OCSP_ENABLED, OCSP_DIRECTIVE
register = Registry()
class common_smart_card_auth_config(Advice):
"""
Common steps required to properly configure both server and client for
smart card auth
"""
systemwide_nssdb = paths.NSS_DB_DIR
smart_card_ca_certs_variable_name = "SC_CA_CERTS"
single_ca_cert_variable_name = 'ca_cert'
def check_ccache_not_empty(self):
self.log.comment('Check whether the credential cache is not empty')
self.log.exit_on_failed_command(
'klist',
[
"Credential cache is empty",
'Use kinit as privileged user to obtain Kerberos credentials'
])
def check_and_set_ca_cert_paths(self):
ca_paths_variable = self.smart_card_ca_certs_variable_name
single_ca_path_variable = self.single_ca_cert_variable_name
self.log.command("{}=$@".format(ca_paths_variable))
self.log.exit_on_predicate(
'[ -z "${}" ]'.format(ca_paths_variable),
['You need to provide one or more paths to the PEM files '
'containing CAs signing the Smart Cards']
)
with self.log.for_loop(single_ca_path_variable,
'${}'.format(ca_paths_variable)):
self.log.exit_on_predicate(
'[ ! -f "${}" ]'.format(single_ca_path_variable),
['Invalid CA certificate filename: ${}'.format(
single_ca_path_variable),
'Please check that the path exists and is a valid file']
)
def upload_smartcard_ca_certificates_to_systemwide_db(self):
# Newer version of sssd use OpenSSL and read the CA certs
# from /etc/sssd/pki/sssd_auth_ca_db.pem
self.log.command('mkdir -p /etc/sssd/pki')
with self.log.for_loop(
self.single_ca_cert_variable_name,
'${}'.format(self.smart_card_ca_certs_variable_name)):
self.log.command(
'certutil -d {} -A -i ${} -n "Smart Card CA $(uuidgen)" '
'-t CT,C,C'.format(
self.systemwide_nssdb, self.single_ca_cert_variable_name
)
)
self.log.command(
'cat ${} >> /etc/sssd/pki/sssd_auth_ca_db.pem'.format(
self.single_ca_cert_variable_name
)
)
def install_smart_card_signing_ca_certs(self):
with self.log.for_loop(
self.single_ca_cert_variable_name,
'${}'.format(self.smart_card_ca_certs_variable_name)):
self.log.exit_on_failed_command(
'ipa-cacert-manage install ${} -t CT,C,C'.format(
self.single_ca_cert_variable_name
),
['Failed to install external CA certificate to IPA']
)
def update_ipa_ca_certificate_store(self):
self.log.exit_on_failed_command(
'ipa-certupdate',
['Failed to update IPA CA certificate database']
)
@register()
class config_server_for_smart_card_auth(common_smart_card_auth_config):
"""
Configures smart card authentication via Kerberos (PKINIT) and for WebUI
"""
description = ("Instructions for enabling Smart Card authentication on "
" a single FreeIPA server. Includes Apache configuration, "
"enabling PKINIT on KDC and configuring WebUI to accept "
"Smart Card auth requests. To enable the feature in the "
"whole topology you have to run the script on each master")
ssl_conf = paths.HTTPD_SSL_CONF
ssl_ocsp_directive = OCSP_DIRECTIVE
kdc_service_name = services.knownservices.krb5kdc.systemd_name
def get_info(self):
self.log.exit_on_nonroot_euid()
self.check_and_set_ca_cert_paths()
self.check_ccache_not_empty()
self.check_hostname_is_in_masters()
self.resolve_ipaca_records()
self.enable_ssl_ocsp()
self.restart_httpd()
self.record_httpd_ocsp_status()
self.check_and_enable_pkinit()
self.enable_ok_to_auth_as_delegate_on_http_principal()
self.upload_smartcard_ca_certificates_to_systemwide_db()
self.install_smart_card_signing_ca_certs()
self.update_ipa_ca_certificate_store()
self.restart_kdc()
def check_hostname_is_in_masters(self):
self.log.comment('Check whether the host is IPA master')
self.log.exit_on_failed_command(
'ipa server-find $(hostname -f)',
["This script can be run on IPA master only"])
def resolve_ipaca_records(self):
ipa_domain_name = self.api.env.domain
self.log.comment('make sure bind-utils are installed so that we can '
'dig for ipa-ca records')
self.log.exit_on_failed_command(
'yum install -y bind-utils',
['Failed to install bind-utils'])
self.log.comment('make sure ipa-ca records are resolvable, '
'otherwise error out and instruct')
self.log.comment('the user to update the DNS infrastructure')
self.log.command('ipaca_records=$(dig +short '
'ipa-ca.{})'.format(ipa_domain_name))
self.log.exit_on_predicate(
'[ -z "$ipaca_records" ]',
[
'Can not resolve ipa-ca records for ${domain_name}',
'Please make sure to update your DNS infrastructure with ',
'ipa-ca record pointing to IP addresses of IPA CA masters'
])
def enable_ssl_ocsp(self):
self.log.comment('look for the OCSP directive in ssl.conf')
self.log.comment(' if it is present, switch it on')
self.log.comment(
'if it is absent, append it to the end of VirtualHost section')
predicate = self._interpolate_ocsp_directive_file_into_command(
"grep -q '{directive} ' {filename}")
self.log.commands_on_predicate(
predicate,
[
self._interpolate_ocsp_directive_file_into_command(
"sed -i.ipabkp -r "
"'s/^#*[[:space:]]*{directive}[[:space:]]+(on|off)$"
"/{directive} on/' {filename}")
],
commands_to_run_when_false=[
self._interpolate_ocsp_directive_file_into_command(
r"sed -i.ipabkp '/<\/VirtualHost>/i {directive} on' "
r"{filename}")
]
)
def _interpolate_ocsp_directive_file_into_command(self, fmt_line):
return self._format_command(
fmt_line, self.ssl_ocsp_directive, self.ssl_conf)
def _format_command(self, fmt_line, directive, filename):
return fmt_line.format(directive=directive, filename=filename)
def restart_httpd(self):
self.log.comment('finally restart apache')
self.log.command('systemctl restart httpd')
def record_httpd_ocsp_status(self):
self.log.comment('store the OCSP upgrade state')
self.log.command(
"{} -c 'from ipaserver.install import sysupgrade; "
"sysupgrade.set_upgrade_state(\"httpd\", "
"\"{}\", True)'".format(sys.executable, OCSP_ENABLED))
def check_and_enable_pkinit(self):
self.log.comment('check whether PKINIT is configured on the master')
with self.log.if_branch(
"ipa-pkinit-manage status | grep -q 'enabled'"):
self.log.command('echo "PKINIT already enabled"')
with self.log.else_branch():
self.log.exit_on_failed_command(
'ipa-pkinit-manage enable',
['Failed to issue PKINIT certificates to local KDC'])
def enable_ok_to_auth_as_delegate_on_http_principal(self):
self.log.comment('Enable OK-AS-DELEGATE flag on the HTTP principal')
self.log.comment('This enables smart card login to WebUI')
self.log.command(
'output=$(ipa service-mod HTTP/$(hostname -f) '
'--ok-to-auth-as-delegate=True 2>&1)')
self.log.exit_on_predicate(
'[ "$?" -ne "0" -a '
'-z "$(echo $output | grep \'no modifications\')" ]',
["Failed to set OK_AS_AUTH_AS_DELEGATE flag on HTTP principal"]
)
def restart_kdc(self):
self.log.exit_on_failed_command(
'systemctl restart {}'.format(self.kdc_service_name),
['Failed to restart KDC. Please restart the service manually.']
)
@register()
class config_client_for_smart_card_auth(common_smart_card_auth_config):
"""
Configures smart card authentication on FreeIPA client
"""
description = ("Instructions for enabling Smart Card authentication on "
" a single FreeIPA client. Configures Smart Card daemon, "
"set the system-wide trust store and configures SSSD to "
"allow smart card logins to desktop")
opensc_module_name = "OpenSC"
pkcs11_shared_lib = '/usr/lib64/opensc-pkcs11.so'
smart_card_service_file = 'pcscd.service'
smart_card_socket = 'pcscd.socket'
def get_info(self):
self.log.exit_on_nonroot_euid()
self.check_and_set_ca_cert_paths()
self.check_ccache_not_empty()
self.check_and_remove_pam_pkcs11()
self.install_opensc_and_dconf_packages()
self.install_krb5_client_dependencies()
self.start_enable_smartcard_daemon()
self.add_pkcs11_module_to_systemwide_db()
self.upload_smartcard_ca_certificates_to_systemwide_db()
self.update_ipa_ca_certificate_store()
self.run_authselect_to_configure_smart_card_auth()
self.configure_pam_cert_auth()
self.restart_sssd()
def check_and_remove_pam_pkcs11(self):
self.log.command('rpm -qi pam_pkcs11 > /dev/null')
self.log.commands_on_predicate(
'[ "$?" -eq "0" ]',
[
'yum remove -y pam_pkcs11'
]
)
def install_opensc_and_dconf_packages(self):
self.log.comment(
'authconfig often complains about missing dconf, '
'install it explicitly')
self.log.exit_on_failed_command(
'yum install -y {} dconf'.format(self.opensc_module_name.lower()),
['Could not install OpenSC package']
)
def install_krb5_client_dependencies(self):
self.log.exit_on_failed_command(
'yum install -y krb5-pkinit-openssl',
['Failed to install Kerberos client PKINIT extensions.']
)
def start_enable_smartcard_daemon(self):
self.log.command(
'systemctl start {service} {socket} '
'&& systemctl enable {service} {socket}'.format(
service=self.smart_card_service_file,
socket=self.smart_card_socket))
def add_pkcs11_module_to_systemwide_db(self):
module_name = self.opensc_module_name
nssdb = self.systemwide_nssdb
shared_lib = self.pkcs11_shared_lib
self.log.commands_on_predicate(
'modutil -dbdir {} -list | grep -q {}'.format(
nssdb, module_name),
[
'echo "{} PKCS#11 module already configured"'.format(
module_name)
],
commands_to_run_when_false=[
'echo "" | modutil -dbdir {} -add "{}" -libfile {}'.format(
nssdb, module_name, shared_lib),
]
)
def run_authselect_to_configure_smart_card_auth(self):
self.log.exit_on_failed_command(
'authselect enable-feature with-smartcard',
[
'Failed to configure Smart Card authentication in SSSD'
]
)
def configure_pam_cert_auth(self):
self.log.comment('Set pam_cert_auth=True in /etc/sssd/sssd.conf')
self.log.command(
"{} -c 'from SSSDConfig import SSSDConfig; "
"c = SSSDConfig(); c.import_config(); "
"c.set(\"pam\", \"pam_cert_auth\", \"True\"); "
"c.write()'".format(sys.executable))
def restart_sssd(self):
self.log.command('systemctl restart sssd.service')