Add sudo-style elevation for superadmin actions
CurrentSuperuser now requires a short-lived (5 min) elevated JWT
claim in addition to the is_superadmin flag, obtained via the new
POST /auth/elevate (no re-authentication — the user is already the
only superadmin in practice; this is a deliberate-confirmation guard
against accidentally triggering a destructive action, not a defense
against a stolen session). Regular login tokens keep working unchanged
for all non-superadmin routes; DELETE /participants/{id} from task 5
now needs a fresh /auth/elevate call, no other code changes required
since CurrentSuperuser's meaning changed underneath it.
create_access_token gained an extra_claims param (backward compatible)
to carry the elevated flag.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ded23efc6a
commit
0ee6ef2e68
5 changed files with 36 additions and 3 deletions
|
|
@ -2,3 +2,4 @@ DATABASE_URL=postgresql+psycopg://user:password@localhost:5432/vinindkoeb
|
|||
ENVIRONMENT=development
|
||||
SECRET_KEY=change-me # generér med: openssl rand -hex 32
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=120
|
||||
ELEVATION_EXPIRE_MINUTES=5
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class Settings(BaseSettings):
|
|||
environment: str = "development"
|
||||
secret_key: str
|
||||
access_token_expire_minutes: int = 120
|
||||
elevation_expire_minutes: int = 5
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
|
|
|
|||
|
|
@ -17,12 +17,19 @@ def verify_password(plain: str, hashed: str) -> bool:
|
|||
return _password_hash.verify(plain, hashed)
|
||||
|
||||
|
||||
def create_access_token(subject: str, expires_delta: timedelta | None = None) -> str:
|
||||
def create_access_token(
|
||||
subject: str,
|
||||
expires_delta: timedelta | None = None,
|
||||
extra_claims: dict | None = None,
|
||||
) -> str:
|
||||
settings = get_settings()
|
||||
expire = datetime.now(timezone.utc) + (
|
||||
expires_delta or timedelta(minutes=settings.access_token_expire_minutes)
|
||||
)
|
||||
return jwt.encode({"sub": subject, "exp": expire}, settings.secret_key, algorithm=ALGORITHM)
|
||||
payload = {"sub": subject, "exp": expire}
|
||||
if extra_claims:
|
||||
payload.update(extra_claims)
|
||||
return jwt.encode(payload, settings.secret_key, algorithm=ALGORITHM)
|
||||
|
||||
|
||||
def decode_access_token(token: str) -> dict | None:
|
||||
|
|
|
|||
|
|
@ -31,9 +31,18 @@ def get_current_user(session: SessionDep, token: Annotated[str, Depends(oauth2_s
|
|||
CurrentUser = Annotated[User, Depends(get_current_user)]
|
||||
|
||||
|
||||
def get_current_active_superuser(current_user: CurrentUser) -> User:
|
||||
def get_current_active_superuser(
|
||||
current_user: CurrentUser,
|
||||
token: Annotated[str, Depends(oauth2_scheme)],
|
||||
) -> User:
|
||||
if not current_user.is_superadmin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough privileges")
|
||||
payload = decode_access_token(token)
|
||||
if not payload or not payload.get("elevated"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="This action requires an elevated session — call POST /auth/elevate first",
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from datetime import timedelta
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import SQLModel, select
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.security import create_access_token, verify_password
|
||||
from app.db import SessionDep
|
||||
from app.dependencies import CurrentUser
|
||||
|
|
@ -35,3 +37,16 @@ def login(
|
|||
@router.get("/me", response_model=UserPublic)
|
||||
def me(current_user: CurrentUser) -> User:
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post("/elevate")
|
||||
def elevate(current_user: CurrentUser) -> Token:
|
||||
if not current_user.is_superadmin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is not a superadmin")
|
||||
settings = get_settings()
|
||||
token = create_access_token(
|
||||
subject=str(current_user.id),
|
||||
expires_delta=timedelta(minutes=settings.elevation_expire_minutes),
|
||||
extra_claims={"elevated": True},
|
||||
)
|
||||
return Token(access_token=token)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue