Files
Aegis/backend/app/routers/users.py
T
kitos 58c0143304
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
feat(permissions): admin no longer acts on the test workflow; managers coordinate operators
Admin administers the site — it no longer has any override on
validate-red/blue, review-red/blue, resolve-dispute, request-discussion,
reopen, hold, resume, assign-operators, or classification-update.
Introduces require_any_role_strict(), a role dependency without the
global admin bypass, so these specific endpoints truly exclude admin
instead of only removing it from the (redundant) role tuple.

Managers gain the ability to assign red_tech/blue_tech operators
(POST /tests/{id}/assign, GET /users/operators) alongside leads, since
that's coordination, not resolving a ticket.

Also enlarges and repositions the operator-assignment controls next
to the Start Execution button, and fixes a literal '\u2014' rendering
as text instead of an em dash in the test detail technique line.
2026-07-10 10:28:04 +02:00

243 lines
7.9 KiB
Python

"""User management router (admin only)."""
# Import uuid
import uuid
# Import APIRouter, Depends, status from fastapi
from fastapi import APIRouter, Depends, status
# Import Session from sqlalchemy.orm
from sqlalchemy.orm import Session
# Import get_db from app.database
from app.database import get_db
# Import require_role from app.dependencies.auth
from app.dependencies.auth import require_role, require_any_role_strict
# Import UnitOfWork from app.domain.unit_of_work
from app.domain.unit_of_work import UnitOfWork
# Import User from app.models.user
from app.models.user import User
from app.dependencies.auth import get_current_user
from app.schemas.user import UserCreate, UserUpdate, UserOut, UserPreferencesUpdate, UserOperatorOut
from app.services.audit_service import log_action
# Import from app.services.user_service
from app.services.user_service import (
create_user,
get_user_or_raise,
list_users,
update_user,
)
# Assign router = APIRouter(prefix="/users", tags=["users"])
router = APIRouter(prefix="/users", tags=["users"])
# ---------------------------------------------------------------------------
# PATCH /users/me/preferences — update current user preferences
# ---------------------------------------------------------------------------
@router.patch("/me/preferences", response_model=UserOut)
def update_my_preferences(
payload: UserPreferencesUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update the current user's notification preferences, Jira account ID and Jira API token.
Send ``jira_api_token: ""`` to clear a previously stored token.
The token is never returned in any response.
"""
update_data = payload.model_dump(exclude_unset=True)
for field, value in update_data.items():
if field in ("jira_api_token", "jira_email", "tempo_api_token"):
# Empty string means "clear the value"
setattr(current_user, field, value if value else None)
else:
setattr(current_user, field, value)
db.commit()
db.refresh(current_user)
return current_user
# ---------------------------------------------------------------------------
# GET /users/me — get current user's own profile
# ---------------------------------------------------------------------------
@router.get("/me", response_model=UserOut)
def get_me(
current_user: User = Depends(get_current_user),
):
"""Return the currently authenticated user's profile."""
return current_user
# ---------------------------------------------------------------------------
# GET /users — list all users
# ---------------------------------------------------------------------------
@router.get("", response_model=list[UserOut])
# Define function list_users_route
def list_users_route(
# Entry: db
db: Session = Depends(get_db),
# Entry: current_user
current_user: User = Depends(require_role("admin")),
) -> list[UserOut]:
"""Return a list of all users. **Requires admin role.**."""
# Return list_users(db)
return list_users(db)
# ---------------------------------------------------------------------------
# POST /users — create a new user
# ---------------------------------------------------------------------------
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
# Define function create_user_route
def create_user_route(
# Entry: payload
payload: UserCreate,
# Entry: db
db: Session = Depends(get_db),
# Entry: current_user
current_user: User = Depends(require_role("admin")),
) -> UserOut:
"""Create a new user. **Requires admin role.**."""
# Open context manager
with UnitOfWork(db) as uow:
# Assign user = create_user(
user = create_user(
db,
# Keyword argument: username
username=payload.username,
# Keyword argument: email
email=payload.email,
# Keyword argument: password
password=payload.password,
# Keyword argument: role
role=payload.role,
)
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=current_user.id,
# Keyword argument: action
action="create_user",
# Keyword argument: entity_type
entity_type="user",
# Keyword argument: entity_id
entity_id=user.id,
# Keyword argument: details
details={"username": user.username, "role": user.role},
)
# Call uow.commit()
uow.commit()
# Reload ORM object attributes from the database
db.refresh(user)
# Return user
return user
# ---------------------------------------------------------------------------
# GET /users/operators — minimal operator/lead list for assignment pickers
# ---------------------------------------------------------------------------
@router.get("/operators", response_model=list[UserOperatorOut])
def list_operators_route(
db: Session = Depends(get_db),
current_user: User = Depends(require_any_role_strict("manager", "red_lead", "blue_lead")),
) -> list[UserOperatorOut]:
"""Return active red/blue operators and leads, for lead/manager assignment pickers.
Not reachable by admin — admin administers the site, leads/managers
coordinate operators. Returns only id/username/role — no emails or
tokens — since this is reachable by non-admin leads.
"""
return (
db.query(User)
.filter(
User.role.in_(["red_tech", "red_lead", "blue_tech", "blue_lead", "admin"]),
User.is_active.is_(True),
)
.order_by(User.username)
.all()
)
# ---------------------------------------------------------------------------
# GET /users/{id} — get a single user
# ---------------------------------------------------------------------------
@router.get("/{user_id}", response_model=UserOut)
# Define function get_user
def get_user(
# Entry: user_id
user_id: uuid.UUID,
# Entry: db
db: Session = Depends(get_db),
# Entry: current_user
current_user: User = Depends(require_role("admin")),
) -> UserOut:
"""Return a single user by ID. **Requires admin role.**."""
# Return get_user_or_raise(db, user_id)
return get_user_or_raise(db, user_id)
# ---------------------------------------------------------------------------
# PATCH /users/{id} — update a user
# ---------------------------------------------------------------------------
@router.patch("/{user_id}", response_model=UserOut)
# Define function update_user_route
def update_user_route(
# Entry: user_id
user_id: uuid.UUID,
# Entry: payload
payload: UserUpdate,
# Entry: db
db: Session = Depends(get_db),
# Entry: current_user
current_user: User = Depends(require_role("admin")),
) -> UserOut:
"""Update one or more fields of an existing user. **Requires admin role.**."""
# Assign update_data = payload.model_dump(exclude_unset=True)
update_data = payload.model_dump(exclude_unset=True)
# Open context manager
with UnitOfWork(db) as uow:
# Assign user = update_user(db, user_id, **update_data)
user = update_user(db, user_id, **update_data)
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=current_user.id,
# Keyword argument: action
action="update_user",
# Keyword argument: entity_type
entity_type="user",
# Keyword argument: entity_id
entity_id=user.id,
# Keyword argument: details
details={"updated_fields": list(update_data.keys())},
)
# Call uow.commit()
uow.commit()
# Reload ORM object attributes from the database
db.refresh(user)
# Return user
return user