375 lines
No EOL
14 KiB
Python
375 lines
No EOL
14 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
|
|
data = await request.json()
|
|
logger.info(f"Registration request data: {data}")
|
|
|
|
# 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=500,
|
|
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)
|
|
|
|
# Check required fields
|
|
if not data.get("username") or not data.get("password"):
|
|
logger.error("Missing required fields in registration request")
|
|
raise HTTPException(status_code=400, detail="Username and password are required")
|
|
|
|
# Create user data
|
|
userData = {
|
|
"username": data["username"],
|
|
"password": data["password"],
|
|
"email": data.get("email"),
|
|
"fullName": data.get("fullName"),
|
|
"language": data.get("language", "de"),
|
|
"_mandateId": rootMandateId,
|
|
"disabled": False,
|
|
"privilege": "user"
|
|
}
|
|
|
|
# Create the user
|
|
logger.info(f"Attempting to create user with data: {userData}")
|
|
createdUser = adminGateway.createUser(**userData)
|
|
logger.info(f"User created successfully: {createdUser}")
|
|
|
|
# Add a small delay to ensure database consistency
|
|
time.sleep(0.5)
|
|
|
|
# Verify the user was created and password was stored
|
|
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")
|
|
|
|
# Return success response
|
|
return {
|
|
"message": "User registered successfully",
|
|
"userId": createdUser["id"]
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.error(f"Validation error during registration: {str(e)}")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except PermissionError as e:
|
|
logger.error(f"Permission error during registration: {str(e)}")
|
|
raise HTTPException(status_code=403, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error during registration: {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@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 |