mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 15:34:36 +01:00
703 lines
19 KiB
Python
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(),
|
|
} |