diff --git a/freeipa.spec.in b/freeipa.spec.in index 457989293..22ca6007d 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -597,7 +597,7 @@ Requires: python2-sssdconfig Requires: cyrus-sasl-gssapi%{?_isa} Requires: chrony Requires: krb5-workstation >= %{krb5_version} -Requires: authconfig +Requires: authselect >= 0.4-2 Requires: curl # NIS domain name config: /usr/lib/systemd/system/*-domainname.service Requires: initscripts diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index 0526833dd..7acbd54dc 100644 --- a/ipaclient/install/client.py +++ b/ipaclient/install/client.py @@ -2036,6 +2036,22 @@ def install_check(options): "Invalid hostname, '{}' must not be used.".format(hostname), rval=CLIENT_INSTALL_ERROR) + # --no-sssd is not supported any more for rhel-based distros + if not tasks.is_nosssd_supported() and not options.sssd: + raise ScriptError( + "Option '--no-sssd' is incompatible with the 'authselect' tool " + "provided by this distribution for configuring system " + "authentication resources", + rval=CLIENT_INSTALL_ERROR) + + # --noac is not supported any more for rhel-based distros + if not tasks.is_nosssd_supported() and options.no_ac: + raise ScriptError( + "Option '--noac' is incompatible with the 'authselect' tool " + "provided by this distribution for configuring system " + "authentication resources", + rval=CLIENT_INSTALL_ERROR) + # when installing with '--no-sssd' option, check whether nss-ldap is # installed if not options.sssd: @@ -2899,9 +2915,11 @@ def _install(options): if not options.no_ac: # Modify nsswitch/pam stack - tasks.modify_nsswitch_pam_stack(sssd=options.sssd, - mkhomedir=options.mkhomedir, - statestore=statestore) + tasks.modify_nsswitch_pam_stack( + sssd=options.sssd, + mkhomedir=options.mkhomedir, + statestore=statestore + ) logger.info("%s enabled", "SSSD" if options.sssd else "LDAP") diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index 06c621991..185176d3a 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -376,6 +376,8 @@ class BasePathNamespace(object): IF_INET6 = '/proc/net/if_inet6' WSGI_PREFIX_DIR = "/run/httpd/wsgi" AUTHCONFIG = None + AUTHSELECT = None + SYSCONF_NETWORK = None IPA_SERVER_UPGRADE = '/usr/sbin/ipa-server-upgrade' KEYCTL = '/usr/bin/keyctl' GETENT = '/usr/bin/getent' diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py index f22c4c1b2..72b7f3d8f 100644 --- a/ipaplatform/base/tasks.py +++ b/ipaplatform/base/tasks.py @@ -135,7 +135,7 @@ class BaseTaskNamespace(object): def modify_nsswitch_pam_stack(self, sssd, mkhomedir, statestore): """ - If sssd flag is true, configure pam and nsswtich so that SSSD is used + If sssd flag is true, configure pam and nsswitch so that SSSD is used for retrieving user information and authentication. Otherwise, configure pam and nsswitch to leverage pure LDAP. @@ -150,6 +150,13 @@ class BaseTaskNamespace(object): raise NotImplementedError() + def is_nosssd_supported(self): + """ + Check if the flag --no-sssd is supported for client install. + """ + + return True + def backup_auth_configuration(self, path): """ Create backup of access control configuration. @@ -165,6 +172,12 @@ class BaseTaskNamespace(object): """ raise NotImplementedError() + def migrate_auth_configuration(self, statestore): + """ + Migrate pam stack configuration to authselect. + """ + return + def set_selinux_booleans(self, required_settings, backup_func=None): """Set the specified SELinux booleans diff --git a/ipaplatform/redhat/authconfig.py b/ipaplatform/redhat/authconfig.py index 3203e096e..2c7bd0a12 100644 --- a/ipaplatform/redhat/authconfig.py +++ b/ipaplatform/redhat/authconfig.py @@ -19,6 +19,10 @@ # along with this program. If not, see . from __future__ import absolute_import +import logging +import six +import abc +import re from ipaplatform.paths import paths from ipapython import ipautil @@ -27,8 +31,192 @@ import os FILES_TO_NOT_BACKUP = ['passwd', 'group', 'shadow', 'gshadow'] +logger = logging.getLogger(__name__) -class RedHatAuthConfig(object): + +def get_auth_tool(): + return RedHatAuthSelect() + + +@six.add_metaclass(abc.ABCMeta) +class RedHatAuthToolBase(object): + + @abc.abstractmethod + def configure(self, sssd, mkhomedir, statestore): + pass + + @abc.abstractmethod + def unconfigure(self, fstore, statestore, + was_sssd_installed, + was_sssd_configured): + pass + + @abc.abstractmethod + def backup(self, path): + """ + Backup the system authentication resources configuration + :param path: directory where the backup will be stored + """ + pass + + @abc.abstractmethod + def restore(self, path): + """ + Restore the system authentication resources configuration from a backup + :param path: directory where the backup is stored + """ + pass + + @abc.abstractmethod + def set_nisdomain(self, nisdomain): + pass + + +class RedHatAuthSelect(RedHatAuthToolBase): + + def _get_authselect_current_output(self): + try: + current = ipautil.run( + [paths.AUTHSELECT, "current"], env={"LC_ALL": "C.UTF8"}) + except ipautil.CalledProcessError: + logger.debug("Current configuration not managed by authselect") + return None + + return current.raw_output.decode() + + def _parse_authselect_output(self, output_text=None): + """ + Parses the output_text to extract the profile and options. + When no text is provided, runs the 'authselect profile' command to + generate the text to be parsed. + """ + if output_text is None: + output_text = self._get_authselect_current_output() + if output_text is None: + return None + + cfg_params = re.findall( + r"\s*Profile ID:\s*(\S+)\s*\n\s*Enabled features:\s*(.*)", + output_text, + re.DOTALL + ) + + profile = cfg_params[0][0] + + if not profile: + return None + + features = re.findall(r"-\s*(\S+)", cfg_params[0][1], re.DOTALL) + + return profile, features + + def configure(self, sssd, mkhomedir, statestore): + # In the statestore, the following keys are used for the + # 'authselect' module: + # profile: name of the profile configured pre-installation + # features_list: lsit of features configured pre-installation + # mkhomedir: True if installation was called with --mkhomedir + # profile and features_list are used when reverting to the + # pre-install state + cfg = self._parse_authselect_output() + if cfg: + statestore.backup_state('authselect', 'profile', cfg[0]) + statestore.backup_state( + 'authselect', 'features_list', " ".join(cfg[1])) + else: + # cfg = None means that the current conf is not managed by + # authselect but by authconfig. + # As we are using authselect to configure the host, + # it will not be possible to revert to a custom authconfig + # configuration later (during uninstall) + # Best thing to do will be to use sssd profile at this time + logger.warning( + "WARNING: The configuration pre-client installation is not " + "managed by authselect and cannot be backed up. " + "Uninstallation may not be able to revert to the original " + "state.") + + cmd = [paths.AUTHSELECT, "select", "sssd"] + if mkhomedir: + cmd.append("with-mkhomedir") + statestore.backup_state('authselect', 'mkhomedir', True) + cmd.append("--force") + + ipautil.run(cmd) + + def unconfigure( + self, fstore, statestore, was_sssd_installed, was_sssd_configured + ): + if not statestore.has_state('authselect'): + logger.warning( + "WARNING: Unable to revert to the pre-installation state " + "('authconfig' tool has been deprecated in favor of " + "'authselect'). The default sssd profile will be used " + "instead.") + # Build the equivalent command line that will be displayed + # to the user + # This is a copy-paste of unconfigure code, except that it + # creates the command line but does not actually call it + authconfig = RedHatAuthConfig() + authconfig.prepare_unconfigure( + fstore, statestore, was_sssd_installed, was_sssd_configured) + args = authconfig.build_args() + logger.warning( + "The authconfig arguments would have been: authconfig %s", + " ".join(args)) + + profile = 'sssd' + features = '' + else: + profile = statestore.restore_state('authselect', 'profile') + features = statestore.restore_state('authselect', 'features_list') + statestore.delete_state('authselect', 'mkhomedir') + + cmd = [paths.AUTHSELECT, "select", profile, features, "--force"] + ipautil.run(cmd) + + def backup(self, path): + current = self._get_authselect_current_output() + if current is None: + return + + if not os.path.exists(path): + os.makedirs(path) + + with open(os.path.join(path, "authselect.backup"), 'w') as f: + f.write(current) + + def restore(self, path): + with open(os.path.join(path, "authselect.backup"), "r") as f: + cfg = self._parse_authselect_output(f.read()) + + if cfg: + profile = cfg[0] + + cmd = [ + paths.AUTHSELECT, "select", profile, + " ".join(cfg[1]), "--force"] + ipautil.run(cmd) + + def set_nisdomain(self, nisdomain): + try: + with open(paths.SYSCONF_NETWORK, 'r') as f: + content = [ + line for line in f + if not line.strip().upper().startswith('NISDOMAIN') + ] + except IOError: + content = [] + + content.append("NISDOMAIN={}\n".format(nisdomain)) + + with open(paths.SYSCONF_NETWORK, 'w') as f: + f.writelines(content) + + +# RedHatAuthConfig concrete class definition to be removed later +# when agreed on exact path to migrate to authselect +class RedHatAuthConfig(RedHatAuthToolBase): """ AuthConfig class implements system-independent interface to configure system authentication resources. In Red Hat systems this is done with @@ -95,6 +283,60 @@ class RedHatAuthConfig(object): except ipautil.CalledProcessError: raise ScriptError("Failed to execute authconfig command") + def configure(self, sssd, mkhomedir, statestore): + if sssd: + statestore.backup_state('authconfig', 'sssd', True) + statestore.backup_state('authconfig', 'sssdauth', True) + self.enable("sssd") + self.enable("sssdauth") + else: + statestore.backup_state('authconfig', 'ldap', True) + self.enable("ldap") + self.enable("forcelegacy") + + statestore.backup_state('authconfig', 'krb5', True) + self.enable("krb5") + self.add_option("nostart") + + if mkhomedir: + statestore.backup_state('authconfig', 'mkhomedir', True) + self.enable("mkhomedir") + + self.execute() + self.reset() + + def prepare_unconfigure(self, fstore, statestore, + was_sssd_installed, + was_sssd_configured): + if statestore.has_state('authconfig'): + # disable only those configurations that we enabled during install + for conf in ('ldap', 'krb5', 'sssd', 'sssdauth', 'mkhomedir'): + cnf = statestore.restore_state('authconfig', conf) + # Do not disable sssd, as this can cause issues with its later + # uses. Remove it from statestore however, so that it becomes + # empty at the end of uninstall process. + if cnf and conf != 'sssd': + self.disable(conf) + else: + # There was no authconfig status store + # It means the code was upgraded after original install + # Fall back to old logic + self.disable("ldap") + self.disable("krb5") + if not(was_sssd_installed and was_sssd_configured): + # Only disable sssdauth. Disabling sssd would cause issues + # with its later uses. + self.disable("sssdauth") + self.disable("mkhomedir") + + def unconfigure(self, fstore, statestore, + was_sssd_installed, + was_sssd_configured): + self.prepare_unconfigure( + fstore, statestore, was_sssd_installed, was_sssd_configured) + self.execute() + self.reset() + def backup(self, path): try: ipautil.run([paths.AUTHCONFIG, "--savebackup", path]) @@ -116,3 +358,9 @@ class RedHatAuthConfig(object): ipautil.run([paths.AUTHCONFIG, "--restorebackup", path]) except ipautil.CalledProcessError: raise ScriptError("Failed to execute authconfig command") + + def set_nisdomain(self, nisdomain): + # Let authconfig setup the permanent configuration + self.reset() + self.add_parameter("nisdomain", nisdomain) + self.execute() diff --git a/ipaplatform/redhat/paths.py b/ipaplatform/redhat/paths.py index 5706d46cd..8ccd04bb5 100644 --- a/ipaplatform/redhat/paths.py +++ b/ipaplatform/redhat/paths.py @@ -37,6 +37,8 @@ class RedHatPathNamespace(BasePathNamespace): PAM_KRB5_SO = BasePathNamespace.PAM_KRB5_SO_64 BIND_LDAP_SO = BasePathNamespace.BIND_LDAP_SO_64 AUTHCONFIG = '/usr/sbin/authconfig' + AUTHSELECT = '/usr/bin/authselect' + SYSCONF_NETWORK = '/etc/sysconfig/network' paths = RedHatPathNamespace() diff --git a/ipaplatform/redhat/tasks.py b/ipaplatform/redhat/tasks.py index dee5fe0d2..c1d33c08b 100644 --- a/ipaplatform/redhat/tasks.py +++ b/ipaplatform/redhat/tasks.py @@ -45,7 +45,7 @@ import ipapython.errors from ipaplatform.constants import constants from ipaplatform.paths import paths -from ipaplatform.redhat.authconfig import RedHatAuthConfig +from ipaplatform.redhat.authconfig import get_auth_tool from ipaplatform.base.tasks import BaseTaskNamespace logger = logging.getLogger(__name__) @@ -186,70 +186,67 @@ class RedHatTaskNamespace(BaseTaskNamespace): was_sssd_installed, was_sssd_configured): - auth_config = RedHatAuthConfig() - if statestore.has_state('authconfig'): - # disable only those configurations that we enabled during install - for conf in ('ldap', 'krb5', 'sssd', 'sssdauth', 'mkhomedir'): - cnf = statestore.restore_state('authconfig', conf) - # Do not disable sssd, as this can cause issues with its later - # uses. Remove it from statestore however, so that it becomes - # empty at the end of uninstall process. - if cnf and conf != 'sssd': - auth_config.disable(conf) - else: - # There was no authconfig status store - # It means the code was upgraded after original install - # Fall back to old logic - auth_config.disable("ldap") - auth_config.disable("krb5") - if not(was_sssd_installed and was_sssd_configured): - # Only disable sssdauth. Disabling sssd would cause issues - # with its later uses. - auth_config.disable("sssdauth") - auth_config.disable("mkhomedir") - - auth_config.execute() + auth_config = get_auth_tool() + auth_config.unconfigure( + fstore, statestore, was_sssd_installed, was_sssd_configured + ) def set_nisdomain(self, nisdomain): - # Let authconfig setup the permanent configuration - auth_config = RedHatAuthConfig() - auth_config.add_parameter("nisdomain", nisdomain) - auth_config.execute() + try: + with open(paths.SYSCONF_NETWORK, 'r') as f: + content = [ + line for line in f + if not line.strip().upper().startswith('NISDOMAIN') + ] + except IOError: + content = [] + + content.append("NISDOMAIN={}\n".format(nisdomain)) + + with open(paths.SYSCONF_NETWORK, 'w') as f: + f.writelines(content) def modify_nsswitch_pam_stack(self, sssd, mkhomedir, statestore): - auth_config = RedHatAuthConfig() + auth_config = get_auth_tool() + auth_config.configure(sssd, mkhomedir, statestore) - if sssd: - statestore.backup_state('authconfig', 'sssd', True) - statestore.backup_state('authconfig', 'sssdauth', True) - auth_config.enable("sssd") - auth_config.enable("sssdauth") - else: - statestore.backup_state('authconfig', 'ldap', True) - auth_config.enable("ldap") - auth_config.enable("forcelegacy") - - if mkhomedir: - statestore.backup_state('authconfig', 'mkhomedir', True) - auth_config.enable("mkhomedir") - - auth_config.execute() - - def modify_pam_to_use_krb5(self, statestore): - auth_config = RedHatAuthConfig() - statestore.backup_state('authconfig', 'krb5', True) - auth_config.enable("krb5") - auth_config.add_option("nostart") - auth_config.execute() + def is_nosssd_supported(self): + # The flag --no-sssd is not supported any more for rhel-based distros + return False def backup_auth_configuration(self, path): - auth_config = RedHatAuthConfig() + auth_config = get_auth_tool() auth_config.backup(path) def restore_auth_configuration(self, path): - auth_config = RedHatAuthConfig() + auth_config = get_auth_tool() auth_config.restore(path) + def migrate_auth_configuration(self, statestore): + """ + Migrate the pam stack configuration from authconfig to an authselect + profile. + """ + # Check if mkhomedir was enabled during installation + mkhomedir = statestore.get_state('authconfig', 'mkhomedir') + + # Force authselect 'sssd' profile + authselect_cmd = [paths.AUTHSELECT, "select", "sssd"] + if mkhomedir: + authselect_cmd.append("with-mkhomedir") + authselect_cmd.append("--force") + ipautil.run(authselect_cmd) + + # Remove all remaining keys from the authconfig module + for conf in ('ldap', 'krb5', 'sssd', 'sssdauth', 'mkhomedir'): + statestore.restore_state('authconfig', conf) + + # Create new authselect module in the statestore + statestore.backup_state('authselect', 'profile', 'sssd') + statestore.backup_state( + 'authselect', 'features_list', '') + statestore.backup_state('authselect', 'mkhomedir', bool(mkhomedir)) + def reload_systemwide_ca_store(self): try: ipautil.run([paths.UPDATE_CA_TRUST]) @@ -408,7 +405,6 @@ class RedHatTaskNamespace(BaseTaskNamespace): if fstore.has_file(filepath): fstore.restore_file(filepath) - def set_selinux_booleans(self, required_settings, backup_func=None): def get_setsebool_args(changes): args = [paths.SETSEBOOL, "-P"] diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py index 2e44a295c..1fcf9a7ef 100644 --- a/ipaserver/install/server/upgrade.py +++ b/ipaserver/install/server/upgrade.py @@ -1654,6 +1654,21 @@ def update_replica_config(db_suffix): api.Backend.ldap2.update_entry(entry) +def migrate_to_authselect(): + logger.info('[Migrating to authselect profile]') + if sysupgrade.get_upgrade_state('authcfg', 'migrated_to_authselect'): + logger.info("Already migrated to authselect profile") + return + + statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + try: + tasks.migrate_auth_configuration(statestore) + except ipautil.CalledProcessError as e: + raise RuntimeError( + "Failed to migrate to authselect profile: %s" % e, 1) + sysupgrade.set_upgrade_state('authcfg', 'migrated_to_authselect', True) + + def upgrade_configuration(): """ Execute configuration upgrade of the IPA services @@ -1933,6 +1948,7 @@ def upgrade_configuration(): ca.setup_lightweight_ca_key_retrieval() cainstance.ensure_ipa_authority_entry() + migrate_to_authselect() set_sssd_domain_option('ipa_server_mode', 'True') set_sssd_domain_option('ipa_server', api.env.host)