feat: implement Phase 2 stabilization
Some checks failed
CI / lint-and-test (push) Has been cancelled

- Cache Graph API tokens with expiry-aware reuse in graph/auth.py
- Add tenacity-based retry/backoff wrapper (utils/http.py) and apply to all Graph/source API calls
- Add Pydantic request/response models (models/api.py) and FastAPI query constraints
- Add unit tests for event_model, auth and integration tests for API endpoints
- Configure ruff linter/formatter in pyproject.toml
- Add GitHub Actions CI pipeline (.github/workflows/ci.yml)
- Add requirements-dev.txt with pytest, mongomock, httpx, ruff
- Clean up typing imports and fix ruff linting across codebase
This commit is contained in:
2026-04-14 12:02:28 +02:00
parent 4f6e16d64d
commit 9271b4e461
29 changed files with 518 additions and 118 deletions

View File

@@ -1,19 +1,17 @@
import time
import structlog
from typing import Optional, Set
import requests
from fastapi import Depends, HTTPException, Header
from jose import jwt
from jose.jwk import construct
import structlog
from config import (
AUTH_ALLOWED_GROUPS,
AUTH_ALLOWED_ROLES,
AUTH_CLIENT_ID,
AUTH_ENABLED,
AUTH_TENANT_ID,
AUTH_CLIENT_ID,
AUTH_ALLOWED_ROLES,
AUTH_ALLOWED_GROUPS,
)
from fastapi import Header, HTTPException
from jose import jwt
from jose.jwk import construct
JWKS_CACHE = {"exp": 0, "keys": []}
logger = structlog.get_logger("aoc.auth")
@@ -35,16 +33,15 @@ def _get_jwks():
return keys
def _allowed(claims: dict, allowed_roles: Set[str], allowed_groups: Set[str]) -> bool:
def _allowed(claims: dict, allowed_roles: set[str], allowed_groups: set[str]) -> bool:
if not allowed_roles and not allowed_groups:
return True
roles = set(claims.get("roles", []) or claims.get("role", []) or [])
groups = set(claims.get("groups", []) or [])
if allowed_roles and roles.intersection(allowed_roles):
return True
if allowed_groups and groups.intersection(allowed_groups):
return True
return False
return bool(
(allowed_roles and roles.intersection(allowed_roles))
or (allowed_groups and groups.intersection(allowed_groups))
)
def _decode_token(token: str, jwks):
@@ -72,10 +69,10 @@ def _decode_token(token: str, jwks):
raise
except Exception as exc:
logger.warning("Token verification failed", error=str(exc))
raise HTTPException(status_code=401, detail="Invalid token")
raise HTTPException(status_code=401, detail="Invalid token") from None
def require_auth(authorization: Optional[str] = Header(None)):
def require_auth(authorization: str | None = Header(None)):
if not AUTH_ENABLED:
return {"sub": "anonymous"}