Skip to content

Commit

Permalink
Fix and test IdM related parsers and combiners (#4178)
Browse files Browse the repository at this point in the history
* fix(parsr): do not auto-create magic methods in __getattr__

The previous code would just provide any attribute, including
the magic methods. Especially __deepcopy__ method that breaks
deepcopy(parser_object).

Signed-off-by: Pavel Březina <[email protected]>

* fix(identity_domain): use correct SSSD realm option

SSSD does not have krb5_domain, but krb5_realm.

Signed-off-by: Pavel Březina <[email protected]>

* fix(identity_domain): do not add realm twice

This realm could have been already added as SSSD domain.

Signed-off-by: Pavel Březina <[email protected]>

* fix(sssd): refactor the code, fix issues and add SSSDConfAll

Existing implementation of SSSD configuration parser did not consinder
SSSD's include directory and other new functionality.

* Add support for sssd.conf and conf.d include folder
* Add SSSDConfAll combiner that merge all configuration files together
* Add support for [domain/$dom]/enabled in addition to [sssd]/domain
* Refactor the code to follow naming convention of other
  parsers/combiners

Signed-off-by: Pavel Březina <[email protected]>

* fix(ipa): simplify code and logic

The current IPA combiner logic was quite complex, difficult to test
and prone to errors.

* It is sufficient to only test the SSSD configuration in order
  to get the server mode, other checks are redundant.
* It is sufficient if there is any SSSD domain with ipa provider
  to get the client mode
* SSSD domain name may differ from the IPA domain name, even though
  this is not the default setup, it can be changed.
* Release information was removed, it make testing unnecessarily
  more complex.
* It is possible to configure SSSD as IPA client without the
  freeipa-client package if one does not use ipa-client-install
  or realm command. Checking if SSSD package is there and IPA
  domain is configured is sufficient.

Signed-off-by: Pavel Březina <[email protected]>

* fix(identity_domain): write extensive tests

...and make krb5 configuration deterministic.

Signed-off-by: Pavel Březina <[email protected]>

---------

Signed-off-by: Pavel Březina <[email protected]>
  • Loading branch information
pbrezina authored Aug 8, 2024
1 parent 789a3fc commit 66c6081
Show file tree
Hide file tree
Showing 13 changed files with 1,196 additions and 441 deletions.
3 changes: 3 additions & 0 deletions docs/shared_combiners_catalog/sssd_conf.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.. automodule:: insights.combiners.sssd_conf
:members:
:show-inheritance:
15 changes: 9 additions & 6 deletions insights/combiners/identity_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from insights.core.plugins import combiner
from insights.combiners.ipa import IPA
from insights.parsers.samba import SambaConfigs
from insights.parsers.sssd_conf import SSSD_Config
from insights.combiners.sssd_conf import SSSDConfAll
from insights.combiners.krb5 import AllKrb5Conf


Expand Down Expand Up @@ -124,7 +124,7 @@ class IPAMode(object):
"""


@combiner(optional=[SSSD_Config, AllKrb5Conf, IPA, SambaConfigs])
@combiner(optional=[SSSDConfAll, AllKrb5Conf, IPA, SambaConfigs])
class IdentityDomain(object):
"""
A combiner for identity domains.
Expand Down Expand Up @@ -186,7 +186,7 @@ def _parse_sssd(self, sssd, ipa):
Supports id_providers "ad", "ipa", and "ldap".
"""
id_auth_providers = set(["ldap", "krb5", "ipa", "ad", "proxy"])
for name in sssd.domains:
for name in sssd.enabled_domains:
if "/" in name:
# Ignore trusted domain (subdomain) configuration. Subdomain
# settings are configured as
Expand All @@ -207,15 +207,15 @@ def _parse_sssd(self, sssd, ipa):
dtype = DomainTypes.AD_SSSD
srv = ServerSoftware.AD
domain = conf.get("ad_domain", name)
realm = conf.get("krb5_domain", domain.upper())
realm = conf.get("krb5_realm", domain.upper())
elif id_provider == "ipa":
if ipa is None or not ipa.is_client:
# unsupported configuration
continue
dtype = DomainTypes.IPA
srv = ServerSoftware.IPA
domain = conf.get("ipa_domain", name)
realm = conf.get("krb5_domain", domain.upper())
realm = conf.get("krb5_realm", domain.upper())
ipa_mode = IPAMode.IPA_SERVER if ipa.is_server else IPAMode.IPA_CLIENT
elif id_provider == "ldap":
if auth_provider == "ldap":
Expand Down Expand Up @@ -278,7 +278,10 @@ def _parse_smb(self, smb):

def _parse_krb5(self, krb5):
"""Parse krb5.conf to detect additional generic Kerberos realms"""
for realm in krb5.realms:
for realm in sorted(krb5.realms):
if realm in self._realms:
continue

self._add_domaininfo(
realm.lower(),
DomainTypes.KRB5,
Expand Down
73 changes: 34 additions & 39 deletions insights/combiners/ipa.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,35 @@
IPA - Combiner for RHEL IdM / FreeIPA information
=================================================
"""
from insights.core.plugins import combiner

from insights.combiners.sssd_conf import SSSDConfAll
from insights.core.exceptions import SkipComponent
from insights.core.plugins import combiner
from insights.parsers.installed_rpms import InstalledRpms
from insights.parsers.redhat_release import RedhatRelease
from insights.parsers.ipa_conf import IPAConfig
from insights.parsers.sssd_conf import SSSD_Config


@combiner(IPAConfig, SSSD_Config, InstalledRpms, RedhatRelease)
@combiner(IPAConfig, SSSDConfAll, InstalledRpms)
class IPA(object):
"""Combiner for IPA, SSSD, and installed RPMs
Provides additional information, e.g. whether the host is an IPA server.
"""

def __init__(self, ipa_conf, sssd_conf, rpms, release):
def __init__(self, ipa_conf, sssd_conf, rpms):
self._ipa_conf = ipa_conf
self._sssd_conf = sssd_conf
# IPA package names are different on Fedora
if release.is_fedora:
self._client_rpm = rpms.get_max("freeipa-client")
self._server_rpm = rpms.get_max("freeipa-server")
else:
self._client_rpm = rpms.get_max("ipa-client")
self._server_rpm = rpms.get_max("ipa-server")
if self._client_rpm is None:
raise SkipComponent("IPA client package is not installed")

self._is_client = None
self._is_server = None
self._ipa_domains = None

self._check_installed_packages(rpms)

def _check_installed_packages(self, rpms):
# IPA is relying on SSSD which will be installed on both client and server
if rpms.get_max("sssd") is None:
raise SkipComponent("sssd package is not installed")

@property
def ipa_conf(self):
Expand All @@ -43,42 +43,37 @@ def sssd_conf(self):
return self._sssd_conf

@property
def sssd_domain_config(self):
"""Get SSSD domain configuration for host's IPA domain"""
return self._sssd_conf.domain_config(self._ipa_conf.domain)
def sssd_ipa_domains(self):
"""Get all SSSD domains where id_provider is set to ipa"""
if self._ipa_domains is None:
self._ipa_domains = []
for domain in self.sssd_conf.enabled_domains:
id_provider = self.sssd_conf.domain_get(domain, "id_provider")
if id_provider == "ipa":
self._ipa_domains.append(domain)

return self._ipa_domains

@property
def is_client(self):
"""Is the host an IPA client?"""
# IPAConfig validates that /etc/ipa/default.conf exists and is a
# valid IPA config file with all required values present.
# Check if there is at least one IPA domain in SSSD.
if self._is_client is None:
id_provider = self.sssd_domain_config.get("id_provider")
if id_provider == "ipa":
self._is_client = True
else:
self._is_client = False
self._is_client = len(self.sssd_ipa_domains) > 0

return self._is_client

@property
def is_server(self):
"""Is the host an IPA server?"""
if self._is_server is None:
server_mode = self.sssd_domain_config.get(
"ipa_server_mode", "false"
)
if (
self._server_rpm and
# all servers are also clients
self.is_client and
# only servers use LDAPI (LDAP over Unix socket)
self._ipa_conf.ldap_uri.startswith("ldapi://") and
# SSSD domain must be in server mode
server_mode.lower() == "true"
):
self._is_server = True
else:
self._is_server = False
for domain in self.sssd_ipa_domains:
self._is_server = self.sssd_conf.domain_getboolean(
domain, "ipa_server_mode", False
)

# Break if there is at least one IPA domain in server mode
if self._is_server:
break

return self._is_server
154 changes: 154 additions & 0 deletions insights/combiners/sssd_conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
SSSD Configuration
==================
Provides access to complete SSSD configuration: /etc/sssd/sssd.conf with merged
configuration snippets from /etc/sssd/conf.d.
"""

from copy import deepcopy

from insights.core.plugins import combiner
from insights.parsers.sssd_conf import SSSDConf, SSSDConfd


@combiner(SSSDConf, SSSDConfd)
class SSSDConfAll(object):
"""
Provides access to complete SSSD configuration: /etc/sssd/sssd.conf with
merged configuration snippets from /etc/sssd/conf.d.
"""
def __init__(self, sssd_conf, sssd_conf_d):
self.config = deepcopy(sssd_conf)

for parser in sorted(sssd_conf_d, key=lambda x: x.file_name):
if parser.file_name.startswith("."):
continue

for section in parser.sections():
for key, value in parser.items(section).items():
self.config._set(section, key, value)

self._enabled_domains = None

@property
def enabled_domains(self):
"""
Returns the list of enabled domains.
Domains can be enabled either using the ``domains`` option in the
``sssd`` section of the configuration file or using the ``enabled``
option in the domain configuration.
[sssd]
domains = a, b
[domain/a]
...
[domain/b]
...
[domain/c]
enabled = true
"""
if self._enabled_domains is None:
enabled_domains = []

if self.config.has_option("sssd", "domains"):
domains = self.config.get("sssd", "domains")
enabled_domains = [domain.strip() for domain in domains.split(",")]

prefix = "domain/"
for section in self.config.sections():
# Ignore if this is not a domain configuration
if not section.startswith(prefix):
continue

name = section[len(prefix):].strip()
if not name:
# Invalid configuration
continue

# Ignore if this is a subdomain configuration
# `domain/$dom/$subdom`
if "/" in name:
continue

if self.config.has_option(section, "enabled"):
enabled = self.config.getboolean(section, "enabled")

if enabled and name not in enabled_domains:
enabled_domains.append(name)
elif not enabled and name in enabled_domains:
enabled_domains.remove(name)

self._enabled_domains = enabled_domains

return self._enabled_domains

def domain_config(self, domain):
"""
Return the configuration dictionary for a specific domain, given as
the raw name as listed in the 'domains' property of the sssd section.
This then looks for the equivalent 'domain/{domain}' section of the
config file.
"""
full_domain = self.domain_section(domain)
if full_domain not in self.config:
return {}

return self.config.items(full_domain)

def domain_section(self, domain):
"""
Transform plain SSSD domain name into a configuration section.
ipa.test -> domain/ipa.test
Args:
domain (str): SSSD domain name.
Returns:
str: Returns the configuration section.
"""
return "domain/" + domain

def domain_get(self, domain, option, default=None):
"""
Lookup option in domain.
Args:
domain (str): The SSSD domain name.
option (str): The option str to search for.
default (any): Default value if the option is not found.
Returns:
str: Returns the value of the option in the specified section.
"""
section = self.domain_section(domain)

if not self.config.has_option(section, option):
return default

return self.config.get(section, option)

def domain_getboolean(self, domain, option, default=None):
"""
Lookup boolean option in domain.
Args:
domain (str): The SSSD domain name.
option (str): The option str to search for.
default (any): Default value if the option is not found.
Returns:
bool: Returns boolean form based on the data from get.
"""
section = self.domain_section(domain)

if not self.config.has_option(section, option):
return default

return self.config.getboolean(section, option)
Loading

0 comments on commit 66c6081

Please sign in to comment.