dmslanes/lanes
Lanes - team task boards with roles, history and an MCP endpoint for Claude and ChatGPT
A remix template · 0 remixes · Remix dmslanes/lanes
app/infrastructure/identity.py
"""Who is calling, from the headers the Dodil Ignite gateway injects.
With a user pool attached, the gateway signs the person in (browser or an AI
client over OAuth), strips any inbound copy of these headers and adds:
X-Dodil-User {"sub", "email", "connection", "app_roles"} (JSON)
So the app ships no login code: it reads the email and lets the use cases decide
what that person may see and do. One parser for both doors (web and MCP).
Local development without the gateway: DEV_ALLOW_ANON=1 makes every request
DEV_USER_EMAIL, and an `X-Dev-User: someone@x.io` header switches person (to try
the roles). Neither works with DEV_ALLOW_ANON unset, which is the deployed default.
"""
from __future__ import annotations
import json
import os
USER_HEADER = "x-dodil-user"
DEV_HEADER = "x-dev-user"
class Unauthenticated(Exception):
pass
def dev_mode() -> bool:
return os.environ.get("DEV_ALLOW_ANON", "").strip().lower() in {"1", "true", "yes", "on"}
def email_from_headers(headers) -> str:
h = {str(k).lower(): v for k, v in (headers or {}).items()}
raw = h.get(USER_HEADER)
if raw:
try:
claims = json.loads(raw)
except ValueError:
raise Unauthenticated("malformed identity header") from None
email = (claims.get("email") or "").strip().lower()
if not email:
raise Unauthenticated("the signed-in identity carries no email")
return email
if dev_mode():
return (h.get(DEV_HEADER) or os.environ.get("DEV_USER_EMAIL", "dev@localhost.test")).strip().lower()
raise Unauthenticated("sign in first")