"""Keycloak JWT validation for role-based response truncation. The map dashboard has two tiers: * anonymous — no token (or an invalid/expired one). Sees a truncated view. * manager — a valid Keycloak access token carrying the realm role `manager`. Sees the full data. Design rule: a MISSING or INVALID token is NOT an error — it simply means the caller is anonymous. So role_from_authorization() never raises; the worst case degrades to the anonymous tier. This keeps the public map working even when Keycloak is unreachable or a token has expired. Validation checks the signature (RS256, keys fetched from the realm JWKS with in-process caching + automatic rotation on unknown kid) and the issuer. The access token's audience is `account` (Keycloak default), so we intentionally do NOT verify `aud`; authorization is decided by the presence of the realm role. """ import os import jwt from jwt import PyJWKClient # Public realm URL — matches the `iss` Keycloak stamps into the token, so issuer # validation lines up without extra config. Overridable via env for other envs. KC_REALM_URL = os.environ.get( "KC_REALM_URL", "https://keycloak.intense-sale.ru/realms/intense-sale" ) KC_ISSUER = KC_REALM_URL JWKS_URL = f"{KC_REALM_URL}/protocol/openid-connect/certs" MANAGER_ROLE = "manager" # PyJWKClient caches signing keys and re-fetches when it sees an unknown `kid` # (key rotation), so we build it once at import time. _jwk_client = PyJWKClient(JWKS_URL) def role_from_authorization(authorization: str | None) -> str: """Return "manager" for a valid token with the manager role, else "anonymous". Never raises: any problem (no header, wrong scheme, bad signature, expired, JWKS fetch failure) falls through to "anonymous". """ if not authorization or not authorization.lower().startswith("bearer "): return "anonymous" token = authorization[7:].strip() try: signing_key = _jwk_client.get_signing_key_from_jwt(token) claims = jwt.decode( token, signing_key.key, algorithms=["RS256"], issuer=KC_ISSUER, options={"verify_aud": False}, ) except Exception: return "anonymous" roles = claims.get("realm_access", {}).get("roles", []) or [] return MANAGER_ROLE if MANAGER_ROLE in roles else "anonymous"