Skip to content

Commit 079e60f

Browse files
committed
Add SP/OBO auth modes for Databricks App, fix topbar bleed-through, enhance deploy scripts
- Add Service Principal and On-Behalf-of User auth modes when running as Databricks App - SP auth uses SDK Config with client credentials; OBO reads X-Forwarded-Access-Token header - Fix topbar background bleed-through in iframe by using opaque colors and removing backdrop-filter - Deploy scripts now: configure OBO scopes via account API, upload app thumbnail, and optionally add SP to admins group - Ensure https:// scheme on DATABRICKS_HOST env var Co-authored-by: Isaac
1 parent 4cc287d commit 079e60f

6 files changed

Lines changed: 410 additions & 36 deletions

File tree

app.py

Lines changed: 114 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
get_current_user_info,
5252
get_host,
5353
get_metastore_name,
54+
get_sp_token,
5455
get_workspace_name,
5556
make_api_call,
5657
resolve_account_connection,
@@ -87,7 +88,7 @@
8788
</html>'''
8889

8990
# Default connection config
90-
_DEFAULT_CONN = {"mode": "profile", "profile": DATABRICKS_PROFILE}
91+
_DEFAULT_CONN = {"mode": "sp"} if IS_DATABRICKS_APP else {"mode": "profile", "profile": DATABRICKS_PROFILE}
9192

9293

9394
# ── Flask route: serve ~/.databrickscfg in the browser ────────────────────────
@@ -724,7 +725,13 @@ def _resolve_conn(
724725
A ``(host, token)`` tuple.
725726
"""
726727
if IS_DATABRICKS_APP:
727-
return get_host(), flask_request.headers.get("x-forwarded-access-token")
728+
mode = (conn_config or {}).get("mode", "sp")
729+
if mode == "obo":
730+
# OBO: read the user's token from the proxy header (requires
731+
# User Authorization enabled + scopes configured in the app)
732+
return get_host(), flask_request.headers.get("X-Forwarded-Access-Token")
733+
# Default: Service Principal
734+
return get_host(), get_sp_token()
728735
return resolve_local_connection(conn_config or _DEFAULT_CONN)
729736

730737

@@ -1053,55 +1060,55 @@ def build_sidebar() -> html.Div:
10531060
_THEMES = {
10541061
"midnight": {"label": "Midnight", "dark": True, "icon": "bi-moon-stars-fill",
10551062
"bg-void": "#050810", "bg-dark": "#080c18", "bg-panel": "#0d1225",
1056-
"bg-surface": "#141a2e", "bg-topbar": "rgba(8,12,24,0.95)",
1063+
"bg-surface": "#141a2e", "bg-topbar": "#080c18",
10571064
"bg-card": "rgba(255,255,255,0.028)", "bg-hover": "rgba(255,255,255,0.055)",
10581065
"bg-active": "rgba(0,212,255,0.09)", "border": "rgba(255,255,255,0.07)",
10591066
"border-hi": "rgba(0,212,255,0.45)", "text-hi": "#f8fafc",
10601067
"text-1": "#e2e8f0", "text-2": "#94a3b8", "text-3": "#4b5563",
10611068
"accent": "#00d4ff", "card-bg": "#1a1d23"},
10621069
"obsidian": {"label": "Obsidian", "dark": True, "icon": "bi-gem",
10631070
"bg-void": "#0a0a0a", "bg-dark": "#111111", "bg-panel": "#181818",
1064-
"bg-surface": "#222222", "bg-topbar": "rgba(10,10,10,0.95)",
1071+
"bg-surface": "#222222", "bg-topbar": "#0a0a0a",
10651072
"bg-card": "rgba(255,255,255,0.035)", "bg-hover": "rgba(255,255,255,0.06)",
10661073
"bg-active": "rgba(168,85,247,0.1)", "border": "rgba(255,255,255,0.08)",
10671074
"border-hi": "rgba(168,85,247,0.5)", "text-hi": "#fafafa",
10681075
"text-1": "#e0e0e0", "text-2": "#9e9e9e", "text-3": "#555555",
10691076
"accent": "#a855f7", "card-bg": "#1e1e1e"},
10701077
"deep-ocean": {"label": "Deep Ocean", "dark": True, "icon": "bi-water",
10711078
"bg-void": "#020617", "bg-dark": "#0f172a", "bg-panel": "#1e293b",
1072-
"bg-surface": "#263448", "bg-topbar": "rgba(2,6,23,0.95)",
1079+
"bg-surface": "#263448", "bg-topbar": "#020617",
10731080
"bg-card": "rgba(255,255,255,0.03)", "bg-hover": "rgba(255,255,255,0.05)",
10741081
"bg-active": "rgba(56,189,248,0.1)", "border": "rgba(255,255,255,0.06)",
10751082
"border-hi": "rgba(56,189,248,0.5)", "text-hi": "#f8fafc",
10761083
"text-1": "#cbd5e1", "text-2": "#64748b", "text-3": "#475569",
10771084
"accent": "#38bdf8", "card-bg": "#1e293b"},
10781085
"aurora": {"label": "Aurora", "dark": True, "icon": "bi-stars",
10791086
"bg-void": "#030712", "bg-dark": "#0c1427", "bg-panel": "#162032",
1080-
"bg-surface": "#1e2a3e", "bg-topbar": "rgba(3,7,18,0.95)",
1087+
"bg-surface": "#1e2a3e", "bg-topbar": "#030712",
10811088
"bg-card": "rgba(255,255,255,0.03)", "bg-hover": "rgba(255,255,255,0.055)",
10821089
"bg-active": "rgba(16,185,129,0.1)", "border": "rgba(255,255,255,0.07)",
10831090
"border-hi": "rgba(16,185,129,0.5)", "text-hi": "#ecfdf5",
10841091
"text-1": "#d1fae5", "text-2": "#6ee7b7", "text-3": "#34d399",
10851092
"accent": "#10b981", "card-bg": "#1a2332"},
10861093
"snowlight": {"label": "Snowlight", "dark": False, "icon": "bi-sun-fill",
10871094
"bg-void": "#f8fafc", "bg-dark": "#f1f5f9", "bg-panel": "#ffffff",
1088-
"bg-surface": "#eef2f7", "bg-topbar": "rgba(255,255,255,0.92)",
1095+
"bg-surface": "#eef2f7", "bg-topbar": "#ffffff",
10891096
"bg-card": "rgba(0,0,0,0.03)", "bg-hover": "rgba(0,0,0,0.05)",
10901097
"bg-active": "rgba(99,102,241,0.08)", "border": "rgba(0,0,0,0.1)",
10911098
"border-hi": "rgba(99,102,241,0.5)", "text-hi": "#0f172a",
10921099
"text-1": "#1e293b", "text-2": "#64748b", "text-3": "#94a3b8",
10931100
"accent": "#6366f1", "card-bg": "#ffffff"},
10941101
"paper": {"label": "Paper", "dark": False, "icon": "bi-file-earmark-text",
10951102
"bg-void": "#fafaf9", "bg-dark": "#f5f5f4", "bg-panel": "#ffffff",
1096-
"bg-surface": "#edeceb", "bg-topbar": "rgba(255,255,255,0.92)",
1103+
"bg-surface": "#edeceb", "bg-topbar": "#ffffff",
10971104
"bg-card": "rgba(0,0,0,0.025)", "bg-hover": "rgba(0,0,0,0.04)",
10981105
"bg-active": "rgba(234,88,12,0.08)", "border": "rgba(0,0,0,0.08)",
10991106
"border-hi": "rgba(234,88,12,0.45)", "text-hi": "#1c1917",
11001107
"text-1": "#292524", "text-2": "#78716c", "text-3": "#a8a29e",
11011108
"accent": "#ea580c", "card-bg": "#ffffff"},
11021109
"cloud": {"label": "Cloud", "dark": False, "icon": "bi-cloud-sun-fill",
11031110
"bg-void": "#f0f9ff", "bg-dark": "#e0f2fe", "bg-panel": "#ffffff",
1104-
"bg-surface": "#daeaf8", "bg-topbar": "rgba(255,255,255,0.92)",
1111+
"bg-surface": "#daeaf8", "bg-topbar": "#ffffff",
11051112
"bg-card": "rgba(0,0,0,0.02)", "bg-hover": "rgba(0,0,0,0.04)",
11061113
"bg-active": "rgba(14,165,233,0.08)", "border": "rgba(0,0,0,0.08)",
11071114
"border-hi": "rgba(14,165,233,0.45)", "text-hi": "#0c4a6e",
@@ -1496,17 +1503,47 @@ def _custom_section() -> html.Div:
14961503
html.Div("Connection", className="auth-section-title mb-2"),
14971504
dbc.RadioItems(
14981505
id="conn-mode-radio",
1499-
options=[
1500-
{"label": "CLI Profile", "value": "profile"},
1501-
{"label": "SSO Login", "value": "sso"},
1502-
{"label": "URL + Token", "value": "custom"},
1503-
],
1504-
value="profile",
1506+
options=(
1507+
[
1508+
{"label": "On-Behalf-of User", "value": "obo"},
1509+
{"label": "Service Principal", "value": "sp"},
1510+
] if IS_DATABRICKS_APP else [
1511+
{"label": "CLI Profile", "value": "profile"},
1512+
{"label": "SSO Login", "value": "sso"},
1513+
{"label": "URL + Token", "value": "custom"},
1514+
]
1515+
),
1516+
value="sp" if IS_DATABRICKS_APP else "profile",
15051517
inline=True,
15061518
className="conn-mode-radio mb-3",
15071519
input_class_name="conn-radio-input",
15081520
label_class_name="conn-radio-label",
15091521
),
1522+
html.Div([
1523+
html.Div([
1524+
html.I(className="bi bi-person-badge me-2 text-info"),
1525+
"Using the logged-in user's identity via the ",
1526+
html.Code("x-forwarded-access-token"),
1527+
" header.",
1528+
], className="conn-hint"),
1529+
], id="obo-section", style={"display": "none"}),
1530+
html.Div([
1531+
html.Div([
1532+
html.I(className="bi bi-robot me-2 text-warning"),
1533+
"Using the app's Service Principal credentials ",
1534+
html.Code("(DATABRICKS_CLIENT_ID / SECRET)"),
1535+
".",
1536+
], className="conn-hint"),
1537+
html.Div([
1538+
html.I(className="bi bi-exclamation-triangle me-1"),
1539+
"The Service Principal must be a member of the ",
1540+
html.Strong("admins"),
1541+
" group to access all workspace objects. ",
1542+
"Use the deploy script or add it manually via ",
1543+
html.Code("Settings → Identity and access → Groups → admins"),
1544+
".",
1545+
], className="conn-hint mt-2 text-warning", style={"fontSize": "11px"}),
1546+
], id="sp-section", style={} if IS_DATABRICKS_APP else {"display": "none"}),
15101547
_profile_section(),
15111548
_sso_section(),
15121549
_custom_section(),
@@ -1978,7 +2015,8 @@ def populate_dropdown(is_open, conn_config):
19782015

19792016
# Auth type
19802017
if IS_DATABRICKS_APP:
1981-
auth_type_display = "On-Behalf-Of (OBO)"
2018+
app_mode = (conn_config or {}).get("mode", "obo")
2019+
auth_type_display = "Service Principal (SP)" if app_mode == "sp" else "On-Behalf-Of (OBO)"
19822020
elif conn_config.get("mode") == "custom":
19832021
auth_type_display = "Personal Access Token"
19842022
else:
@@ -2027,21 +2065,26 @@ def populate_dropdown(is_open, conn_config):
20272065
profile_options,
20282066
)
20292067

2030-
# 4. Show/hide profile vs sso vs custom section
2068+
# 4. Show/hide profile vs sso vs custom vs obo vs sp section
20312069
@app.callback(
2070+
Output("obo-section", "style"),
2071+
Output("sp-section", "style"),
20322072
Output("profile-section", "style"),
20332073
Output("sso-section", "style"),
20342074
Output("custom-section", "style"),
20352075
Input("conn-mode-radio", "value"),
20362076
)
20372077
def toggle_conn_mode(mode):
2038-
"""Callback 4: Show/hide the profile, SSO, or custom connection section."""
2078+
"""Callback 4: Show/hide the active connection section."""
20392079
hide = {"display": "none"}
2040-
if mode == "profile":
2041-
return {}, hide, hide
2042-
if mode == "sso":
2043-
return hide, {}, hide
2044-
return hide, hide, {}
2080+
show = {}
2081+
return (
2082+
show if mode == "obo" else hide,
2083+
show if mode == "sp" else hide,
2084+
show if mode == "profile" else hide,
2085+
show if mode == "sso" else hide,
2086+
show if mode == "custom" else hide,
2087+
)
20452088

20462089

20472090
# 5. Show auth type hint when profile changes
@@ -2092,6 +2135,54 @@ def apply_connection(n_clicks, mode, profile, sso_host, custom_host, custom_toke
20922135
if not n_clicks:
20932136
return no_update, no_update, no_update
20942137

2138+
if mode == "obo":
2139+
# On-Behalf-Of — uses X-Forwarded-Access-Token header injected by
2140+
# Databricks Apps proxy when User Authorization is enabled and
2141+
# scopes are configured for the app.
2142+
obo_token = flask_request.headers.get("X-Forwarded-Access-Token")
2143+
if not obo_token:
2144+
return no_update, html.Div([
2145+
html.Span("No OBO token available.", className="text-danger"),
2146+
html.Br(),
2147+
html.Span(
2148+
"To enable OBO: 1) A workspace admin must enable "
2149+
"User Authorization in workspace settings. "
2150+
"2) Add scopes (e.g. 'all-apis') to this app in the "
2151+
"Databricks Apps UI → Configure → Add scope. "
2152+
"3) Restart the app.",
2153+
className="text-muted",
2154+
style={"fontSize": "11px"},
2155+
),
2156+
]), no_update
2157+
host = get_host()
2158+
r = make_api_call("GET", "/api/2.0/preview/scim/v2/Me", obo_token, host)
2159+
if r["success"]:
2160+
user_label = r["data"].get("displayName", r["data"].get("userName", "User"))
2161+
return {"mode": "obo"}, html.Span([
2162+
html.I(className="bi bi-check-circle-fill me-1 text-success"),
2163+
f"Connected as {user_label} (OBO)",
2164+
]), no_update
2165+
return no_update, html.Span(
2166+
f"OBO token present but API call failed: {r.get('error', 'unknown error')}",
2167+
className="text-danger",
2168+
), no_update
2169+
2170+
if mode == "sp":
2171+
# Service Principal — validate we can get a token
2172+
token = get_sp_token()
2173+
if not token:
2174+
return no_update, html.Span(
2175+
"Could not obtain SP token — check DATABRICKS_CLIENT_ID / SECRET env vars.",
2176+
className="text-danger",
2177+
), no_update
2178+
host = get_host()
2179+
r = make_api_call("GET", "/api/2.0/preview/scim/v2/Me", token, host)
2180+
sp_label = r["data"].get("displayName", "Service Principal") if r["success"] else "Service Principal"
2181+
return {"mode": "sp"}, html.Span([
2182+
html.I(className="bi bi-check-circle-fill me-1 text-success"),
2183+
f"Connected as {sp_label}",
2184+
]), no_update
2185+
20952186
if mode == "profile":
20962187
if not profile:
20972188
return no_update, html.Span("No profile selected.", className="text-danger"), no_update
227 KB
Loading

assets/style.css

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
--bg-dark: #080c18;
1111
--bg-panel: #0d1225;
1212
--bg-surface: #141a2e;
13-
--bg-topbar: rgba(8,12,24,0.95);
13+
--bg-topbar: #080c18;
1414
--bg-card: rgba(255,255,255,0.028);
1515
--bg-hover: rgba(255,255,255,0.055);
1616
--bg-active: rgba(0,212,255,0.09);
@@ -46,8 +46,11 @@
4646
/* ── Reset & Base ─────────────────────────────────────────────────── */
4747
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
4848

49-
body {
49+
html, body {
5050
background: var(--bg-void) !important;
51+
background-color: var(--bg-void) !important;
52+
}
53+
body {
5154
font-family: var(--font-ui);
5255
color: var(--text-1) !important;
5356
overflow: hidden;
@@ -61,12 +64,13 @@ body {
6164
height: 56px;
6265
background: var(--bg-topbar) !important;
6366
border-bottom: 1px solid var(--border) !important;
64-
backdrop-filter: blur(20px);
65-
-webkit-backdrop-filter: blur(20px);
6667
padding: 0 20px !important;
6768
flex-shrink: 0;
6869
z-index: 100;
6970
}
71+
.topbar.navbar {
72+
background-color: var(--bg-topbar) !important;
73+
}
7074

7175
.topbar .container-fluid {
7276
display: flex;

auth.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,40 @@
2828
IS_DATABRICKS_APP: bool = bool(os.getenv("DATABRICKS_CLIENT_SECRET"))
2929

3030

31+
def _ensure_host_scheme(host: str) -> str:
32+
"""Ensure the host URL has an https:// scheme."""
33+
host = host.strip().rstrip("/")
34+
if host and not host.startswith("https://"):
35+
host = "https://" + host
36+
return host
37+
38+
39+
def get_sp_token() -> Optional[str]:
40+
"""Obtain an OAuth token using the Service Principal client credentials.
41+
42+
Uses the Databricks SDK ``Config`` which reads ``DATABRICKS_HOST``,
43+
``DATABRICKS_CLIENT_ID``, and ``DATABRICKS_CLIENT_SECRET`` env vars
44+
auto-injected in Databricks Apps. The SDK handles the correct OIDC
45+
endpoint for each cloud provider (AWS, Azure, GCP).
46+
47+
Returns:
48+
The access token string, or ``None`` on failure.
49+
"""
50+
try:
51+
from databricks.sdk.core import Config
52+
cfg = Config(
53+
host=_ensure_host_scheme(os.getenv("DATABRICKS_HOST", "")),
54+
client_id=os.getenv("DATABRICKS_CLIENT_ID", ""),
55+
client_secret=os.getenv("DATABRICKS_CLIENT_SECRET", ""),
56+
)
57+
auth_val = cfg.authenticate().get("Authorization", "")
58+
return auth_val[7:] if auth_val.startswith("Bearer ") else None
59+
except Exception:
60+
return None
61+
62+
63+
64+
3165
# ── CLI Profile discovery ──────────────────────────────────────────────────────
3266

3367
def get_cli_profiles() -> List[str]:
@@ -253,7 +287,10 @@ def get_host() -> str:
253287
on failure.
254288
"""
255289
if IS_DATABRICKS_APP:
256-
return os.getenv("DATABRICKS_HOST", "").rstrip("/")
290+
host = os.getenv("DATABRICKS_HOST", "").rstrip("/")
291+
if host and not host.startswith("https://"):
292+
host = "https://" + host
293+
return host
257294
try:
258295
return (_get_local_config().host or "").rstrip("/")
259296
except Exception:

0 commit comments

Comments
 (0)