first
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Per-request credential extraction from X- headers (FastAPI dependencies).
|
||||
|
||||
The service is a STATELESS proxy: it stores no secrets. Every credential is
|
||||
supplied per request as an X- header and used only to talk to the upstream API
|
||||
(see AGENTS.md "Secrets v parametrech"). Declaring the headers as FastAPI
|
||||
``Header`` parameters makes them appear per-operation in Swagger, including the
|
||||
"Try it out" form.
|
||||
|
||||
Google Analytics (chosen model: token has precedence over service account):
|
||||
* ``X-GA-Access-Token`` - a ready OAuth2 access token; used directly as Bearer.
|
||||
* ``X-GA-Credentials`` - base64-encoded service-account JSON key; the proxy
|
||||
mints a short-lived access token from it.
|
||||
* ``X-GA-Quota-Project`` - optional billing/quota project id.
|
||||
At least one of token / credentials must be present.
|
||||
|
||||
Sklik:
|
||||
* ``X-Sklik-Token`` - the Sklik API token from account settings. The proxy
|
||||
calls ``client.loginByToken`` to obtain a session.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Header
|
||||
|
||||
from .errors import MissingCredentialsError
|
||||
|
||||
|
||||
# --- Google Analytics ---------------------------------------------------------
|
||||
@dataclass
|
||||
class GaCredentials:
|
||||
access_token: str | None
|
||||
service_account_info: dict | None
|
||||
quota_project: str | None
|
||||
|
||||
|
||||
def get_ga_credentials(
|
||||
x_ga_access_token: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GA-Access-Token",
|
||||
description="Ready OAuth2 access token used directly as a Bearer token. "
|
||||
"Takes precedence over X-GA-Credentials.",
|
||||
),
|
||||
x_ga_credentials: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GA-Credentials",
|
||||
description="Base64-encoded Google service-account JSON key. The proxy "
|
||||
"mints a short-lived access token from it (scope analytics.readonly). "
|
||||
"Used only if X-GA-Access-Token is absent.",
|
||||
),
|
||||
x_ga_quota_project: str | None = Header(
|
||||
default=None,
|
||||
alias="X-GA-Quota-Project",
|
||||
description="Optional Google Cloud project id used for quota/billing "
|
||||
"(sets the x-goog-user-project header upstream).",
|
||||
),
|
||||
) -> GaCredentials:
|
||||
"""Resolve GA credentials from headers. Token wins over service account."""
|
||||
access_token = (x_ga_access_token or "").strip() or None
|
||||
|
||||
service_account_info: dict | None = None
|
||||
raw = (x_ga_credentials or "").strip()
|
||||
if raw:
|
||||
try:
|
||||
decoded = base64.b64decode(raw, validate=True)
|
||||
except (binascii.Error, ValueError) as exc:
|
||||
raise MissingCredentialsError(
|
||||
"X-GA-Credentials is not valid base64."
|
||||
) from exc
|
||||
try:
|
||||
service_account_info = json.loads(decoded)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
raise MissingCredentialsError(
|
||||
"X-GA-Credentials does not decode to valid JSON."
|
||||
) from exc
|
||||
if not isinstance(service_account_info, dict):
|
||||
raise MissingCredentialsError(
|
||||
"X-GA-Credentials JSON must be a service-account object."
|
||||
)
|
||||
|
||||
if not access_token and service_account_info is None:
|
||||
raise MissingCredentialsError(
|
||||
"Provide either X-GA-Access-Token or X-GA-Credentials."
|
||||
)
|
||||
|
||||
return GaCredentials(
|
||||
access_token=access_token,
|
||||
service_account_info=service_account_info,
|
||||
quota_project=(x_ga_quota_project or "").strip() or None,
|
||||
)
|
||||
|
||||
|
||||
# --- Sklik --------------------------------------------------------------------
|
||||
def get_sklik_token(
|
||||
x_sklik_token: str | None = Header(
|
||||
default=None,
|
||||
alias="X-Sklik-Token",
|
||||
description="Sklik API token from Sklik account settings. The proxy uses "
|
||||
"it to obtain a session via client.loginByToken.",
|
||||
),
|
||||
) -> str:
|
||||
token = (x_sklik_token or "").strip()
|
||||
if not token:
|
||||
raise MissingCredentialsError("X-Sklik-Token header is required.")
|
||||
return token
|
||||
Reference in New Issue
Block a user