Update
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-27 23:28:43 +02:00
parent 21015757bd
commit c76a746cd7
39 changed files with 3268 additions and 519 deletions
+48 -6
View File
@@ -9,6 +9,8 @@ import threading
import time
from collections import deque
from dataclasses import dataclass
from typing import Literal, cast
from uuid import UUID
from app.core.config import Settings
@@ -17,6 +19,8 @@ from app.core.config import Settings
class AuthPrincipal:
username: str
expires_at: int
role: Literal["operator", "guest"] = "operator"
project_id: UUID | None = None
class AuthService:
@@ -93,15 +97,32 @@ class AuthService:
return username_matches and password_matches
@classmethod
def create_session_token(cls, username: str, settings: Settings, *, now: int | None = None) -> str:
def create_session_token(
cls,
username: str,
settings: Settings,
*,
role: Literal["operator", "guest"] = "operator",
project_id: UUID | None = None,
ttl_seconds: int | None = None,
now: int | None = None,
) -> str:
issued_at = int(time.time() if now is None else now)
if role == "guest" and project_id is None:
raise ValueError("Guest sessions must be scoped to a demo project")
resolved_ttl = ttl_seconds if ttl_seconds is not None else (
settings.guest_session_ttl_seconds if role == "guest" else settings.auth_session_ttl_seconds
)
payload = {
"exp": issued_at + settings.auth_session_ttl_seconds,
"exp": issued_at + resolved_ttl,
"iat": issued_at,
"jti": secrets.token_urlsafe(12),
"role": role,
"sub": username,
"v": 1,
"v": 2,
}
if project_id is not None:
payload["project_id"] = str(project_id)
encoded_payload = cls._b64_encode(
json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
)
@@ -136,14 +157,35 @@ class AuthService:
username = str(payload.get("sub") or "")
expires_at = int(payload.get("exp") or 0)
issued_at = int(payload.get("iat") or 0)
version = int(payload.get("v") or 0)
role_value = str(payload.get("role") or "operator")
current = int(time.time() if now is None else now)
if payload.get("v") != 1 or username != settings.auth_username:
if version not in {1, 2} or role_value not in {"operator", "guest"}:
return None
role = cast(Literal["operator", "guest"], role_value)
if issued_at <= 0 or issued_at > current + 60 or expires_at <= current:
return None
if expires_at - issued_at > settings.auth_session_ttl_seconds:
if role == "operator":
if username != settings.auth_username:
return None
max_ttl = settings.auth_session_ttl_seconds
project_id = None
else:
if not settings.guest_access_enabled or username != settings.guest_display_name:
return None
max_ttl = settings.guest_session_ttl_seconds
raw_project_id = payload.get("project_id")
if not raw_project_id:
return None
project_id = UUID(str(raw_project_id))
if expires_at - issued_at > max_ttl:
return None
return AuthPrincipal(username=username, expires_at=expires_at)
return AuthPrincipal(
username=username,
expires_at=expires_at,
role=role,
project_id=project_id,
)
except (TypeError, ValueError, json.JSONDecodeError, UnicodeDecodeError):
return None