Added super user for whole service management

This commit is contained in:
gulimabr
2025-12-05 15:00:14 -03:00
parent 18f44c0e85
commit 592da7a2b6
21 changed files with 2268 additions and 25 deletions

View File

@@ -17,7 +17,9 @@ from src.models import (
RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES,
CommentResponse, CommentReplyResponse, CommentCreateRequest, ReplyCreateRequest,
RequirementStatusResponse, DeletedRequirementResponse,
UserCreateRequest, UserCreateResponse
UserCreateRequest, UserCreateResponse,
SystemUserResponse, SystemProjectResponse, SystemProjectMemberResponse,
AssignUserToProjectRequest, SystemUserCreateRequest
)
from src.controller import AuthController
from src.config import get_openid, get_settings
@@ -83,6 +85,45 @@ async def lifespan(app: FastAPI):
await session.commit()
logger.info("Default validation statuses ensured")
# Provision super admin if configured
if settings.super_admin_username and settings.super_admin_email and settings.super_admin_password:
logger.info(f"Super admin configuration found, provisioning user: {settings.super_admin_username}")
try:
# Check if super admin already exists in local DB
async with AsyncSessionLocal() as session:
user_repo = UserRepository(session)
existing_user = await user_repo.get_by_username(settings.super_admin_username)
if existing_user:
logger.info(f"Super admin already exists in database: {existing_user.username} (id: {existing_user.id})")
# Ensure they have super_admin role (role_id=6)
if existing_user.role_id != 6:
logger.info(f"Updating existing super admin to role_id=6")
await user_repo.update_role(existing_user.id, 6)
await session.commit()
else:
# Provision in Keycloak first
keycloak_sub = await KeycloakAdminService.provision_super_admin(
username=settings.super_admin_username,
email=settings.super_admin_email,
password=settings.super_admin_password
)
# Create in local database with super_admin role (role_id=6)
new_user = await user_repo.create(
sub=keycloak_sub,
role_id=6, # super_admin role
username=settings.super_admin_username,
full_name="Super Admin"
)
await session.commit()
logger.info(f"Super admin provisioned successfully: {new_user.username} (id: {new_user.id})")
except Exception as e:
logger.error(f"Failed to provision super admin: {e}")
# Don't fail startup if super admin provisioning fails
else:
logger.info("No super admin configuration found, skipping provisioning")
yield
# Shutdown
@@ -2064,3 +2105,488 @@ async def delete_reply(
await reply_repo.soft_delete_reply(reply_id)
await db.commit()
# ===========================================
# Super Admin System Endpoints (role_id=6 only)
# ===========================================
def _require_super_admin(user):
"""
Helper to check if user is a super admin (role_id=6).
Raises:
HTTPException: 403 Forbidden if user is not a super admin
"""
if user.role_id != 6:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only super admins can access this endpoint"
)
@app.get("/api/admin/system/users", response_model=List[SystemUserResponse])
async def get_all_system_users(
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Get all users in the system (super admin only).
Includes Keycloak enabled status.
Returns:
List of all users with their roles and status.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
user_repo = UserRepository(db)
users = await user_repo.get_all()
# Build response with Keycloak status
result = []
for user in users:
# Get enabled status from Keycloak
is_enabled = True
try:
is_enabled = await KeycloakAdminService.get_user_status(user.sub)
except Exception as e:
logger.warning(f"Could not get Keycloak status for user {user.id}: {e}")
result.append(SystemUserResponse(
id=user.id,
sub=user.sub,
username=user.username,
full_name=user.full_name,
role_id=user.role_id,
role_name=user.role.role_name if user.role else "unknown",
role_display_name=ROLE_DISPLAY_NAMES.get(user.role.role_name, user.role.role_name.title()) if user.role else "Unknown",
is_enabled=is_enabled,
created_at=user.created_at
))
return result
@app.get("/api/admin/system/projects", response_model=List[SystemProjectResponse])
async def get_all_system_projects(
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Get all projects in the system (super admin only).
Includes member count for each project.
Returns:
List of all projects with member counts.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
project_repo = ProjectRepository(db)
projects = await project_repo.get_all()
result = []
for project in projects:
member_count = await project_repo.get_member_count(project.id)
result.append(SystemProjectResponse(
id=project.id,
project_name=project.project_name,
project_desc=project.project_desc,
member_count=member_count,
created_at=project.created_at
))
return result
@app.post("/api/admin/system/projects", response_model=SystemProjectResponse, status_code=status.HTTP_201_CREATED)
async def create_system_project(
request: Request,
project_data: ProjectCreateRequest,
db: AsyncSession = Depends(get_db)
):
"""
Create a new project (super admin only).
Unlike regular project creation, this doesn't add the creator as a member.
Args:
project_data: The project data
Returns:
The created project.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
project_repo = ProjectRepository(db)
project = await project_repo.create(
project_name=project_data.project_name,
project_desc=project_data.project_desc,
creator_id=None, # Super admin creates without being a member
)
await db.commit()
return SystemProjectResponse(
id=project.id,
project_name=project.project_name,
project_desc=project.project_desc,
member_count=0,
created_at=project.created_at
)
@app.get("/api/admin/system/projects/{project_id}/members", response_model=List[SystemProjectMemberResponse])
async def get_system_project_members(
project_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Get all members of a project (super admin only).
Args:
project_id: The project ID
Returns:
List of project members.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
project_repo = ProjectRepository(db)
# Check if project exists
project = await project_repo.get_by_id(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Project with id {project_id} not found"
)
members = await project_repo.get_members(project_id)
return [
SystemProjectMemberResponse(
user_id=member.id,
username=member.username,
full_name=member.full_name,
role_id=member.role_id,
role_name=member.role.role_name if member.role else "unknown",
joined_at=None # TODO: Add joined_at from project_members table
)
for member in members
]
@app.post("/api/admin/system/projects/{project_id}/members", status_code=status.HTTP_201_CREATED)
async def assign_user_to_project(
project_id: int,
request: Request,
data: AssignUserToProjectRequest,
db: AsyncSession = Depends(get_db)
):
"""
Assign a user to a project (super admin only).
Args:
project_id: The project ID
data: The user to assign
Returns:
Success message.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
project_repo = ProjectRepository(db)
user_repo = UserRepository(db)
# Check if project exists
project = await project_repo.get_by_id(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Project with id {project_id} not found"
)
# Check if user exists
user = await user_repo.get_by_id(data.user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with id {data.user_id} not found"
)
# Optionally update role if provided
if data.role_id is not None:
role_repo = RoleRepository(db)
role = await role_repo.get_by_id(data.role_id)
if not role:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid role id {data.role_id}"
)
await user_repo.update_role(data.user_id, data.role_id)
# Add user to project
added = await project_repo.add_member(project_id, data.user_id)
if not added:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User is already a member of this project"
)
await db.commit()
return {"message": "User assigned to project successfully"}
@app.delete("/api/admin/system/projects/{project_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_user_from_project(
project_id: int,
user_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Remove a user from a project (super admin only).
Args:
project_id: The project ID
user_id: The user ID to remove
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
project_repo = ProjectRepository(db)
# Check if project exists
project = await project_repo.get_by_id(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Project with id {project_id} not found"
)
removed = await project_repo.remove_member(project_id, user_id)
if not removed:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User is not a member of this project"
)
await db.commit()
@app.put("/api/admin/system/users/{user_id}/block", status_code=status.HTTP_200_OK)
async def block_user(
user_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Block a user by disabling them in Keycloak (super admin only).
Blocked users cannot log in.
Args:
user_id: The user ID to block
Returns:
Success message.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
# Prevent blocking yourself
if current_user.id == user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You cannot block yourself"
)
user_repo = UserRepository(db)
user = await user_repo.get_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with id {user_id} not found"
)
# Disable in Keycloak
await KeycloakAdminService.disable_user(user.sub)
return {"message": f"User {user.username or user.sub} has been blocked"}
@app.put("/api/admin/system/users/{user_id}/unblock", status_code=status.HTTP_200_OK)
async def unblock_user(
user_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Unblock a user by enabling them in Keycloak (super admin only).
Args:
user_id: The user ID to unblock
Returns:
Success message.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
user_repo = UserRepository(db)
user = await user_repo.get_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with id {user_id} not found"
)
# Enable in Keycloak
await KeycloakAdminService.enable_user(user.sub)
return {"message": f"User {user.username or user.sub} has been unblocked"}
@app.put("/api/admin/system/users/{user_id}/role", response_model=SystemUserResponse)
async def update_system_user_role(
user_id: int,
request: Request,
role_data: UserRoleUpdateRequest,
db: AsyncSession = Depends(get_db)
):
"""
Update a user's role (super admin only).
Can promote users to super_admin or demote them.
Args:
user_id: The user ID to update
role_data: The new role ID
Returns:
The updated user info.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
# Prevent self-demotion from super admin
if current_user.id == user_id and role_data.role_id != 6:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You cannot demote yourself. Ask another super admin to change your role."
)
# Verify role exists
role_repo = RoleRepository(db)
role = await role_repo.get_by_id(role_data.role_id)
if not role:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid role id {role_data.role_id}"
)
# Update the user's role
user_repo = UserRepository(db)
updated_user = await user_repo.update_role(user_id, role_data.role_id)
if not updated_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with id {user_id} not found"
)
await db.commit()
# Get enabled status from Keycloak
is_enabled = True
try:
is_enabled = await KeycloakAdminService.get_user_status(updated_user.sub)
except Exception as e:
logger.warning(f"Could not get Keycloak status for user {user_id}: {e}")
return SystemUserResponse(
id=updated_user.id,
sub=updated_user.sub,
username=updated_user.username,
full_name=updated_user.full_name,
role_id=updated_user.role_id,
role_name=role.role_name,
role_display_name=ROLE_DISPLAY_NAMES.get(role.role_name, role.role_name.title()),
is_enabled=is_enabled,
created_at=updated_user.created_at
)
@app.post("/api/admin/system/users", response_model=SystemUserResponse, status_code=status.HTTP_201_CREATED)
async def create_system_user(
request: Request,
user_data: SystemUserCreateRequest,
db: AsyncSession = Depends(get_db)
):
"""
Create a new user in the system (super admin only).
Creates the user in Keycloak with a temporary password.
Args:
user_data: The user data
Returns:
The created user info.
"""
current_user = await _get_current_user_db(request, db)
_require_super_admin(current_user)
# Validate role exists and is not super_admin unless current user is super admin
role_repo = RoleRepository(db)
role = await role_repo.get_by_id(user_data.role_id)
if not role:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid role id {user_data.role_id}"
)
# Create user in Keycloak
keycloak_sub = await KeycloakAdminService.create_user(
username=user_data.username,
email=user_data.email,
password=user_data.password,
first_name=user_data.first_name,
last_name=user_data.last_name
)
# Build full name
full_name = None
if user_data.first_name or user_data.last_name:
full_name = f"{user_data.first_name or ''} {user_data.last_name or ''}".strip()
# Create in local database
user_repo = UserRepository(db)
new_user = await user_repo.create(
sub=keycloak_sub,
role_id=user_data.role_id,
username=user_data.username,
full_name=full_name
)
await db.commit()
logger.info(f"Super admin {current_user.id} created new user {new_user.id} ({user_data.username})")
return SystemUserResponse(
id=new_user.id,
sub=new_user.sub,
username=new_user.username,
full_name=new_user.full_name,
role_id=new_user.role_id,
role_name=role.role_name,
role_display_name=ROLE_DISPLAY_NAMES.get(role.role_name, role.role_name.title()),
is_enabled=True,
created_at=new_user.created_at
)