gateway/modules/interfaces/serviceAppClass.py
ValueOn AG cf94b1115b ref
2025-05-26 07:04:30 +02:00

586 lines
23 KiB
Python

"""
Interface to the Gateway system.
Manages users and mandates for authentication.
"""
from datetime import datetime, timedelta
import os
import logging
from typing import Dict, Any, List, Optional, Union
import importlib
import json
from passlib.context import CryptContext
from modules.connectors.connectorDbJson import DatabaseConnector
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppAccess import AppAccess
from modules.interfaces.serviceAppModel import (
User, Mandate, UserInDB, UserConnection,
Session, AuthEvent, AuthAuthority, UserPrivilege,
ConnectionStatus
)
logger = logging.getLogger(__name__)
# Singleton factory for GatewayInterface instances per context
_gatewayInterfaces = {}
# Root interface instance
_rootGatewayInterface = None
# Password-Hashing
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
class GatewayInterface:
"""
Interface to the Gateway system.
Manages users and mandates.
"""
def __init__(self, currentUser: Dict[str, Any] = None):
"""Initializes the Gateway Interface."""
# Initialize variables
self.currentUser = currentUser
self.userId = currentUser.get("id") if currentUser else None
self.mandateId = currentUser.get("mandateId") if currentUser else None
self.access = None # Will be set when user context is provided
# Initialize database
self._initializeDatabase()
# Initialize standard records if needed
self._initRecords()
# Set user context if provided
if currentUser:
self.setUserContext(currentUser)
def setUserContext(self, currentUser: Dict[str, Any]):
"""Sets the user context for the interface."""
if not currentUser:
logger.info("Initializing interface without user context")
return
self.currentUser = currentUser
self.userId = currentUser.get("id")
self.mandateId = currentUser.get("mandateId")
if not self.userId or not self.mandateId:
raise ValueError("Invalid user context: id and mandateId are required")
# Add language settings
self.userLanguage = currentUser.get("language", "en") # Default user language
# Initialize access control with user context
self.access = AppAccess(self.currentUser, self.db)
logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
def _initializeDatabase(self):
"""Initializes the database connection."""
try:
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_APP_HOST", "_no_config_default_data")
dbDatabase = APP_CONFIG.get("DB_APP_DATABASE", "app")
dbUser = APP_CONFIG.get("DB_APP_USER")
dbPassword = APP_CONFIG.get("DB_APP_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword
)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def _initRecords(self):
"""Initialize standard records if they don't exist."""
self._initRootMandate()
self._initAdminUser()
def _initRootMandate(self):
"""Creates the Root mandate if it doesn't exist."""
existingMandateId = self.getInitialId("mandates")
mandates = self.db.getRecordset("mandates")
if existingMandateId is None or not mandates:
logger.info("Creating Root mandate")
rootMandate = Mandate(
name="Root",
language="en"
)
createdMandate = self.db.recordCreate("mandates", rootMandate.model_dump())
logger.info(f"Root mandate created with ID {createdMandate['id']}")
# Register the initial ID
self.db._registerInitialId("mandates", createdMandate['id'])
# Update mandate context
self.mandateId = createdMandate['id']
def _initAdminUser(self):
"""Creates the Admin user if it doesn't exist."""
existingUserId = self.getInitialId("users")
users = self.db.getRecordset("users")
if existingUserId is None or not users:
logger.info("Creating Admin user")
adminUser = UserInDB(
mandateId=self.getInitialId("mandates"),
username="admin",
email="admin@example.com",
fullName="Administrator",
disabled=False,
language="en",
privilege=UserPrivilege.SYSADMIN,
authenticationAuthority=AuthAuthority.LOCAL,
hashedPassword=self._getPasswordHash("The 1st Poweron Admin"), # Use a secure password in production!
connections=[]
)
createdUser = self.db.recordCreate("users", adminUser.model_dump())
logger.info(f"Admin user created with ID {createdUser['id']}")
# Register the initial ID
self.db._registerInitialId("users", createdUser['id'])
# Update user context
self.currentUser = createdUser
self.userId = createdUser.get("id")
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Unified user access management function that filters data based on user privileges
and adds access control attributes.
Args:
table: Name of the table
recordset: Recordset to filter based on access rules
Returns:
Filtered recordset with access control attributes
"""
return self.access.uam(table, recordset)
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
Args:
table: Name of the table
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
return self.access.canModify(table, recordId)
def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
return self.db.getInitialId(table)
def _getPasswordHash(self, password: str) -> str:
"""Creates a hash for a password."""
return pwdContext.hash(password)
def _verifyPassword(self, plainPassword: str, hashedPassword: str) -> bool:
"""Checks if the password matches the hash."""
return pwdContext.verify(plainPassword, hashedPassword)
# User methods
def getAllUsers(self) -> List[User]:
"""Returns users based on user access level."""
allUsers = self.db.getRecordset("users")
filteredUsers = self._uam("users", allUsers)
# Convert to User models
return [User.from_dict(user) for user in filteredUsers]
def getUsersByMandate(self, mandateId: str) -> List[User]:
"""Returns users for a specific mandate if user has access."""
# Get users for this mandate
users = self.db.getRecordset("users", recordFilter={"mandateId": mandateId})
filteredUsers = self._uam("users", users)
# Convert to User models
return [User.from_dict(user) for user in filteredUsers]
def getUserByUsername(self, username: str) -> Optional[User]:
"""Returns a user by username."""
try:
# Get users table
users = self.db.getRecordset("users")
if not users:
return None
# Find user by username
for user in users:
if user.get("username") == username:
logger.info(f"Found user with username {username}")
return User.from_dict(user)
logger.info(f"No user found with username {username}")
return None
except Exception as e:
logger.error(f"Error getting user by username: {str(e)}")
return None
def getUser(self, userId: str) -> Optional[User]:
"""Returns a user by ID if user has access."""
users = self.db.getRecordset("users", recordFilter={"id": userId})
if not users:
return None
filteredUsers = self._uam("users", users)
if not filteredUsers:
return None
return User.from_dict(filteredUsers[0])
def addUserConnection(self, userId: str, authority: AuthAuthority, externalId: str,
externalUsername: str, externalEmail: Optional[str] = None) -> UserConnection:
"""Add a new connection to an external service for a user"""
try:
# Get user
user = self.getUser(userId)
if not user:
raise ValueError(f"User {userId} not found")
# Check if connection already exists
for conn in user.connections:
if conn.authority == authority and conn.externalId == externalId:
raise ValueError(f"Connection to {authority} already exists for user {userId}")
# Create new connection
connection = UserConnection(
authority=authority,
externalId=externalId,
externalUsername=externalUsername,
externalEmail=externalEmail,
status=ConnectionStatus.ACTIVE
)
# Add connection to user
user.connections.append(connection)
# Update user record
self.db.recordModify("users", userId, {"connections": [c.model_dump() for c in user.connections]})
return connection
except Exception as e:
logger.error(f"Error adding user connection: {str(e)}")
raise ValueError(f"Failed to add user connection: {str(e)}")
def removeUserConnection(self, userId: str, connectionId: str) -> None:
"""Remove a connection to an external service for a user"""
try:
# Get user
user = self.getUser(userId)
if not user:
raise ValueError(f"User {userId} not found")
# Find and remove connection
user.connections = [c for c in user.connections if c.id != connectionId]
# Update user record
self.db.recordModify("users", userId, {"connections": [c.model_dump() for c in user.connections]})
except Exception as e:
logger.error(f"Error removing user connection: {str(e)}")
raise ValueError(f"Failed to remove user connection: {str(e)}")
def authenticateLocalUser(self, username: str, password: str) -> Optional[User]:
"""Authenticates a user by username and password using local authentication."""
# Clear the users table from cache and reload it
if "users" in self.db._tablesCache:
del self.db._tablesCache["users"]
# Get user by username
user = self.getUserByUsername(username)
if not user:
raise ValueError("User not found")
# Check if the user is disabled
if user.disabled:
raise ValueError("User is disabled")
# Verify that the user has local authentication enabled
if user.authenticationAuthority != AuthAuthority.LOCAL:
raise ValueError("User does not have local authentication enabled")
# Get the full user record with password hash for verification
userWithPassword = UserInDB(**self.db.getRecordset("users", recordFilter={"id": user.id})[0])
if not self._verifyPassword(password, userWithPassword.hashedPassword):
raise ValueError("Invalid password")
return user
def createUser(self, username: str, password: str = None, email: str = None,
fullName: str = None, language: str = "en", disabled: bool = False,
privilege: UserPrivilege = UserPrivilege.USER,
authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL,
externalId: str = None, externalUsername: str = None,
externalEmail: str = None) -> User:
"""Create a new user with optional external connection"""
try:
# Create user data using UserInDB model
userData = UserInDB(
username=username,
email=email,
fullName=fullName,
language=language,
mandateId=self.mandateId,
disabled=disabled,
privilege=privilege,
authenticationAuthority=authenticationAuthority,
hashedPassword=self._getPasswordHash(password) if password else None,
connections=[]
)
# Create user record
createdRecord = self.db.recordCreate("users", userData.to_dict())
if not createdRecord or not createdRecord.get("id"):
raise ValueError("Failed to create user record")
# Add external connection if provided
if externalId and externalUsername:
self.addUserConnection(
createdRecord["id"],
authenticationAuthority,
externalId,
externalUsername,
externalEmail
)
# Get created user using the returned ID
createdUser = self.db.getRecordset("users", recordFilter={"id": createdRecord["id"]})
if not createdUser or len(createdUser) == 0:
raise ValueError("Failed to retrieve created user")
# Clear users table from cache
if hasattr(self.db, '_tablesCache') and "users" in self.db._tablesCache:
del self.db._tablesCache["users"]
return User.from_dict(createdUser[0])
except ValueError as e:
logger.error(f"Error creating user: {str(e)}")
raise
except Exception as e:
logger.error(f"Unexpected error creating user: {str(e)}")
raise ValueError(f"Failed to create user: {str(e)}")
def updateUser(self, userId: str, updateData: Dict[str, Any]) -> User:
"""Update a user's information"""
try:
# Get user
user = self.getUser(userId)
if not user:
raise ValueError(f"User {userId} not found")
# Update user data using model
updatedData = user.model_dump()
updatedData.update(updateData)
updatedUser = User.from_dict(updatedData)
# Update user record
self.db.recordModify("users", userId, updatedUser.to_dict())
# Get updated user
updatedUser = self.getUser(userId)
if not updatedUser:
raise ValueError("Failed to retrieve updated user")
return updatedUser
except Exception as e:
logger.error(f"Error updating user: {str(e)}")
raise ValueError(f"Failed to update user: {str(e)}")
def disableUser(self, userId: str) -> User:
"""Disables a user if current user has permission."""
return self.updateUser(userId, {"disabled": True})
def enableUser(self, userId: str) -> User:
"""Enables a user if current user has permission."""
return self.updateUser(userId, {"disabled": False})
def _deleteUserReferencedData(self, userId: str) -> None:
"""Deletes all data associated with a user."""
try:
# Delete user sessions
sessions = self.db.getRecordset("sessions", recordFilter={"userId": userId})
for session in sessions:
self.db.recordDelete("sessions", session["id"])
logger.debug(f"Deleted session {session['id']} for user {userId}")
# Delete user auth events
events = self.db.getRecordset("auth_events", recordFilter={"userId": userId})
for event in events:
self.db.recordDelete("auth_events", event["id"])
logger.debug(f"Deleted auth event {event['id']} for user {userId}")
# Delete user connections
user = self.getUser(userId)
if user and user.connections:
for conn in user.connections:
self.removeUserConnection(userId, conn.id)
logger.debug(f"Deleted connection {conn.id} for user {userId}")
logger.info(f"All referenced data for user {userId} has been deleted")
except Exception as e:
logger.error(f"Error deleting referenced data for user {userId}: {str(e)}")
raise
# Mandate methods
def getAllMandates(self) -> List[Mandate]:
"""Returns all mandates based on user access level."""
allMandates = self.db.getRecordset("mandates")
filteredMandates = self._uam("mandates", allMandates)
return [Mandate.from_dict(mandate) for mandate in filteredMandates]
def getMandate(self, mandateId: str) -> Optional[Mandate]:
"""Returns a mandate by ID if user has access."""
mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId})
if not mandates:
return None
filteredMandates = self._uam("mandates", mandates)
if not filteredMandates:
return None
return Mandate.from_dict(filteredMandates[0])
def createMandate(self, name: str, language: str = "en") -> Mandate:
"""Creates a new mandate if user has permission."""
if not self._canModify("mandates"):
raise PermissionError("No permission to create mandates")
# Create mandate data using model
mandateData = Mandate(
name=name,
language=language
)
# Create mandate record
createdRecord = self.db.recordCreate("mandates", mandateData.to_dict())
if not createdRecord or not createdRecord.get("id"):
raise ValueError("Failed to create mandate record")
return Mandate.from_dict(createdRecord)
def updateMandate(self, mandateId: str, updateData: Dict[str, Any]) -> Mandate:
"""Updates a mandate if user has access."""
try:
# Get mandate
mandate = self.getMandate(mandateId)
if not mandate:
raise ValueError(f"Mandate {mandateId} not found")
# Update mandate data using model
updatedData = mandate.model_dump()
updatedData.update(updateData)
updatedMandate = Mandate.from_dict(updatedData)
# Update mandate record
self.db.recordModify("mandates", mandateId, updatedMandate.to_dict())
# Get updated mandate
updatedMandate = self.getMandate(mandateId)
if not updatedMandate:
raise ValueError("Failed to retrieve updated mandate")
return updatedMandate
except Exception as e:
logger.error(f"Error updating mandate: {str(e)}")
raise ValueError(f"Failed to update mandate: {str(e)}")
def deleteMandate(self, mandateId: str) -> bool:
"""Deletes a mandate if user has access."""
try:
# Check if mandate exists and user has access
mandate = self.getMandate(mandateId)
if not mandate:
return False
if not self._canModify("mandates", mandateId):
raise PermissionError(f"No permission to delete mandate {mandateId}")
# Check if mandate has users
users = self.getUsersByMandate(mandateId)
if users:
raise ValueError(f"Cannot delete mandate {mandateId} with existing users")
# Delete mandate
return self.db.recordDelete("mandates", mandateId)
except Exception as e:
logger.error(f"Error deleting mandate: {str(e)}")
raise ValueError(f"Failed to delete mandate: {str(e)}")
# Public Methods
def getInterface(currentUser: Dict[str, Any]) -> GatewayInterface:
"""
Returns a GatewayInterface instance for the current user.
Handles initialization of database and records.
"""
mandateId = currentUser.get("mandateId")
userId = currentUser.get("id")
if not mandateId or not userId:
raise ValueError("Invalid user context: mandateId and id are required")
# Create context key
contextKey = f"{mandateId}_{userId}"
# Create new instance if not exists
if contextKey not in _gatewayInterfaces:
_gatewayInterfaces[contextKey] = GatewayInterface(currentUser)
return _gatewayInterfaces[contextKey]
def getRootUser() -> Dict[str, Any]:
"""
Returns the root user from the database.
This is the user with the initial ID in the users table.
"""
try:
readInterface = getInterface()
# Get the initial user ID
initialUserId = readInterface.db.getInitialId("users")
if not initialUserId:
raise ValueError("No initial user ID found in database")
# Get the user record
users = readInterface.db.getRecordset("users", recordFilter={"id": initialUserId})
if not users:
raise ValueError(f"Root user with ID {initialUserId} not found in database")
return users[0]
except Exception as e:
logger.error(f"Error getting root user: {str(e)}")
raise ValueError(f"Failed to get root user: {str(e)}")
def getRootInterface() -> GatewayInterface:
"""
Returns a GatewayInterface instance with root privileges.
This is used for initial setup and user creation.
"""
global _rootGatewayInterface
if _rootGatewayInterface is None:
rootUser = getRootUser()
_rootGatewayInterface = GatewayInterface(rootUser)
return _rootGatewayInterface