Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release / publish (updater) (push) Canceled after 0s
- **Single Sign-on (OIDC)**: Anmeldung über Authentik, Keycloak, Entra ID, Google u. a. (Authorization Code mit PKCE). Die Rolle kommt bei jeder Anmeldung aus den Gruppen des Anbieters, ohne zugeordnete Gruppe kommt niemand hinein; SSO-Konten werden nie mit lokalen Konten verknüpft; lokale Anmeldung bleibt als Notzugang. ID-Token werden streng geprüft (Signatur nur asymmetrisch, Aussteller, Zielgruppe, Ablauf, Nonce, einmaliger State), gegen einen nachgebauten Anbieter getestet (inkl. Fälschungsversuche). Neue Tabellen `oidc_settings`, `oidc_login_states`, Spalten `admin_users.auth_source/oidc_subject` (Migration `0013`). Neue Abhängigkeit: PyJWT. - **Behoben:** `opnexus backup --full` und `opnexus restore` (Vollsicherung, in 1.19.0 beschrieben) fehlten im 1.19.0-Bundle und -Updater-Image, weil die Änderung am Skript nicht mit eingecheckt war; sie sind erst ab dieser Version enthalten. - **Aufgabentrennung**: Schalter, mit dem nur Admins ausrollen dürfen, Operatoren aber vorbereiten (Migration `0014`).
42 lines
1.5 KiB
Python
42 lines
1.5 KiB
Python
"""Separation of duties: with ``operators_may_deploy`` off, only admins roll changes out (AuthMiddleware enforces it)."""
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.errors import ErrorCode, api_error
|
|
from app.models.governance import GovernanceSettings
|
|
from app.models.user import AdminUser
|
|
from app.routers.auth import current_user
|
|
|
|
router = APIRouter(prefix="/settings/governance", tags=["governance"])
|
|
|
|
|
|
class GovernanceRead(BaseModel):
|
|
operators_may_deploy: bool
|
|
|
|
|
|
async def get_settings(db: AsyncSession) -> GovernanceSettings:
|
|
row = await db.get(GovernanceSettings, 1)
|
|
if row is None:
|
|
row = GovernanceSettings(id=1, operators_may_deploy=True)
|
|
db.add(row)
|
|
await db.flush()
|
|
return row
|
|
|
|
|
|
@router.get("", response_model=GovernanceRead)
|
|
async def read(db: AsyncSession = Depends(get_db), user: AdminUser = Depends(current_user)):
|
|
"""Readable by everybody signed in (the UI explains why a deploy was refused)."""
|
|
return await get_settings(db)
|
|
|
|
|
|
@router.put("", response_model=GovernanceRead)
|
|
async def update(payload: GovernanceRead, db: AsyncSession = Depends(get_db), user: AdminUser = Depends(current_user)):
|
|
if user.role != "admin":
|
|
raise api_error(403, ErrorCode.ADMIN_REQUIRED)
|
|
row = await get_settings(db)
|
|
row.operators_may_deploy = payload.operators_may_deploy
|
|
await db.commit()
|
|
return row
|