Source code for SCAutolib.utils

"""
This module provides a collection of utility and helper functions utilized
across the SCAutolib library. These functions are
specifically designed to support various internal demands of the library,
including system checks, package management, key/certificate handling,
and data serialization. They are not intended as
general-purpose utilities but as specialized aids tailored to SCAutolib's
operations.
"""


import json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from pathlib import Path

from SCAutolib import (run, logger, TEMPLATES_DIR, LIB_DUMP_USERS, LIB_DUMP_CAS,
                       LIB_DUMP_CARDS)
from SCAutolib.exceptions import SCAutolibException
from SCAutolib.models.CA import LocalCA, BaseCA, CustomCA, IPAServerCA
from SCAutolib.models.card import Card
from SCAutolib.models.file import OpensslCnf, SSSDConf
from SCAutolib.models.user import User


def _check_selinux():
    """
    Checks if a specific SELinux module, 'virtcacard' (for virtual smart cards),
    is currently installed and active on the system.
    If the module is not found, this function attempts to install it from
    a predefined template file (``virtcacard.cil``) and then restarts the
    ``pcscd`` service.

    :return: None
    """
    result = run("semodule -l", print_=False)
    if "virtcacard" not in result.stdout:
        logger.debug(
            "SELinux module for virtual smart cards is not present in the "
            "system. Installing...")

        run(["semodule", "-i", f"{TEMPLATES_DIR}/virtcacard.cil"])

        run(["systemctl", "restart", "pcscd"])
        logger.debug("pcscd service is restarted")

    logger.debug("SELinux module for virtual smart cards is installed")


def _gen_private_key(key_path: Path, size: int = 2048):
    """
    Generates an RSA private key and saves it to the specified file path in PEM
    format without encryption.
    This function is used when a private key is needed for a user's smart card
    or certificate request and doesn't already exist.

    :param key_path: The ``pathlib.Path`` object specifying the full path
                     (including filename) where the generated private key
                     should be saved.
    :type key_path: pathlib.Path
    :param size: The size of the key in bits to be created.
    :type size: int
    :return: None
    """
    # CAC specification do not specify key size specifies key size
    # up to 2048 bits, so keys greater than 2048 bits is not supported
    key = rsa.generate_private_key(public_exponent=65537, key_size=size)

    with key_path.open("wb") as f:
        f.write(key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.TraditionalOpenSSL,
            encryption_algorithm=serialization.NoEncryption()))


def _install_packages(packages):
    """
    Installs a list of specified RPM packages on the system.
    After installation, it logs the installed version of each package for
    verification.

    :param packages: A list of strings, where each string is the name of a
                     package to be installed (e.g., ``["opensc", "sssd"]``).
    :type packages: list
    :return: None
    """
    run(f"dnf install -y {' '.join(packages)}")
    for pkg in packages:
        pkg = run(["rpm", "-q", pkg]).stdout
        logger.debug(f"Package {pkg} is installed")


def _check_packages(packages):
    """
    Identifies and returns a list of packages that are required for SCAutolib
    but are not currently installed on the system.
    It uses ``rpm -q`` to query each package's installation status.

    :param packages: A list of strings, where each string is the name of a
                     package to check for.
    :type packages: list
    :return: A list of strings, containing the names of packages that were
             found to be missing on the system.
    :rtype: list
    """
    missing = []
    for pkg in packages:
        # Return code 1 means the package is not installed
        out = run(["rpm", "-q", pkg], return_code=[0, 1])
        if out.returncode == 1:
            logger.warning(f"Package {pkg} is required for the testing, "
                           f"but is not present in the system")
            missing.append(pkg)
        else:
            logger.debug(f"Package {out.stdout.strip()} is present")
    return missing


