5.3 KiB
Design Spec: Auth — Öffentliche Routen & Permission-System
Datum: 2026-04-27 Status: Genehmigt
Ziel
Öffentliche und geschützte Routen unterscheiden. Geschützte Routen sind durch eine Dependency einfach zu kennzeichnen. Die Landing Page ist ohne Login erreichbar, zeigt aber dem eingeloggten User seine persönlichen Kacheln. Kacheln werden nur angezeigt, wenn der aktuelle User (oder Anonymous) berechtigt ist.
Permission-Modell
permission auf einem Modul ist eine list[str]. Zugriff wird gewährt wenn der User mindestens einen Wert erfüllt (OR-Logik).
| Wert | Bedingung |
|---|---|
"public" |
immer True (auch anonyme User) |
"authenticated" |
User ist eingeloggt |
"admin" |
user.is_admin == True |
"<gruppenname>" |
User ist Mitglied dieser Gruppe (Part 2) |
Modul-Metadaten (slug, icon, name, description, status) bleiben als Code-Artefakt im MODULES-Dict. Berechtigungen wandern erst in die DB wenn Gruppen in Part 2 implementiert werden — check_permission ist der einzige Ort der dann angepasst werden muss.
Neue/geänderte Komponenten
app/modules/auth/dependencies.py
Drei Ergänzungen:
get_current_user_optional — gibt User | None zurück, wirft nie eine Exception. Für öffentliche Routen die optional Userinfo brauchen.
async def get_current_user_optional(
request: Request, db: Session = Depends(get_db)
) -> User | None:
token = get_token_from_request(request)
if not token:
return None
payload = decode_token(token)
if not payload:
return None
return get_user(db, payload.get("sub", ""))
check_permission — reine Funktion, kein FastAPI-Kontext:
def check_permission(user: User | None, permissions: list[str]) -> bool:
if "public" in permissions:
return True
if user is None:
return False
if "authenticated" in permissions:
return True
if "admin" in permissions and user.is_admin:
return True
return False # group check: Part 2
require_permission — Dependency-Factory:
def require_permission(permissions: list[str]):
async def _dep(
request: Request, db: Session = Depends(get_db)
) -> User | None:
user = await get_current_user_optional(request, db)
if not check_permission(user, permissions):
raise RequiresLoginException()
return user
return _dep
Bestehende get_current_user und require_admin bleiben für Kompatibilität erhalten, delegieren aber intern an require_permission:
get_current_user=require_permission(["authenticated"])require_admin=require_permission(["admin"])
app/main.py — MODULES-Registry
MODULES bekommt slug und permission:
MODULES = [
{
"slug": "rss",
"icon": "📡",
"name": "RSS-Feed Server",
"description": "Aggregiert und verteilt Neuigkeiten der Fakultät als standardkonformen RSS 2.0 Feed.",
"status": "active",
"permission": ["authenticated"],
},
{
"slug": "kalender",
"icon": "📅",
"name": "Kalender",
"description": "Veranstaltungen, Prüfungstermine und Fristen im iCal-Format.",
"status": "planned",
"permission": ["authenticated"],
},
# ...
]
Aktive Module (status == "active") werden beim App-Start automatisch registriert:
import importlib
for module in MODULES:
if module["status"] == "active":
mod = importlib.import_module(f"app.modules.{module['slug']}.router")
app.include_router(mod.router, prefix=f"/{module['slug']}")
GET / wird öffentlich — get_current_user_optional statt get_current_user:
@app.get("/", response_class=HTMLResponse)
async def root(request: Request, current_user=Depends(get_current_user_optional)):
visible = [m for m in MODULES if check_permission(current_user, m["permission"])]
return templates.TemplateResponse(request, "index.html", {
"nav_items": NAV_ITEMS,
"modules": visible,
"db_mode": _db_mode(),
"app_version": "0.1.0",
"current_user": current_user,
})
app/templates/base.html — Navbar
Navbar zeigt conditional:
- Eingeloggt: Username + Logout-Button
- Nicht eingeloggt: Anmelden-Button (→
/auth/login)
{% if current_user %}
<span class="text-white/85 text-sm">{{ current_user.username }}</span>
<a href="/auth/logout" class="...">Abmelden</a>
{% else %}
<a href="/auth/login" class="...">Anmelden</a>
{% endif %}
Neue Modul-Konvention
Ein Entwickler der ein neues Modul anlegt:
- Verzeichnis
app/modules/<slug>/mit__init__.pyundrouter.py router.pyschützt alle Routen über den Router:
Für feinere Kontrolle einzelner Routen:router = APIRouter(dependencies=[Depends(require_permission(["authenticated"]))])@router.get("/secret", dependencies=[Depends(require_permission(["admin"]))])- Eintrag in
MODULESinmain.pymitstatus: "active"und passendempermission
Out of Scope
- Gruppen-Tabelle und Gruppen-Verwaltung (→ Part 2)
- Modul-Permissions in der DB (→ Part 2, zusammen mit Gruppen)
- Admin-UI für Berechtigungsvergabe
- Per-Objekt-Permissions (Row-Level Security)