Applied AI Governance
16 AI services. 6 identity models. 12 OAuth scopes.
Every audit gap mapped. Every proxy hop documented.
The Problem
Different services use different identity models. Some preserve the human in audit, others show only the service principal UUID. Without a map, you can't predict what shows up in system.access.audit.
Under M2M patterns, UC audit records the SP UUID, not the human who initiated the action. Application-plane audit is the only bridge back to the human.
OAuth scopes with undocumented requirements, silent 403 failures, and UI-vs-CLI behavior differences. The genie scope isn't even shown in the account UI but is required on Azure.
Databricks Apps proxy strips tokens and injects headers. App-to-app calls add a second proxy. External MCP adds a third. Each hop changes the identity semantics.
Complete Service Identity Map
| # | Service | Identity Model | current_user() | UC Audit Shows |
|---|---|---|---|---|
| 1 | Genie Agents (Conversation API) | True OBO | User email | User |
| 2 | AI/BI Dashboard (run-as-viewer) | True OBO | Viewer email | Viewer |
| 3 | AI/BI Dashboard (run-as-owner) | Delegated | Owner email | Owner |
| 4 | Agent Bricks / Model Serving | True OBO | User email | User |
| 5 | SQL Warehouse (user token) | True OBO | User email | User |
| 6 | SQL Warehouse (SP token) | Pure M2M | SP UUID | SP UUID |
| 7 | Custom MCP (Databricks Apps) | Proxy + M2M | SP UUID | SP UUID |
| 8 | UC Functions (via M2M SQL) | Pure M2M | SP UUID | SP UUID |
| 9 | UC Functions (via Genie Agents OBO) | True OBO | User email | User |
| 10 | Vector Search | Pure M2M | N/A | SP UUID |
| 11 | Foundation Model API | M2M / OBO | N/A | Caller identity |
| 12 | SDP / Lakeflow Pipeline | Delegated | Owner email | Owner |
| 13 | External MCP (shared bearer) | M2M at Proxy | N/A | Caller at proxy |
| 14 | External MCP (per-user OAuth) | OBO at Proxy | N/A | User at proxy |
| 15 | Custom MCP → Custom MCP | Proxy + M2M | SP UUID | SP UUID |
| 16 | Lakebase | DB → Postgres | DB email | Management ops |
Identity Models
Services: #1, 2, 4, 5, 9
User's token executes the operation. UC sees the human. Automatic audit capture. The gold standard.
Services: #7, 15
App SP executes. Human identity from X-Forwarded-Email (proxy-injected, unforgeable). UC audit shows SP UUID only.
Services: #6, 8, 10, 11
App SP executes. No user involvement at all. UC audit shows SP UUID. Per-user attribution impossible without app-plane audit.
Services: #3, 12
Owner's identity executes. UC sees the owner, not the end user. Fine for dashboards, problematic for multi-user pipelines.
Services: #13, 14
Databricks proxy authenticates. External service sees shared bearer (#13) or per-user OAuth (#14). Split audit across two systems.
Services: #16
Databricks OAuth identity mapped to Postgres role via databricks_auth. RLS enforced at database level. Data-plane query audit undocumented.
Audit Gap Matrix
| Identity Model | UC Captures Human? | App-Plane Audit | Affected Services |
|---|---|---|---|
| True OBO | Yes | Optional | #1, 2, 4, 5, 9 |
| Proxy + M2M | No, SP UUID | Required | #7, 15 |
| Pure M2M | No, SP UUID | Required | #6, 8, 10, 11 |
| Delegated | Owner only | Depends | #3, 12 |
| External MCP | At proxy | Recommended | #13, 14 |
| DB → Postgres | Mgmt ops only | Depends | #16 |
@audited decorator is the only way to close the gap.
OAuth Scopes Reference
| Scope | Grants | Required By | Gotcha |
|---|---|---|---|
openid | OIDC identity token | All OAuth flows | none |
email | Email claim in token | Proxy X-Forwarded-Email | none |
profile | Display name claims | Proxy X-Forwarded-Preferred-Username | none |
offline_access | Refresh token | Long-lived sessions | none |
dashboards.genie | Genie Agent access | Genie Conversation API | Must pair with genie on Azure |
genie | Genie Conversation API | Genie Agents (Azure) | Not shown in account UI. Required on Azure. Silent 403 without it. |
model-serving | Model Serving / Agent Bricks | Agent Bricks, FM API OBO | none |
sql | Statement Execution API | Direct SQL with user token | CLI custom-app-integration update does NOT embed scope in JWT. Use UI “User Authorization” instead. |
unity-catalog | UC REST API + External MCP proxy | External MCP via UC HTTP | Undocumented. Proxy checks this before USE CONNECTION. Without it: 403. |
all-apis | Catch-all for Databricks REST APIs | General API access | Not a superset of granular scopes |
Interactive Tool
Select the services your app uses. Get the minimum scopes required.
Select services to calculate minimum scopes.
Proxy Architecture
| Header | Trust | Content |
|---|---|---|
X-Forwarded-Email | High | Proxy-verified user identity. Unforgeable. Primary identity source in M2M patterns. |
X-Forwarded-Access-Token | High | OIDC JWT. SP-scoped but unforgeable. Used for OBO API calls (Genie Agents, Agent Bricks). |
X-Forwarded-User | High | {user_id}@{workspace_id}. Unique cross-workspace identifier. |
authorization: disabled on App B + read X-Forwarded-Email for identity. Trade-off: external clients can't use App B directly.
Service Principal Isolation
| SP | Application | Read Capabilities | Write Capabilities | Why Separate |
|---|---|---|---|---|
| SP-A | Streamlit app | Genie Agents (OBO), VS indexes, UC Functions, FM API | None | Frontend, no direct write access |
| SP-B | Custom MCP server | opportunities, quota_viewers, customers | approval_requests (INSERT) | MCP needs write; Streamlit does not |
| SP-C | Claude Code MCP | opportunities, quota_viewers, customers | approval_requests (INSERT) | Different auth setting, OAuth integration, lifecycle |
| SP-D | Agent Bricks supervisor | Sub-agents inherit user token (OBO) | Through sub-agent tools only | Platform-managed; sub-agent access = user's access |
Confused Deputy Prevention
| # | Anti-Pattern | Why Dangerous | Correct Approach |
|---|---|---|---|
| 1 | Sharing CLIENT_ID/SECRET across apps |
One compromise = all apps compromised | Each databricks apps create auto-generates a unique SP |
| 2 | ALL PRIVILEGES on a catalog |
SP can read, write, drop, grant | Object-level permissions only |
| 3 | Same SP for read and write tools | Read vulnerability becomes write exploit | Separate SPs per capability boundary |
| 4 | Re-using app OAuth integration for external clients | Scope changes affect production users | Separate OAuth integration per target |
| 5 | Adding MCP SP to admin groups | Bypasses all row filters and column masks | SP only in groups matching declared capabilities |
Stopgap Audit Solution
Drop-in decorator. Zero changes to tool logic. Silent failure, audit write errors never break the tool.
@mcp.tool()
@audited()
def get_deal_approval_status(opp_id: str) -> dict:
# ... existing logic unchanged ...
@mcp.tool()
@audited(service_type="uc_function")
def call_uc_function(func_name: str) -> dict:
# ... existing logic unchanged ...
caller_email, X-Forwarded-Emailcaller_user_id, X-Forwarded-Usercaller_display, display nameapp_sp_id, DATABRICKS_CLIENT_IDapp_name, application nameservice_type, custom_mcp | uc_function | ...tool_name, function nametool_args_hash, SHA-256 for deduptool_args_safe, sanitized argsresult_status, success | error | deniederror_message, error detailtrace_id, MLflow trace IDrequest_id, UUID per invocationevent_time, UTC timestampduration_ms, execution time
End-to-End Traceability
SP UUID + time window (±2s start, +30s end). Falls back to trace_id for precise join when MLflow tracing is available.
alert_missing_identity, empty email = broken authalert_high_frequency, >50 calls/hr per useralert_write_ops, all write operationsalert_error_rate, >10% error rate
WITH app_events AS (
SELECT request_id, caller_email, app_sp_id,
tool_name, event_time, trace_id,
event_time - INTERVAL 2 SECONDS AS w_start,
event_time + INTERVAL 30 SECONDS AS w_end
FROM catalog.audit.tool_invocations
WHERE event_time > current_timestamp() - INTERVAL 24 HOURS
),
uc_events AS (
SELECT event_time AS uc_time,
user_identity.email AS uc_executor,
action_name AS uc_action
FROM system.access.audit
WHERE service_name IN ('unityCatalog','databricksSql')
)
SELECT a.caller_email AS human,
a.tool_name AS tool,
u.uc_action AS uc_operation,
u.uc_executor AS uc_identity
FROM app_events a
LEFT JOIN uc_events u
ON u.uc_executor = a.app_sp_id
AND u.uc_time BETWEEN a.w_start AND a.w_end
ORDER BY a.event_time DESC;
Governance
A malicious user creates a function that calls an external endpoint to exfiltrate data. When a benign user triggers that function (via Genie Agents, OneChat, or a shared space), it executes under the benign user's credentials.
CREATE FUNCTION to curated schemasUSE CONNECTIONEXECUTE grants to account usersUSE CONNECTION = no exfiltration. Even a perfectly crafted malicious function has nowhere to send the data.
Honest Assessment
| Limitation | Impact | Mitigation |
|---|---|---|
| Time-window join is approximate | If two users call the same tool within 30s via the same SP, UC audit events may be attributed to the wrong user | Use trace_id for precise join when MLflow tracing is available |
| Audit table requires SP write access | The SP must have MODIFY on the audit table, an additional privilege | Dedicated audit table, not production data. Acceptable trade-off. |
| No platform enforcement of required tags | MLflow trace tags are convention-based. A developer can ship without user_email tag. |
@audited decorator makes it automatic. Code review catches gaps. |
| External MCP audit is one-sided | Records that Alice called the GitHub MCP tool but NOT what GitHub returned | Per-user OAuth: GitHub has its own audit. Shared bearer: limited to shared identity. |
| Lakebase data-plane audit undocumented | Management ops audited. SQL query audit on Lakebase tables: unknown. | Postgres pg_audit extension available. Databricks integration TBD. |
Identity Explorer
A reference for architecture reviews: which identity each service honors, where audit visibility drops, and how application-plane audit closes the gap.
Applied AI Governance · Identity & Audit Architecture Reference