"""
This module defines the ``User`` and ``IPAUser`` classes, which are designed to
represent and manage system and FreeIPA users within the SCAutolib framework.
These classes encapsulate user properties
like username and password, and implement methods for common user management
operations such as adding and deleting users from either the local system
or a specified IPA server.
"""
import json
import pwd
import python_freeipa
from pathlib import Path, PosixPath
from SCAutolib import run, logger, LIB_DUMP_USERS
from SCAutolib.exceptions import SCAutolibException
from SCAutolib.models.CA import IPAServerCA
from SCAutolib.enums import UserType
[docs]class User:
"""
Represents a general system user, typically a local user account on the
machine where SCAutolib is running.
It holds user properties like username and password, and provides methods
to manage the user's presence on the local system.
User objects can be serialized to and loaded from JSON dump files for
persistence across SCAutolib runs.
"""
username: str = None
password: str = None
dump_file: Path = None
user_type: str = None
def __init__(self, username, password):
"""
Initializes a ``User`` object for a local system user.
:param username: The username for the system user.
:type username: str
:param password: The password for the system user.
:type password: str
:return: None
:rtype: None
"""
self.username = username
self.password = password
self.user_type = UserType.local
self.dump_file = LIB_DUMP_USERS.joinpath(f"{self.username}.json")
[docs] def to_dict(self):
"""
Converts the ``User`` object's attributes into a dictionary suitable
for JSON serialization.
:return: A dictionary representation of the user object's attributes.
:rtype: dict
"""
# Retype patlib.Path object to str
d = {k: str(v) if type(v) in (PosixPath, Path) else v
for k, v in self.__dict__.items()}
return d
[docs] @staticmethod
def load(json_file, **kwargs):
"""
Loads user data from a specified JSON file and reconstructs the
corresponding ``User`` or ``IPAUser`` object.
It determines the correct class to instantiate based on the
``user_type`` field in the JSON content.
:param json_file: The ``pathlib.Path`` object pointing to the JSON file
from which to read the user's data.
:type json_file: pathlib.Path
:param kwargs: Additional keyword arguments that might be necessary to
initialize the user object, particularly for ``IPAUser``
which requires an ``ipa_server`` object.
:type kwargs: dict
:return: An initialized ``User`` or ``IPAUser`` object loaded with data
from the JSON file.
:rtype: SCAutolib.models.user.User or SCAutolib.models.user.IPAUser
:raises SCAutolibException: If an unknown user type is encountered in
the JSON data, or if ``ipa_server`` is not
provided for an IPA user.
"""
with json_file.open("r") as f:
cnt = json.load(f)
if cnt["user_type"] == UserType.local:
user = User(username=cnt["username"],
password=cnt["password"])
elif cnt["user_type"] == UserType.ipa:
if "ipa_server" not in kwargs:
raise SCAutolibException("IPA Server object was not provided. "
"Can't load IPA user.")
user = IPAUser(ipa_server=kwargs["ipa_server"],
username=cnt["username"],
password=cnt["password"])
else:
raise SCAutolibException(f"Unknown user type: {cnt['user_type']}")
logger.debug(f"User {user.__class__} is loaded: {user.__dict__}")
return user
[docs] def add_user(self):
"""
Adds the user to the local system using the ``useradd`` system
management command and sets their password via ``passwd --stdin``.
It checks if the user already exists to prevent collisions.
:return: None
:rtype: None
:raises SCAutolibException: If the user already exists on the system.
"""
try:
pwd.getpwnam(self.username)
msg = f"User {self.username} already exists on this " \
f"machine. Username should be unique to avoid " \
f"future problems with collisions"
logger.critical(msg)
raise SCAutolibException(msg)
except KeyError:
logger.debug(f"Creating new user {self.username}")
cmd = ['useradd', '-m', self.username]
run(cmd, check=True)
cmd = ["passwd", self.username, "--stdin"]
run(cmd, input=self.password)
logger.info(f"User {self.username} was added to the system")
[docs] def delete_user(self):
"""
Deletes the local user from the system using the ``userdel -f``
command.
It also removes the corresponding JSON dump file for the user.
:return: None
:rtype: None
"""
try:
pwd.getpwnam(self.username)
logger.info(f"Deleting the user {self.username}")
run(['userdel', '-f', self.username], check=True)
except KeyError:
logger.info(f"User {self.username} is not present on the system")
if self.dump_file.exists():
self.dump_file.unlink()
logger.debug(f"Removed {self.dump_file} dump file")
[docs]class IPAUser(User):
"""
Represents an IPA (Identity Management for Linux) user.
This class extends the base ``User`` class to include specific
functionalities for managing users within an IPA server environment,
primarily through the ``python_freeipa`` library.
"""
default_password = "redhat"
def __init__(self, ipa_server: IPAServerCA, *args, **kwargs):
"""
Initializes an ``IPAUser`` object.
IPA client should be configured first before creating an IPA user
through this class.
It requires an ``IPAServerCA`` object to facilitate communication with
the IPA server and inherits user attributes from the base ``User``
class.
:param ipa_server: An ``IPAServerCA`` object that provides the
necessary IPA server hostname and ``ClientMeta``
object for interaction.
:type ipa_server: SCAutolib.models.CA.IPAServerCA
:param username: The username for the system user.
:type username: str
:param password: The password for the system user.
:type password: str
:return: None
:rtype: None
"""
super().__init__(*args, **kwargs)
self.user_type = UserType.ipa
self._meta_client = ipa_server.meta_client
self._ipa_hostname = ipa_server.ipa_server_hostname
[docs] def to_dict(self):
"""
Converts the ``IPAUser`` object's attributes into a dictionary for
JSON serialization. It calls the base ``User.to_dict()``
method and then removes internal ``_meta_client`` and ``_ipa_hostname``
attributes, which are not directly serializable.
:return: A dictionary representation of the IPA user object's
attributes.
:rtype: dict
"""
d = super().to_dict()
d.pop("_meta_client")
d.pop("_ipa_hostname")
return d
[docs] def add_user(self):
"""
Adds the IPA user to the IPA server using the ``python_freeipa`` client.
It sets a default password and then changes it to the specified
password to avoid requiring a password change on first login.
:return: None
:rtype: None
:raises SCAutolibException: If the user already exists on the IPA
server.
"""
try:
r = self._meta_client.user_add(self.username, self.username,
self.username, self.username,
o_userpassword=self.default_password)
logger.debug(r)
# To avoid forcing IPA server to change the password on first login
# we changing it through the client
client = python_freeipa.client.Client(self._ipa_hostname,
verify_ssl=False)
client.change_password(self.username, self.password,
self.default_password)
logger.info(f"User {self.username} is added to the IPA server")
except python_freeipa.exceptions.DuplicateEntry:
msg = f"User {self.username} already exists on the " \
f"IPA server. Username should be unique to avoid " \
f"future problems with collisions"
logger.critical(msg)
raise SCAutolibException(msg)
[docs] def delete_user(self):
"""
Deletes the IPA user from the IPA server using the ``python_freeipa``
client. If the user is not found on the server,
the operation is silently ignored.
:return: None
:rtype: None
"""
try:
r = self._meta_client.user_del(self.username)["result"]
logger.info(f"User {self.username} is removed from the IPA server")
logger.debug(r)
except python_freeipa.exceptions.NotFound:
pass