second commit
This commit is contained in:
122
app/auth/deps.py
Normal file
122
app/auth/deps.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""FastAPI-Dependencies fuer Authentifizierung und Autorisierung."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Cookie, Depends, HTTPException, Request, Response, WebSocket, status
|
||||
|
||||
from app.db import get_db
|
||||
from app.rbac import user_has_role, user_has_role_for_host
|
||||
from app.security.sessions import (
|
||||
SESSION_COOKIE_NAME,
|
||||
decode_session_token,
|
||||
is_expired,
|
||||
refresh_session_token,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CurrentUser:
|
||||
id: int
|
||||
username: str
|
||||
is_admin: bool
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
response: Response,
|
||||
jh_session: str | None = Cookie(default=None, alias=SESSION_COOKIE_NAME),
|
||||
) -> CurrentUser:
|
||||
if jh_session is None:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Nicht angemeldet")
|
||||
|
||||
payload = decode_session_token(jh_session)
|
||||
if payload is None or is_expired(payload):
|
||||
response.delete_cookie(SESSION_COOKIE_NAME)
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Session abgelaufen oder ungueltig")
|
||||
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"SELECT id, username, is_admin, is_active, session_version FROM users WHERE id = ?",
|
||||
(payload.user_id,),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if row is None or not row[3] or row[4] != payload.session_version:
|
||||
response.delete_cookie(SESSION_COOKIE_NAME)
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Session ungueltig")
|
||||
|
||||
# Gleitenden Idle-Timeout verlaengern (gleiche session_version/login_ts).
|
||||
new_token = refresh_session_token(payload)
|
||||
response.set_cookie(
|
||||
SESSION_COOKIE_NAME,
|
||||
new_token,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="strict",
|
||||
max_age=None, # Session-Cookie; Ablauf wird serverseitig durchgesetzt
|
||||
path="/",
|
||||
)
|
||||
|
||||
return CurrentUser(id=row[0], username=row[1], is_admin=bool(row[2]))
|
||||
|
||||
|
||||
async def get_current_user_ws(websocket: WebSocket) -> CurrentUser | None:
|
||||
"""Wie get_current_user(), aber fuer WebSocket-Handshakes: kein Cookie-Refresh
|
||||
(WebSockets erlauben nach dem Handshake kein Set-Cookie mehr), stattdessen
|
||||
wird der Idle-Timeout beim naechsten regulaeren HTTP-Request durchgesetzt."""
|
||||
token = websocket.cookies.get(SESSION_COOKIE_NAME)
|
||||
if token is None:
|
||||
return None
|
||||
payload = decode_session_token(token)
|
||||
if payload is None or is_expired(payload):
|
||||
return None
|
||||
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"SELECT id, username, is_admin, is_active, session_version FROM users WHERE id = ?",
|
||||
(payload.user_id,),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if row is None or not row[3] or row[4] != payload.session_version:
|
||||
return None
|
||||
return CurrentUser(id=row[0], username=row[1], is_admin=bool(row[2]))
|
||||
|
||||
|
||||
async def require_global_admin(user: CurrentUser = Depends(get_current_user)) -> CurrentUser:
|
||||
if not user.is_admin:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin-Rechte erforderlich")
|
||||
return user
|
||||
|
||||
|
||||
def require_host_group_role(role_name: str):
|
||||
"""Dependency-Factory: prueft Rolle des Users fuer eine per Pfad-/Query-Param
|
||||
uebergebene host_group_id. Globale Admins duerfen immer."""
|
||||
|
||||
async def _dep(host_group_id: int, user: CurrentUser = Depends(get_current_user)) -> CurrentUser:
|
||||
if user.is_admin:
|
||||
return user
|
||||
conn = get_db()
|
||||
allowed = await user_has_role(
|
||||
conn, user_id=user.id, host_group_id=host_group_id, role_name=role_name
|
||||
)
|
||||
if not allowed:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "Keine Berechtigung fuer diese Hostgruppe")
|
||||
return user
|
||||
|
||||
return _dep
|
||||
|
||||
|
||||
def require_host_role(role_name: str):
|
||||
"""Dependency-Factory: prueft Rolle des Users fuer einen konkreten host_id."""
|
||||
|
||||
async def _dep(host_id: int, user: CurrentUser = Depends(get_current_user)) -> CurrentUser:
|
||||
if user.is_admin:
|
||||
return user
|
||||
conn = get_db()
|
||||
allowed = await user_has_role_for_host(
|
||||
conn, user_id=user.id, host_id=host_id, role_name=role_name
|
||||
)
|
||||
if not allowed:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "Keine Berechtigung fuer diesen Host")
|
||||
return user
|
||||
|
||||
return _dep
|
||||
Reference in New Issue
Block a user