[docs]def dump_to_json(obj): """ Serializes a given object into a JSON file, using the object's ``to_dict()`` method for serialization and its ``dump_file`` attribute to determine the output path. This is used to persist the state of SCAutolib's internal objects (like users, CAs, and cards) across different runs. :param obj: The object to be serialized. It must have a ``to_dict()`` method and a ``dump_file`` attribute. :type obj: object :return: None """ with obj.dump_file.open("w") as f: json.dump(obj.to_dict(), f) logger.debug(f"Object {type(obj)} is stored to the {obj.dump_file} file")
[docs]def load_user(username, **kwargs): """ Loads a ``User`` object from a JSON dump file corresponding to the given username. The file is expected to be located in ``LIB_DUMP_USERS`` directory. :param username: The username of the user to load. :type username: str :param kwargs: Additional keyword arguments that might be required by the ``User.load`` static method, particularly for ``IPAUser`` objects (e.g., ``ipa_server`` object). :type kwargs: dict :return: The loaded ``User`` object (either ``User`` or ``IPAUser`` instance). :rtype: SCAutolib.models.user.User :raises SCAutolibException: If the user's JSON dump file does not exist. """ user_file = LIB_DUMP_USERS.joinpath(f"{username}.json") logger.debug(f"Loading user {username} from {user_file}") user = None if user_file.exists(): user = User.load(user_file, **kwargs) else: raise SCAutolibException(f"{user_file} does not exist") return user
[docs]def load_token(card_name: str = None, update_sssd: bool = False): """ Loads a ``Card`` object from a JSON dump file based on the provided card name. This function is primarily intended for use in pytest configurations to set up card objects for tests. Optionally, it can update the SSSD configuration file (``sssd.conf``) with a ``shadowutils`` rule for the user of the loaded card. :param card_name: The name of the card object to load. :type card_name: str, optional :param update_sssd: If ``True``, the SSSD configuration file will be updated with a ``shadowutils`` rule for the user of the loaded card. :return: The loaded ``Card`` object :rtype: SCAutolib.models.card.Card """ card_file = LIB_DUMP_CARDS.joinpath(f"{card_name}.json") logger.debug(f"Loading card {card_name} from {card_file}") card = None if card_file.exists(): card = Card.load(card_file) if update_sssd: sssd_conf = SSSDConf() sssd_conf.set(section=f"certmap/shadowutils/{card.cardholder}", key="matchrule", value=f"<SUBJECT>.*CN={card.CN}.*") sssd_conf.save() run(["sss_cache", "-E"]) run(["systemctl", "restart", "sssd"]) return card
[docs]def ipa_factory(): """ Creates and returns an ``IPAServerCA`` object. This function loads the IPA server CA configuration from its JSON dump file. It specifically asserts that the loaded CA is an instance of ``IPAServerCA``. .. note: Creating new IPA server with CA is not supported. :return: An initialized ``IPAServerCA`` object. :rtype: SCAutolib.models.CA.IPAServerCA :raises SCAutolibException: If the IPA server CA dump file is not found or if the loaded object is not a valid ``IPAServerCA`` instance. """ json_file = LIB_DUMP_CAS.joinpath("ipa-server.json") if not json_file.exists(): msg = "Dump file for ipa server CA is not present." logger.error(msg) logger.error("The reason for this is most likely that the system was " "not configured for IPA client via SCAutolib") raise SCAutolibException(msg) ca = BaseCA.load(json_file) if not isinstance(ca, IPAServerCA): msg = "Values in dump file are not valid for IPA server, so the " \ "object can't be created" logger.error(msg) raise SCAutolibException(msg) return ca
[docs]def ca_factory(path: Path = None, cnf: OpensslCnf = None, card_data: dict = None, ca_name: str = None, create: bool = False): """ A factory function to create or load Certificate Authority (CA) objects based on the provided parameters. It can initialize a new CA instance or load an existing one from a JSON dump file. :param path: The ``pathlib.Path`` object to the CA's root directory. This is used when creating a new ``LocalCA`` instance. :type path: pathlib.Path, optional :param cnf: An ``OpensslCnf`` object representing the OpenSSL configuration file for the CA. Used when creating a new ``LocalCA``. :type cnf: SCAutolib.models.file.OpensslCnf, optional :param card_data: A dictionary containing various attributes of the card (e.g., PIN, cardholder, slot). This data is used when creating a new ``CustomCA`` for physical cards. :type card_data: dict, optional :param ca_name: The name of the CA to load. This parameter is used when ``create`` is ``False`` to identify the specific CA JSON dump file. :type ca_name: str, optional :param create: If ``True``, a new CA object will be created (either ``LocalCA`` or ``CustomCA``). If ``False``, an existing CA object will be loaded from a dump file. :type create: bool :return: An initialized CA object (either ``LocalCA``, ``CustomCA``, or ``IPAServerCA`` instance). :rtype: SCAutolib.models.CA.BaseCA """ if not create: ca = BaseCA.load(LIB_DUMP_CAS.joinpath(f"{ca_name}.json")) return ca if not path: # create CA for physical card ca = CustomCA(card_data) return ca else: # create new CA object for virtual card ca = LocalCA(root_dir=path, cnf=cnf) return ca