import os import logging from typing import Dict, Any, List, Optional, Union import importlib from passlib.context import CryptContext from connectors.connector_db_json import DatabaseConnector from modules.configuration import APP_CONFIG logger = logging.getLogger(__name__) # Password-Hashing pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") class GatewayInterface: """ Interface to the Gateway system. Manages users and mandates. """ def __init__(self, mandate_id: int = None, user_id: int = None): """ Initializes the Gateway Interface with optional mandate and user context. Args: mandate_id: ID of the current mandate (optional) user_id: ID of the current user (optional) """ # Context can be empty during initialization self.mandate_id = mandate_id self.user_id = user_id # Import data model module try: self.model_module = importlib.import_module("modules.gateway_model") logger.info("gateway_model successfully imported") except ImportError as e: logger.error(f"Error importing gateway_model: {e}") raise # Initialize database self._initialize_database() def _initialize_database(self): """ Initializes the database with minimal objects """ self.db = DatabaseConnector( db_host=APP_CONFIG.get("DB_SYSTEM_HOST"), db_database=APP_CONFIG.get("DB_SYSTEM_DATABASE"), db_user=APP_CONFIG.get("DB_SYSTEM_USER"), db_password=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"), mandate_id=self.mandate_id if self.mandate_id else 0, user_id=self.user_id if self.user_id else 0 ) # Create Root mandate if needed existing_mandate_id = self.get_initial_id("mandates") mandates = self.db.get_recordset("mandates") if existing_mandate_id is None or not mandates: logger.info("Creating Root mandate") root_mandate = { "name": "Root", "language": "de" } created_mandate = self.db.record_create("mandates", root_mandate) logger.info(f"Root mandate created with ID {created_mandate['id']}") # Update mandate context self.mandate_id = created_mandate['id'] self.user_id = created_mandate['user_id'] # Recreate connector with correct context self.db = DatabaseConnector( db_host=APP_CONFIG.get("DB_SYSTEM_HOST"), db_database=APP_CONFIG.get("DB_SYSTEM_DATABASE"), db_user=APP_CONFIG.get("DB_SYSTEM_USER"), db_password=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"), mandate_id=self.mandate_id, user_id=self.user_id ) # Create Admin user if needed existing_user_id = self.get_initial_id("users") users = self.db.get_recordset("users") if existing_user_id is None or not users: logger.info("Creating Admin user") admin_user = { "mandate_id": self.mandate_id, "username": "admin", "email": "admin@example.com", "full_name": "Administrator", "disabled": False, "language": "de", "privilege": "sysadmin", # SysAdmin privilege "hashed_password": self._get_password_hash("admin") # Use a secure password in production! } created_user = self.db.record_create("users", admin_user) logger.info(f"Admin user created with ID {created_user['id']}") # Update user context self.user_id = created_user['id'] # Recreate connector with correct context self.db = DatabaseConnector( db_host=APP_CONFIG.get("DB_SYSTEM_HOST"), db_database=APP_CONFIG.get("DB_SYSTEM_DATABASE"), db_user=APP_CONFIG.get("DB_SYSTEM_USER"), db_password=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"), mandate_id=self.mandate_id, user_id=self.user_id ) def get_initial_id(self, table: str) -> Optional[int]: """Returns the initial ID for a table""" return self.db.get_initial_id(table) def _get_password_hash(self, password: str) -> str: """Creates a hash for a password""" return pwd_context.hash(password) def _verify_password(self, plain_password: str, hashed_password: str) -> bool: """Checks if the password matches the hash""" return pwd_context.verify(plain_password, hashed_password) def _get_current_timestamp(self) -> str: """Returns the current timestamp in ISO format""" from datetime import datetime return datetime.now().isoformat() # Mandate methods def get_all_mandates(self) -> List[Dict[str, Any]]: """Returns all mandates""" return self.db.get_recordset("mandates") def get_mandate(self, mandate_id: int) -> Optional[Dict[str, Any]]: """Returns a mandate by its ID""" mandates = self.db.get_recordset("mandates", record_filter={"id": mandate_id}) if mandates: return mandates[0] return None def create_mandate(self, name: str, language: str = "de") -> Dict[str, Any]: """Creates a new mandate""" mandate_data = { "name": name, "language": language } return self.db.record_create("mandates", mandate_data) def update_mandate(self, mandate_id: int, mandate_data: Dict[str, Any]) -> Dict[str, Any]: """ Updates an existing mandate Args: mandate_id: The ID of the mandate to update mandate_data: The mandate data to update Returns: Dict[str, Any]: The updated mandate data Raises: ValueError: If the mandate is not found """ # Check if the mandate exists mandate = self.get_mandate(mandate_id) if not mandate: raise ValueError(f"Mandate with ID {mandate_id} not found") # Update the mandate updated_mandate = self.db.record_modify("mandates", mandate_id, mandate_data) return updated_mandate def delete_mandate(self, mandate_id: int) -> bool: """ Deletes a mandate and all associated users and data Args: mandate_id: The ID of the mandate to delete Returns: bool: True if the mandate was successfully deleted, otherwise False """ # Check if the mandate exists mandate = self.get_mandate(mandate_id) if not mandate: return False # Check if it's the initial mandate initial_mandate_id = self.get_initial_id("mandates") if initial_mandate_id is not None and mandate_id == initial_mandate_id: logger.warning(f"Attempt to delete the Root mandate was prevented") return False # Find all users of the mandate users = self.get_users_by_mandate(mandate_id) # Delete all users of the mandate and their associated data for user in users: self.delete_user(user["id"]) # Delete the mandate success = self.db.record_delete("mandates", mandate_id) if success: logger.info(f"Mandate with ID {mandate_id} was successfully deleted") else: logger.error(f"Error deleting mandate with ID {mandate_id}") return success # User methods def get_all_users(self) -> List[Dict[str, Any]]: """Returns all users""" users = self.db.get_recordset("users") # Remove password hashes from the response for user in users: if "hashed_password" in user: del user["hashed_password"] return users def get_users_by_mandate(self, mandate_id: int) -> List[Dict[str, Any]]: """ Returns all users of a specific mandate Args: mandate_id: The ID of the mandate Returns: List[Dict[str, Any]]: List of users in the mandate """ users = self.db.get_recordset("users", record_filter={"mandate_id": mandate_id}) # Remove password hashes from the response for user in users: if "hashed_password" in user: del user["hashed_password"] return users def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]: """Returns a user by username""" users = self.db.get_recordset("users") for user in users: if user.get("username") == username: return user return None def get_user(self, user_id: int) -> Optional[Dict[str, Any]]: """Returns a user by ID""" users = self.db.get_recordset("users", record_filter={"id": user_id}) if users: user = users[0] # Remove password hash from the API response if "hashed_password" in user: user_copy = user.copy() del user_copy["hashed_password"] return user_copy return user return None def create_user(self, username: str, password: str, email: str = None, full_name: str = None, language: str = "de", mandate_id: int = None, disabled: bool = False, privilege: str = "user") -> Dict[str, Any]: """ Creates a new user Args: username: The username password: The password email: The email address (optional) full_name: The full name (optional) language: The preferred language (default: "de") mandate_id: The ID of the mandate (optional) disabled: Whether the user is disabled (default: False) privilege: The privilege level (default: "user") Returns: Dict[str, Any]: The created user data Raises: ValueError: If the username already exists """ # Check if the username already exists existing_user = self.get_user_by_username(username) if existing_user: raise ValueError(f"User '{username}' already exists") # Use the provided mandate_id or the current context user_mandate_id = mandate_id if mandate_id is not None else self.mandate_id user_data = { "mandate_id": user_mandate_id, "username": username, "email": email, "full_name": full_name, "disabled": disabled, "language": language, "privilege": privilege, "hashed_password": self._get_password_hash(password) } created_user = self.db.record_create("users", user_data) # Remove password hash from the response if "hashed_password" in created_user: del created_user["hashed_password"] return created_user def authenticate_user(self, username: str, password: str) -> Optional[Dict[str, Any]]: """ Authenticates a user by username and password Args: username: The username password: The password Returns: Optional[Dict[str, Any]]: The user data or None if authentication fails """ user = self.get_user_by_username(username) if not user: return None if not self._verify_password(password, user.get("hashed_password", "")): return None # Check if the user is disabled if user.get("disabled", False): return None # Create a copy without password hash authenticated_user = {**user} if "hashed_password" in authenticated_user: del authenticated_user["hashed_password"] return authenticated_user def update_user(self, user_id: int, user_data: Dict[str, Any]) -> Dict[str, Any]: """ Updates a user Args: user_id: The ID of the user to update user_data: The user data to update Returns: Dict[str, Any]: The updated user data Raises: ValueError: If the user is not found """ # Get the current user with password hash (directly from DB) users = self.db.get_recordset("users", record_filter={"id": user_id}) if not users: raise ValueError(f"User with ID {user_id} not found") user = users[0] # If the password is being changed, hash it if "password" in user_data: user_data["hashed_password"] = self._get_password_hash(user_data["password"]) del user_data["password"] # Update the user updated_user = self.db.record_modify("users", user_id, user_data) # Remove password hash from the response if "hashed_password" in updated_user: del updated_user["hashed_password"] return updated_user def disable_user(self, user_id: int) -> Dict[str, Any]: """Disables a user""" return self.update_user(user_id, {"disabled": True}) def enable_user(self, user_id: int) -> Dict[str, Any]: """Enables a user""" return self.update_user(user_id, {"disabled": False}) def _delete_user_referenced_data(self, user_id: int) -> None: """ Deletes all data associated with a user Args: user_id: The ID of the user """ # Here all tables are searched and all entries referencing this user are deleted # Delete user attributes try: attributes = self.db.get_recordset("attributes", record_filter={"user_id": user_id}) for attribute in attributes: self.db.record_delete("attributes", attribute["id"]) except Exception as e: logger.error(f"Error deleting attributes for user {user_id}: {e}") # Other tables that might reference the user # (Depending on the application's database structure) logger.info(f"All referenced data for user {user_id} has been deleted") def delete_user(self, user_id: int) -> bool: """ Deletes a user and all associated data Args: user_id: The ID of the user to delete Returns: bool: True if the user was successfully deleted, otherwise False """ # Check if the user exists users = self.db.get_recordset("users", record_filter={"id": user_id}) if not users: return False # Check if it's the initial user initial_user_id = self.get_initial_id("users") if initial_user_id is not None and user_id == initial_user_id: logger.warning("Attempt to delete the Root Admin was prevented") return False # Delete all data associated with the user self._delete_user_referenced_data(user_id) # Delete the user success = self.db.record_delete("users", user_id) if success: logger.info(f"User with ID {user_id} was successfully deleted") else: logger.error(f"Error deleting user with ID {user_id}") return success # Singleton Factory for GatewayInterface instances per context _gateway_interfaces = {} def get_gateway_interface(mandate_id: int = None, user_id: int = None) -> GatewayInterface: """ Returns a GatewayInterface instance for the specified context. Reuses existing instances. """ context_key = f"{mandate_id}_{user_id}" if context_key not in _gateway_interfaces: _gateway_interfaces[context_key] = GatewayInterface(mandate_id, user_id) return _gateway_interfaces[context_key] # Init get_gateway_interface()