gateway/modules/routes/routeUsers.py
2025-05-19 01:14:51 +02:00

393 lines
15 KiB
Python

from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request
from typing import List, Dict, Any, Optional
from fastapi import status
from datetime import datetime
from dataclasses import dataclass
import logging
import time
import traceback
# Import auth module
from modules.security.auth import getCurrentActiveUser, getUserContext
# Import interfaces
from modules.interfaces.gatewayInterface import getGatewayInterface
from modules.interfaces.gatewayModel import User
# Set up logger
logger = logging.getLogger(__name__)
# Determine all attributes of the model
def getModelAttributes(modelClass):
return [attr for attr in dir(modelClass)
if not callable(getattr(modelClass, attr))
and not attr.startswith('_')
and attr not in ('metadata', 'query', 'query_class', 'label', 'field_labels')]
# Model attributes for User
userAttributes = getModelAttributes(User)
@dataclass
class AppContext:
"""Context object for all required connections and user information"""
_mandateId: int
_userId: int
interfaceData: Any # Gateway Interface
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""Creates a central context object with all required connections"""
_mandateId, _userId = await getUserContext(currentUser)
interfaceData = getGatewayInterface(_mandateId, _userId)
return AppContext(
_mandateId=_mandateId,
_userId=_userId,
interfaceData=interfaceData
)
# Create router for user endpoints
router = APIRouter(
prefix="/api/users",
tags=["Users"],
responses={404: {"description": "Not found"}}
)
@router.get("", response_model=List[Dict[str, Any]])
async def getUsers(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Get all available users (only for Admin/SysAdmin users)"""
context = await getContext(currentUser)
# Permission check
if currentUser.get("privilege") not in ["admin", "sysadmin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to access the user list"
)
# Admin sees only users of own mandate, SysAdmin sees all
if currentUser.get("privilege") == "admin":
return context.interfaceData.getUsersByMandate(context._mandateId)
else: # sysadmin
return context.interfaceData.getAllUsers()
@router.post("/register", response_model=Dict[str, Any])
async def registerUser(request: Request):
"""Register a new user."""
try:
# Get request data
userData = await request.json()
# Get root mandate and admin user IDs
adminGateway = getGatewayInterface()
rootMandateId = adminGateway.getInitialId("mandates")
adminUserId = adminGateway.getInitialId("users")
if not rootMandateId or not adminUserId:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="System is not properly initialized with root mandate and admin user"
)
# Create a new gateway interface instance with admin context
adminGateway = getGatewayInterface(rootMandateId, adminUserId)
# Set default values if not provided
if "language" not in userData:
userData["language"] = "en"
if "authenticationAuthority" not in userData:
userData["authenticationAuthority"] = "local"
# Validate authentication authority
if userData["authenticationAuthority"] not in ["local", "microsoft"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid authentication authority: {userData['authenticationAuthority']}"
)
# Validate password for local authentication
if userData["authenticationAuthority"] == "local":
if "password" not in userData:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password is required for local authentication"
)
# Create the user
try:
createdUser = adminGateway.createUser(
username=userData["username"],
password=userData.get("password"),
email=userData.get("email"),
fullName=userData.get("fullName"),
language=userData["language"],
_mandateId=userData.get("_mandateId", rootMandateId),
authenticationAuthority=userData["authenticationAuthority"]
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Verify the user was created
if not createdUser:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user"
)
# For local authentication, verify password was stored
if userData["authenticationAuthority"] == "local":
if "hashedPassword" not in createdUser:
logger.error("Password not stored in user record")
# Try to delete the user
try:
adminGateway.deleteUser(createdUser["id"])
logger.info("Successfully deleted user after password storage failure")
except Exception as e:
logger.error(f"Failed to delete user after password storage failure: {str(e)}")
raise HTTPException(status_code=500, detail="Password storage failed")
logger.info("User verification successful")
# Test authentication
try:
authResult = adminGateway.authenticateUser(userData["username"], userData["password"])
if not authResult:
logger.error("Authentication test failed after user creation")
# Try to delete the user
try:
adminGateway.deleteUser(createdUser["id"])
logger.info("Successfully deleted user after authentication test failure")
except Exception as e:
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
raise HTTPException(status_code=500, detail="Authentication test failed")
except ValueError as e:
logger.error(f"Authentication test failed: {str(e)}")
# Try to delete the user
try:
adminGateway.deleteUser(createdUser["id"])
logger.info("Successfully deleted user after authentication test failure")
except Exception as e:
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
raise HTTPException(status_code=500, detail=f"Authentication test failed: {str(e)}")
logger.info("Authentication test successful")
# Remove sensitive data from response
if "hashedPassword" in createdUser:
del createdUser["hashedPassword"]
return createdUser
except HTTPException:
raise
except Exception as e:
logger.error(f"Unexpected error during user registration: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Registration failed: {str(e)}"
)
@router.post("/register-with-msal", response_model=Dict[str, Any])
async def registerUserWithMsal(userData: dict = Body(...)):
"""Register a new user using Microsoft authentication"""
# Add debug logging
import logging
logger = logging.getLogger(__name__)
logger.info(f"MSAL Registration request data: {userData}")
# Get the initial IDs for mandate and admin user
adminGateway = getGatewayInterface()
# Get ID of the root mandate - we'll use this for new users
rootMandateId = adminGateway.getInitialId("mandates")
adminUserId = adminGateway.getInitialId("users")
if not rootMandateId or not adminUserId:
raise HTTPException(
status_code=500,
detail="System is not properly initialized with root mandate and admin user"
)
# Use a gateway with admin context for user creation
gateway = getGatewayInterface(rootMandateId, adminUserId)
if "username" not in userData:
raise HTTPException(status_code=400, detail="Username required")
try:
# Create user data with a random password since it won't be used
import secrets
random_password = secrets.token_urlsafe(32)
# Create user with required fields
newUser = gateway.createUser(
username=userData["username"],
password=random_password, # Random password since MSAL auth will be used
email=userData.get("email"),
fullName=userData.get("fullName"),
language=userData.get("language", "de"),
_mandateId=rootMandateId,
disabled=False,
privilege="user"
)
return newUser
except ValueError as e:
logger.error(f"ValueError in MSAL registration: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except PermissionError as e:
logger.error(f"PermissionError in MSAL registration: {str(e)}")
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
import traceback
logger.error(f"Unexpected error in MSAL registration: {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"MSAL Registration failed: {str(e)}")
@router.get("/{userId}", response_model=Dict[str, Any])
async def getUser(
userId: str = Path(..., description="ID of the user"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get a specific user"""
context = await getContext(currentUser)
# Initialize gateway interface with user context
userToGet = context.interfaceData.getUser(userId)
if not userToGet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {userId} not found"
)
# Permission check
# User can only view themselves, Admin only users of their own mandate, SysAdmin all
if userId == str(context._userId):
# User can view themselves
pass
elif currentUser.get("privilege") == "admin" and userToGet.get("_mandateId") == context._mandateId:
# Admin can view users of their own mandate
pass
elif currentUser.get("privilege") == "sysadmin":
# SysAdmin can view all users
pass
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to view this user"
)
return userToGet
@router.put("/{userId}", response_model=Dict[str, Any])
async def updateUser(
userId: str = Path(..., description="ID of the user to update"),
userData: Dict[str, Any] = Body(..., description="Updated user data"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Update an existing user"""
context = await getContext(currentUser)
# User exists?
userToUpdate = context.interfaceData.getUser(userId)
if not userToUpdate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {userId} not found"
)
# Permission check
isSelfUpdate = userId == str(context._userId)
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
sameMandate = userToUpdate.get("_mandateId") == context._mandateId
# Filter allowed fields based on permission level
allowedFields = {"username", "email", "fullName", "language"}
sensitiveFields = {"_mandateId", "disabled", "privilege"}
# Check if sensitive fields should be changed
sensitiveUpdate = any(field in userData for field in sensitiveFields)
if isSelfUpdate and sensitiveUpdate:
# Normal users cannot change their sensitive data
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to change sensitive user data"
)
elif isAdmin and sensitiveUpdate and not sameMandate:
# Admins can only change sensitive data for users of their own mandate
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to change sensitive data for users of other mandates"
)
elif not (isSelfUpdate or (isAdmin and sameMandate) or isSysadmin):
# No permission for other cases
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to update this user"
)
# Dynamically filter attributes from the request
updateData = {}
for attr in userAttributes:
if attr in userData and attr != "id": # ID cannot be changed
updateData[attr] = userData[attr]
# Remove disallowed fields for normal users
if not (isAdmin or isSysadmin):
updateData = {k: v for k, v in updateData.items() if k in allowedFields}
# Update user data
updatedUser = context.interfaceData.updateUser(userId, updateData)
return updatedUser
@router.delete("/{userId}", status_code=status.HTTP_204_NO_CONTENT)
async def deleteUser(
userId: str = Path(..., description="ID of the user to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Delete a user"""
context = await getContext(currentUser)
# User exists?
userToDelete = context.interfaceData.getUser(userId)
if not userToDelete:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {userId} not found"
)
# Permission check
isSelfDelete = userId == str(context._userId)
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
sameMandate = userToDelete.get("_mandateId") == context._mandateId
if isSelfDelete:
# User can delete themselves
pass
elif isAdmin and sameMandate:
# Admin can delete users of their own mandate
pass
elif isSysadmin:
# SysAdmin can delete all users
pass
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to delete this user"
)
# Delete user and all referenced objects
success = context.interfaceData.deleteUser(userId)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting user with ID {userId}"
)
return None