Problem
PR #21 (feature/rbac-unity-catalog) assumes X-Forwarded-Access-Token is always available inside a Databricks App. In
practice, user token passthrough is disabled by default in some organizations (it requires an explicit workspace-level
setting). When disabled, X-Forwarded-Access-Token is never injected, and the entire RBAC layer breaks silently.
Impact (when token passthrough is disabled)
| Symptom |
Root Cause |
/users/me returns 401 |
extract_bearer_token raises 401 when header is absent |
Frontend sees no role → defaults to use for everyone |
Frontend never gets a valid role back |
| "Create Gateway" button never shown |
isOwner = false because role resolution failed |
| Genie Spaces / Warehouses dropdowns always empty |
/workspace/genie-spaces and /workspace/warehouses call |
extract_bearer_token directly → 401 |
|
| All management endpoints (POST/PUT/DELETE gateways, settings) return 401 |
_require_role in gateway_routes.py calls |
extract_bearer_token before auth check |
|
The app is completely non-functional for any management operation on these workspaces, with no visible error — just a
locked-down UI.
Root Cause
extract_bearer_token (auth_helpers.py:11) raises HTTP 401 when:
X-Forwarded-Access-Token is absent (passthrough disabled)
Authorization: Bearer is also absent (browser-initiated requests to Databricks Apps never send this)
This is called unconditionally in:
gateway_routes._require_role (line 32)
gateway_routes.list_genie_spaces, list_warehouses, list_serving_endpoints (direct call)
rbac_routes._resolve_caller (line 34)
Why SP token fallback is NOT the right fix
Commit 0052fb9b correctly removed the service principal token fallback. Using a SP token to call SCIM /Me on behalf of
users is architecturally wrong — the SP token always resolves to the SP's own identity/groups, not the user's, making the admin
check meaningless.
Proposed Fix
When X-Forwarded-Access-Token is absent but X-Forwarded-Email is present (user authenticated via Databricks Apps SSO), the
correct behavior is graceful degradation:
- Skip the SCIM admin check — can't verify workspace admin without the user's token. Already safe:
is_workspace_admin
returns False when token is empty (rbac.py:54).
- Still do the DB role lookup —
user_roles table lookup by email works without a token.
- Default to
use if no explicit DB assignment (current behavior).
- Workspace discovery endpoints — fall back to ambient app credentials so Genie Spaces/Warehouses dropdowns populate
correctly.
rbac_routes._resolve_caller:
async def _resolve_caller(req: Request):
identity = req.headers.get("X-Forwarded-Email", "")
try:
token = extract_bearer_token(req)
except HTTPException as e:
if e.status_code == 401 and identity:
# Token passthrough disabled — user is SSO-authenticated but no token injected.
# SCIM admin check is skipped; DB role lookup still works by email.
role = await resolve_role(identity, "", _get_host())
return identity, "", role
return "", "", "use"
role = await resolve_role(identity, token, _get_host())
return identity, token, role
gateway_routes._require_role:
async def _require_role(req: Request, min_role: str):
identity = req.headers.get("X-Forwarded-Email", "")
try:
token = extract_bearer_token(req)
except HTTPException as e:
if e.status_code == 401 and identity:
token = "" # No passthrough token; SCIM admin check will be skipped
else:
raise
role = await resolve_role(identity, token, _get_host())
if not role_gte(role, min_role):
raise HTTPException(status_code=403, detail=f"Role '{min_role}' required. You have '{role}'.")
Bootstrap: first-time admin assignment
With this fix, the app deployer (who may not have token passthrough) can bootstrap owner access by:
- Calling POST /api/admin/users/{email}/role directly with Authorization: Bearer <personal-access-token> (e.g., via the
built-in API docs at /docs).
- Or: adding BOOTSTRAP_ADMIN_EMAIL env var support — auto-assign owner on first startup if no users exist in DB.
How to Reproduce
1. Deploy this app to a workspace where user token passthrough is disabled (Settings → Apps → User token passthrough = OFF).
2. Open the app URL in a browser.
3. Observe: UI shows "No gateways yet" with no Create button. DevTools → Network → GET /api/admin/users/me returns 401.
Problem
PR #21 (
feature/rbac-unity-catalog) assumesX-Forwarded-Access-Tokenis always available inside a Databricks App. Inpractice, user token passthrough is disabled by default in some organizations (it requires an explicit workspace-level
setting). When disabled,
X-Forwarded-Access-Tokenis never injected, and the entire RBAC layer breaks silently.Impact (when token passthrough is disabled)
/users/mereturns 401extract_bearer_tokenraises 401 when header is absentusefor everyoneisOwner = falsebecause role resolution failed/workspace/genie-spacesand/workspace/warehousescallextract_bearer_tokendirectly → 401_require_roleingateway_routes.pycallsextract_bearer_tokenbefore auth checkThe app is completely non-functional for any management operation on these workspaces, with no visible error — just a
locked-down UI.
Root Cause
extract_bearer_token(auth_helpers.py:11) raises HTTP 401 when:X-Forwarded-Access-Tokenis absent (passthrough disabled)Authorization: Beareris also absent (browser-initiated requests to Databricks Apps never send this)This is called unconditionally in:
gateway_routes._require_role(line 32)gateway_routes.list_genie_spaces,list_warehouses,list_serving_endpoints(direct call)rbac_routes._resolve_caller(line 34)Why SP token fallback is NOT the right fix
Commit
0052fb9bcorrectly removed the service principal token fallback. Using a SP token to callSCIM /Meon behalf ofusers is architecturally wrong — the SP token always resolves to the SP's own identity/groups, not the user's, making the admin
check meaningless.
Proposed Fix
When
X-Forwarded-Access-Tokenis absent butX-Forwarded-Emailis present (user authenticated via Databricks Apps SSO), thecorrect behavior is graceful degradation:
is_workspace_adminreturns
Falsewhen token is empty (rbac.py:54).user_rolestable lookup by email works without a token.useif no explicit DB assignment (current behavior).correctly.
rbac_routes._resolve_caller: