from pathlib import Path import shutil from typing import List, Optional from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") token: str = Field(alias="TOKEN") server_name: str = Field(default="user-manage-api", alias="SERVER_NAME") home_base_dir: str = Field(default="/home", alias="HOME_BASE_DIR") link_home_dir: str = Field(default="", alias="LINK_HOME_DIR") whitelist_users: str = Field(default="", alias="WHITELIST_USERS") whitelist_groups: str = Field(default="", alias="WHITELIST_GROUPS") locked_users: str = Field(default="", alias="LOCKED_USERS") hidden_users: str = Field(default="", alias="HIDDEN_USERS") hidden_groups: str = Field(default="", alias="HIDDEN_GROUPS") user_uid_min: Optional[int] = Field(default=None, alias="USER_UID_MIN") user_uid_max: Optional[int] = Field(default=None, alias="USER_UID_MAX") group_gid_min: Optional[int] = Field(default=None, alias="GROUP_GID_MIN") group_gid_max: Optional[int] = Field(default=None, alias="GROUP_GID_MAX") use_libuser: bool = Field(default=False, alias="USE_LIBUSER") log_level: str = Field(default="INFO", alias="LOG_LEVEL") log_path: str = Field(default="./logs/user_manage_api.log", alias="LOG_PATH") sudo_path: str = Field(default="/usr/bin/sudo", alias="SUDO_PATH") command_timeout_seconds: int = 10 @field_validator("user_uid_min", "user_uid_max", "group_gid_min", "group_gid_max", mode="before") @classmethod def empty_string_to_none(cls, value: object) -> object: if value == "": return None return value @property def whitelist_user_list(self) -> List[str]: return self._parse_comma_separated_list(self.whitelist_users) @property def whitelist_group_list(self) -> List[str]: return self._parse_comma_separated_list(self.whitelist_groups) @property def locked_user_list(self) -> List[str]: return self._parse_comma_separated_list(self.locked_users) @property def hidden_user_list(self) -> List[str]: return self._parse_comma_separated_list(self.hidden_users) @property def hidden_group_list(self) -> List[str]: return self._parse_comma_separated_list(self.hidden_groups) def _parse_comma_separated_list(self, value: str) -> List[str]: return [item.strip() for item in value.split(",") if item.strip()] def validate_settings(settings: Settings) -> None: if not settings.token.strip(): raise ValueError("TOKEN is required and cannot be empty.") log_parent = Path(settings.log_path).parent log_parent.mkdir(parents=True, exist_ok=True) if not log_parent.exists() or not log_parent.is_dir(): raise ValueError(f"LOG_PATH parent is invalid: {log_parent}") if shutil.which(settings.sudo_path) is None and not Path(settings.sudo_path).exists(): raise ValueError(f"SUDO_PATH is not executable: {settings.sudo_path}") if settings.user_uid_min is not None and settings.user_uid_max is not None and settings.user_uid_min > settings.user_uid_max: raise ValueError("USER_UID_MIN cannot be greater than USER_UID_MAX.") if settings.group_gid_min is not None and settings.group_gid_max is not None and settings.group_gid_min > settings.group_gid_max: raise ValueError("GROUP_GID_MIN cannot be greater than GROUP_GID_MAX.") required_commands = ["useradd", "usermod", "userdel", "groupadd", "groupdel", "chpasswd", "id", "getent"] if settings.link_home_dir.strip(): required_commands.extend(["mkdir", "ln", "chown", "unlink"]) for command in required_commands: if shutil.which(command) is None: raise ValueError(f"Required command not found in PATH: {command}")