Skip to main content

hc-obo-auth

Wire OBO (On-Behalf-Of) authentication into a service or BFF route. Use when the user asks to "add a route", "wire OBO", "set up authentication", "connect to Lakebase as the user", "extract user identity", or anything about user-passthrough auth. Enforces the canonical pattern where every Postgres connection is opened with the calling user's OAuth credential and Unity Catalog enforces access at the data layer.

معلومات المصدر

المستودع
databricks-solutions/microbricks
آخر نشاط في المصدر
٦ يونيو ٢٠٢٦ في ١٥:١٤
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٤
التفرعات
٣

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
2 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
hc-obo-auth
description
Wire OBO (On-Behalf-Of) authentication into a service or BFF route. Use when the user asks to "add a route", "wire OBO", "set up authentication", "connect to Lakebase as the user", "extract user identity", or anything about user-passthrough auth. Enforces the canonical pattern where every Postgres connection is opened with the calling user's OAuth credential and Unity Catalog enforces access at the data layer.
# hc-obo-auth The non-negotiable auth rule in this architecture: **every Postgres session opens with the calling user's OAuth credential.** No service-principal fallback in production code, no shortcuts. This skill is the source of truth for the canonical patterns. Any deviation needs to be argued for in a PR description. ## When to use - "Add a new route to `patient-svc` that lists allergies" - "Wire OBO into the BFF client for `lab-svc`" - "How do I connect to Lakebase as the user?" - "Set up `db.py` for the new service" - The user is touching anything that crosses the trust boundary (HTTP route, DB connection, downstream service call) ## When NOT to use - The user is writing pure unit tests against a mock — OBO doesn't apply. - The user is writing a one-off ETL script — script auth is its own thing, use a service principal there but document it as outside the OBO trust path. ## The four pillars 1. **Token in.** Every backend route reads `X-Forwarded-Access-Token` from the request — Databricks Apps injects this on every call. 2. **Token to UC.** When the service needs UC operations, it builds `WorkspaceClient(token=user_token)`, never `WorkspaceClient()` without a token in production. 3. **Token to Lakebase.** When the service connects to Postgres, it calls `WorkspaceClient(token=user_token, auth_type="pat").postgres.generate_database_credential(endpoint=ENDPOINT_NAME)` and uses the returned token as the Postgres password. The `auth_type="pat"` is required when the code runs inside a Databricks App: the platform auto-injects `DATABRICKS_CLIENT_ID` + `DATABRICKS_CLIENT_SECRET` for the app's service principal, and the SDK refuses ambiguous oauth+pat config unless one method is forced. Connections are pooled with `max_lifetime=2700` (45 min) so a stale token never lives longer than 1h. 4. **Token forward.** When the BFF or any service calls another service, it copies the user token into both `Authorization: Bearer ...` and `X-Forwarded-Access-Token` headers. The downstream service treats it as if the user called directly. The detailed canonical code (drop-in copies for every service) lives in [`references/canonical-patterns.md`](references/canonical-patterns.md). ## Quick patterns ### Extracting the user token in a FastAPI route ```python from fastapi import Header, HTTPException async def user_token( x_forwarded_access_token: str | None = Header(default=None, alias="X-Forwarded-Access-Token"), ) -> str: if not x_forwarded_access_token: raise HTTPException(401, "Missing X-Forwarded-Access-Token") return x_forwarded_access_token ``` Use it as a dependency: ```python @router.get("/patients", response_model=list[PatientOut], operation_id="listPatients") async def list_patients(token: str = Depends(user_token)): async with patient_db_pool(token).connection() as conn: ... ``` ### Building a per-request DB pool Don't build one global pool. Build one *per user token*, cached briefly. The full helper is in [`references/canonical-patterns.md`](references/canonical-patterns.md), but the shape: ```python from psycopg_pool import AsyncConnectionPool import psycopg from databricks.sdk import WorkspaceClient import os, asyncio _pools: dict[str, AsyncConnectionPool] = {} _lock = asyncio.Lock() class OAuthConnection(psycopg.AsyncConnection): @classmethod async def connect(cls, conninfo: str = "", **kwargs): token = kwargs.pop("_user_token", None) if not token: raise RuntimeError("OAuthConnection requires _user_token kwarg") # auth_type="pat" disambiguates from the SP credentials Apps auto-injects. ws = WorkspaceClient(token=token, auth_type="pat") cred = ws.postgres.generate_database_credential(endpoint=os.environ["ENDPOINT_NAME"]) kwargs["password"] = cred.token return await super().connect(conninfo, **kwargs) def patient_db_pool(user_token: str) -> AsyncConnectionPool: # Cache pools per token (short-lived). In production you'd cache by token's iss+sub claim # to avoid leaking memory if many distinct tokens hit the service. if user_token not in _pools: host = os.environ["PGHOST"] user = os.environ["PGUSER"] _pools[user_token] = AsyncConnectionPool( conninfo=f"host={host} port=5432 dbname=databricks_postgres user={user} sslmode=require", connection_class=OAuthConnection, max_lifetime=2700, kwargs={"_user_token": user_token}, open=False, ) return _pools[user_token] ``` > ⚠️ The `_pools[token]` cache leaks if many distinct tokens hit the service. The full pattern in `references/` uses an LRU keyed by `hash(token)` with a TTL. ### Forwarding the token from the BFF URL resolution: explicit `<SVC>_SVC_URL` override → runtime-derived from `DATABRICKS_APP_URL`. See `references/canonical-patterns.md` for the full `_BaseSvcClient` shape. ```python class PatientClient(_BaseSvcClient): def __init__(self, user_token: str): super().__init__(user_token, service_slug="patient") ``` The BFF route extracts the token the same way as services and constructs one client per request: ```python @router.get("/patient-summary/{patient_id}", operation_id="getPatientSummary") async def patient_summary(patient_id: str, token: str = Depends(user_token)): async with PatientClient(token) as p, AppointmentClient(token) as a: patient, appts = await asyncio.gather( p.get(patient_id), a.list_for_patient(patient_id, limit=3), ) return {"patient": patient, "appointments": appts} ``` ## Local development When running `apx dev start` outside Databricks Apps, there's no `X-Forwarded-Access-Token` injected. Two options, both supported: 1. **Pass-through from the dev's CLI auth.** The `apx` lakebase addon can inject the dev's token into the env so requests pretend to be OBO. The dev's identity is then used for Lakebase. Set `LOCAL_DEV_TOKEN_FROM_CLI=true` in `.env` and the canonical pattern reads it. 2. **Explicit header.** When testing with `curl`, include `-H "X-Forwarded-Access-Token: $(databricks auth token -p hc-dev | jq -r .access_token)"`. What the canonical pattern **does not** support: a service-principal fallback when running locally. Local dev still uses the developer's user identity to talk to Lakebase. This keeps prod-vs-dev behavior identical and prevents "works on my machine, fails in prod" auth bugs. ## Anti-patterns to reject in code review | Pattern | Why it's wrong | |---|---| | `WorkspaceClient()` (no token) in route handlers | Falls back to the app's service principal, defeats OBO | | Storing a connection pool keyed only on env (one global pool) | All users share one connection → all queries run as the SP | | Reading the token from a query param or cookie instead of the header | The header is what Databricks Apps injects; anything else is a different trust path | | Forwarding the token but stripping `Authorization` and only sending `X-Forwarded-Access-Token` | Some downstream libraries only check `Authorization` — send both | | Caching `generate_database_credential` results across requests | Tokens are user-bound — caching them across users leaks identity | ## Verification After wiring OBO into a route: 1. `curl -H "X-Forwarded-Access-Token: <user-1-token>" .../patients/me` returns user-1's data. 2. `curl -H "X-Forwarded-Access-Token: <user-2-token>" .../patients/me` returns user-2's data, not user-1's. 3. `curl .../patients/me` (no header) returns 401. 4. In Lakebase, `SELECT current_user;` from the route returns the calling user's email, not the app's service principal email. Add a debug route that echoes `current_user` to confirm during initial wiring. ## Pointer Full canonical code (`db.py`, `auth.py`, BFF client) is in [`references/canonical-patterns.md`](references/canonical-patterns.md). Copy from there into the service; do not invent new shapes.
عرض على GitHub