Files
enclava/backend/app/api/v1/endpoints/user_management.py
2025-11-20 11:11:18 +01:00

703 lines
19 KiB
Python

"""
User Management API endpoints
Admin endpoints for managing users, roles, and audit logs
"""
import logging
from typing import Optional, List, Dict, Any
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer
from pydantic import BaseModel, EmailStr, validator, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import get_current_user
from app.db.database import get_db
from app.models.user import User
from app.models.role import Role
from app.models.audit_log import AuditLog
from app.services.user_management_service import UserManagementService
from app.services.permission_manager import require_permission
from app.schemas.role import RoleCreate, RoleUpdate
logger = logging.getLogger(__name__)
router = APIRouter()
security = HTTPBearer()
# Request/Response Models
class CreateUserRequest(BaseModel):
email: EmailStr
username: str
password: str
full_name: Optional[str] = None
role_id: Optional[int] = None
is_active: bool = True
force_password_change: bool = False
@validator("password")
def validate_password(cls, v):
if len(v) < 8:
raise ValueError("Password must be at least 8 characters long")
return v
@validator("username")
def validate_username(cls, v):
if len(v) < 3:
raise ValueError("Username must be at least 3 characters long")
if not v.replace("_", "").replace("-", "").isalnum():
raise ValueError("Username must contain only alphanumeric characters, underscores, and hyphens")
return v
class UpdateUserRequest(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = None
full_name: Optional[str] = None
role_id: Optional[int] = None
is_active: Optional[bool] = None
is_verified: Optional[bool] = None
custom_permissions: Optional[Dict[str, Any]] = None
@validator("username")
def validate_username(cls, v):
if v is not None and len(v) < 3:
raise ValueError("Username must be at least 3 characters long")
return v
class AdminPasswordResetRequest(BaseModel):
new_password: str
force_change_on_login: bool = True
@validator("new_password")
def validate_password(cls, v):
if len(v) < 8:
raise ValueError("Password must be at least 8 characters long")
return v
class UserResponse(BaseModel):
id: int
email: str
username: str
full_name: Optional[str]
role_id: Optional[int]
role: Optional[Dict[str, Any]]
is_active: bool
is_verified: bool
account_locked: bool
force_password_change: bool
created_at: datetime
updated_at: datetime
last_login: Optional[datetime]
audit_summary: Optional[Dict[str, Any]] = None
class Config:
from_attributes = True
class UserListResponse(BaseModel):
users: List[UserResponse]
total: int
skip: int
limit: int
class RoleResponse(BaseModel):
id: int
name: str
display_name: str
description: Optional[str]
level: str
is_active: bool
created_at: datetime
class Config:
from_attributes = True
class AuditLogResponse(BaseModel):
id: int
user_id: Optional[int]
action: str
resource_type: str
resource_id: Optional[str]
description: str
details: Dict[str, Any]
severity: str
category: Optional[str]
success: bool
created_at: datetime
class Config:
from_attributes = True
# User Management Endpoints
@router.get("/users", response_model=UserListResponse)
async def get_users(
request: Request,
skip: int = 0,
limit: int = 100,
search: Optional[str] = None,
role_id: Optional[int] = None,
is_active: Optional[bool] = None,
include_audit_summary: bool = False,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get all users with filtering options"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:users:read",
context={"user_id": current_user["id"]}
)
service = UserManagementService(db)
users_data = await service.get_users(
skip=skip,
limit=limit,
search=search,
role_id=role_id,
is_active=is_active,
)
# Convert to response format
users = []
for user in users_data:
user_dict = user.to_dict()
if include_audit_summary:
# Get audit summary for user
audit_logs = await service.get_user_audit_logs(user.id, limit=10)
user_dict["audit_summary"] = {
"recent_actions": len(audit_logs),
"last_login": user.last_login.isoformat() if user.last_login else None,
}
user_response = UserResponse(**user_dict)
users.append(user_response)
return UserListResponse(
users=users,
total=len(users), # Would need actual count query for large datasets
skip=skip,
limit=limit,
)
@router.get("/users/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
include_audit_summary: bool = False,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get specific user by ID"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:users:read",
context={"user_id": current_user["id"], "owner_id": user_id}
)
service = UserManagementService(db)
user = await service.get_user_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
user_dict = user.to_dict()
if include_audit_summary:
# Get audit summary for user
audit_logs = await service.get_user_audit_logs(user_id, limit=10)
user_dict["audit_summary"] = {
"recent_actions": len(audit_logs),
"last_login": user.last_login.isoformat() if user.last_login else None,
}
return UserResponse(**user_dict)
@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: CreateUserRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create a new user"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:users:create",
)
service = UserManagementService(db)
user = await service.create_user(
email=user_data.email,
username=user_data.username,
password=user_data.password,
full_name=user_data.full_name,
role_id=user_data.role_id,
is_active=user_data.is_active,
is_verified=True, # Admin-created users are verified by default
custom_permissions={}, # Empty by default
)
# Log user creation in audit
await service._log_audit_event(
user_id=current_user["id"],
action="create",
resource_type="user",
resource_id=str(user.id),
description=f"User created by admin: {user.email}",
details={
"created_by": current_user["email"],
"target_user": user.email,
"role_id": user_data.role_id,
},
severity="medium",
)
user_dict = user.to_dict()
return UserResponse(**user_dict)
@router.put("/users/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_data: UpdateUserRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update user information"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:users:update",
context={"user_id": current_user["id"], "owner_id": user_id}
)
service = UserManagementService(db)
# Get current user for audit comparison
current_user_data = await service.get_user_by_id(user_id)
if not current_user_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
old_values = current_user_data.to_dict()
# Update user
user = await service.update_user(
user_id=user_id,
email=user_data.email,
username=user_data.username,
full_name=user_data.full_name,
role_id=user_data.role_id,
is_active=user_data.is_active,
is_verified=user_data.is_verified,
custom_permissions=user_data.custom_permissions,
)
# Log user update in audit
await service._log_audit_event(
user_id=current_user["id"],
action="update",
resource_type="user",
resource_id=str(user_id),
description=f"User updated by admin: {user.email}",
details={
"updated_by": current_user["email"],
"target_user": user.email,
},
old_values=old_values,
new_values=user.to_dict(),
severity="medium",
)
user_dict = user.to_dict()
return UserResponse(**user_dict)
@router.post("/users/{user_id}/password-reset")
async def admin_reset_password(
user_id: int,
password_data: AdminPasswordResetRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Admin reset user password with forced change option"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:users:manage",
)
service = UserManagementService(db)
user = await service.admin_reset_user_password(
user_id=user_id,
new_password=password_data.new_password,
force_change_on_login=password_data.force_change_on_login,
admin_user_id=current_user["id"],
)
return {
"message": f"Password reset for user {user.email}",
"force_change_on_login": password_data.force_change_on_login,
}
@router.delete("/users/{user_id}")
async def delete_user(
user_id: int,
hard_delete: bool = False,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Delete or deactivate user"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:users:delete",
)
service = UserManagementService(db)
# Get user info for audit before deletion
user = await service.get_user_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
user_email = user.email
# Delete user
success = await service.delete_user(user_id, hard_delete=hard_delete)
# Log user deletion in audit
await service._log_audit_event(
user_id=current_user["id"],
action="delete",
resource_type="user",
resource_id=str(user_id),
description=f"User {'hard deleted' if hard_delete else 'deactivated'} by admin: {user_email}",
details={
"deleted_by": current_user["email"],
"target_user": user_email,
"hard_delete": hard_delete,
},
severity="high",
)
return {
"message": f"User {'deleted' if hard_delete else 'deactivated'} successfully",
"user_email": user_email,
}
# Role Management Endpoints
@router.get("/roles", response_model=List[RoleResponse])
async def get_roles(
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get all available roles"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:roles:read",
)
service = UserManagementService(db)
roles = await service.get_roles(is_active=True)
return [RoleResponse(**role.to_dict()) for role in roles]
@router.post("/roles", response_model=RoleResponse, status_code=status.HTTP_201_CREATED)
async def create_role(
role_data: RoleCreate,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create a new role"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:roles:create",
)
service = UserManagementService(db)
# Create role
role = await service.create_role(
name=role_data.name,
display_name=role_data.display_name,
description=role_data.description,
level=role_data.level,
permissions=role_data.permissions,
can_manage_users=role_data.can_manage_users,
can_manage_budgets=role_data.can_manage_budgets,
can_view_reports=role_data.can_view_reports,
can_manage_tools=role_data.can_manage_tools,
inherits_from=role_data.inherits_from,
is_active=role_data.is_active,
is_system_role=role_data.is_system_role,
)
# Log role creation
await service._log_audit_event(
user_id=current_user["id"],
action="create",
resource_type="role",
resource_id=str(role.id),
description=f"Role created: {role.name}",
details={
"created_by": current_user["email"],
"role_name": role.name,
"level": role.level,
},
severity="medium",
)
return RoleResponse(**role.to_dict())
@router.put("/roles/{role_id}", response_model=RoleResponse)
async def update_role(
role_id: int,
role_data: RoleUpdate,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update a role"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:roles:update",
)
service = UserManagementService(db)
# Get current role for audit
current_role = await service.get_role_by_id(role_id)
if not current_role:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Role not found"
)
# Prevent updating system roles
if current_role.is_system_role:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot modify system roles"
)
old_values = current_role.to_dict()
# Update role
role = await service.update_role(
role_id=role_id,
display_name=role_data.display_name,
description=role_data.description,
permissions=role_data.permissions,
can_manage_users=role_data.can_manage_users,
can_manage_budgets=role_data.can_manage_budgets,
can_view_reports=role_data.can_view_reports,
can_manage_tools=role_data.can_manage_tools,
is_active=role_data.is_active,
)
# Log role update
await service._log_audit_event(
user_id=current_user["id"],
action="update",
resource_type="role",
resource_id=str(role_id),
description=f"Role updated: {role.name}",
details={
"updated_by": current_user["email"],
"role_name": role.name,
},
old_values=old_values,
new_values=role.to_dict(),
severity="medium",
)
return RoleResponse(**role.to_dict())
@router.delete("/roles/{role_id}")
async def delete_role(
role_id: int,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Delete a role"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:roles:delete",
)
service = UserManagementService(db)
# Get role info for audit before deletion
role = await service.get_role_by_id(role_id)
if not role:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Role not found"
)
# Prevent deleting system roles
if role.is_system_role:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot delete system roles"
)
role_name = role.name
# Delete role
success = await service.delete_role(role_id)
# Log role deletion
await service._log_audit_event(
user_id=current_user["id"],
action="delete",
resource_type="role",
resource_id=str(role_id),
description=f"Role deleted: {role_name}",
details={
"deleted_by": current_user["email"],
"role_name": role_name,
},
severity="high",
)
return {
"message": f"Role {role_name} deleted successfully",
"role_name": role_name,
}
# Audit Log Endpoints
@router.get("/users/{user_id}/audit-logs", response_model=List[AuditLogResponse])
async def get_user_audit_logs(
user_id: int,
skip: int = 0,
limit: int = 50,
action_filter: Optional[str] = None,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get audit logs for a specific user"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:audit:read",
context={"user_id": current_user["id"], "owner_id": user_id}
)
service = UserManagementService(db)
audit_logs = await service.get_user_audit_logs(
user_id=user_id,
skip=skip,
limit=limit,
action_filter=action_filter,
)
return [AuditLogResponse(**log.to_dict()) for log in audit_logs]
@router.get("/audit-logs", response_model=List[AuditLogResponse])
async def get_all_audit_logs(
skip: int = 0,
limit: int = 100,
user_id: Optional[int] = None,
action_filter: Optional[str] = None,
category_filter: Optional[str] = None,
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get all audit logs with filtering"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:audit:read",
)
# Direct database query for audit logs with filters
from sqlalchemy import select, and_, desc
query = select(AuditLog)
conditions = []
if user_id:
conditions.append(AuditLog.user_id == user_id)
if action_filter:
conditions.append(AuditLog.action == action_filter)
if category_filter:
conditions.append(AuditLog.category == category_filter)
if conditions:
query = query.where(and_(*conditions))
query = query.order_by(desc(AuditLog.created_at))
query = query.offset(skip).limit(limit)
result = await db.execute(query)
audit_logs = result.scalars().all()
return [AuditLogResponse(**log.to_dict()) for log in audit_logs]
# Statistics Endpoints
@router.get("/statistics")
async def get_user_management_statistics(
current_user: Dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get user management statistics"""
# Check permission
require_permission(
current_user.get("permissions", []),
"platform:users:read",
)
service = UserManagementService(db)
user_stats = await service.get_user_statistics()
role_stats = await service.get_role_statistics()
return {
"users": user_stats,
"roles": role_stats,
"generated_at": datetime.utcnow().isoformat(),
}