Skip to content

Commit 42e958b

Browse files
zacdav-dbzacdav
andauthored
Fix OAuth isolation across workspaces (#255) (#256)
Co-authored-by: Zac Davies <zaclavis@gmail.com>
1 parent 12e2cf7 commit 42e958b

5 files changed

Lines changed: 136 additions & 24 deletions

File tree

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# brickster (development version)
22

3+
- Fixed OAuth U2M and M2M authentication across multiple Databricks workspaces in one R session by isolating OAuth clients and cached tokens per workspace (#255)
34
- Fixed `dbWriteTable()` and `dbAppendTable()` standard-path writes for binary columns, which now use Databricks `BINARY` types and `X'...'` literals when no staging volume is configured (#245)
45
- `dbConnect()` now preserves Databricks API error details when its validation query fails
56
- `db_perform_request()` and `db_perform_response()` now preserve useful error messages when Databricks returns non-JSON error bodies such as empty, plain-text, or HTML responses (#242)

R/package-auth.R

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -462,20 +462,45 @@ resolve_oauth_auth_mode <- function(
462462
"oauth-u2m"
463463
}
464464

465+
db_oauth_client_cache_name <- function(
466+
auth_mode,
467+
host,
468+
client_id,
469+
token_url,
470+
scope
471+
) {
472+
cache_context <- list(
473+
auth_mode = auth_mode,
474+
host = tolower(host),
475+
client_id = client_id,
476+
token_url = token_url,
477+
scope = scope
478+
)
479+
480+
paste0("brickster-", auth_mode, "-", rlang::hash(cache_context))
481+
}
482+
465483
build_databricks_m2m_oauth_client <- function(host, client_id, client_secret) {
466484
endpoints <- databricks_workspace_oauth_endpoints(host)
485+
scope <- "all-apis"
467486

468487
list(
469488
client = httr2::oauth_client(
470489
id = client_id,
471490
secret = client_secret,
472491
token_url = endpoints$token_url,
473-
name = "brickster"
492+
name = db_oauth_client_cache_name(
493+
auth_mode = "oauth-m2m",
494+
host = host,
495+
client_id = client_id,
496+
token_url = endpoints$token_url,
497+
scope = scope
498+
)
474499
),
475500
auth_url = endpoints$auth_url,
476501
auth_mode = "oauth-m2m",
477502
is_m2m = TRUE,
478-
scope = "all-apis",
503+
scope = scope,
479504
token_params = list()
480505
)
481506
}
@@ -488,40 +513,58 @@ databricks_workspace_oauth_endpoints <- function(host) {
488513
}
489514

490515
build_azure_m2m_oauth_client <- function(
516+
host,
491517
azure_client_id,
492518
azure_client_secret,
493519
azure_tenant_id
494520
) {
521+
token_url <- glue::glue(
522+
"https://login.microsoftonline.com/{azure_tenant_id}/oauth2/v2.0/token"
523+
)
524+
scope <- "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default"
525+
495526
list(
496527
client = httr2::oauth_client(
497528
id = azure_client_id,
498529
secret = azure_client_secret,
499-
token_url = glue::glue(
500-
"https://login.microsoftonline.com/{azure_tenant_id}/oauth2/v2.0/token"
501-
),
502-
name = "brickster"
530+
token_url = token_url,
531+
name = db_oauth_client_cache_name(
532+
auth_mode = "azure-client-secret",
533+
host = host,
534+
client_id = azure_client_id,
535+
token_url = token_url,
536+
scope = scope
537+
)
503538
),
504539
auth_url = NULL,
505540
auth_mode = "azure-client-secret",
506541
is_m2m = TRUE,
507-
scope = "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default",
542+
scope = scope,
508543
token_params = list()
509544
)
510545
}
511546

512547
build_databricks_u2m_oauth_client <- function(host) {
513548
endpoints <- databricks_workspace_oauth_endpoints(host)
549+
client_id <- "databricks-cli"
550+
scope <- "all-apis"
514551

515552
list(
516553
client = httr2::oauth_client(
517-
id = "databricks-cli",
554+
id = client_id,
518555
token_url = endpoints$token_url,
519-
name = "brickster"
556+
name = db_oauth_client_cache_name(
557+
auth_mode = "oauth-u2m",
558+
host = host,
559+
client_id = client_id,
560+
token_url = endpoints$token_url,
561+
scope = scope
562+
)
520563
),
521564
auth_url = endpoints$auth_url,
522565
auth_mode = "oauth-u2m",
523566
is_m2m = FALSE,
524-
scope = "all-apis",
567+
scope = scope,
525568
token_params = list()
526569
)
527570
}
@@ -581,6 +624,7 @@ db_oauth_client <- function(
581624
)
582625
} else if (identical(auth_mode, "azure-client-secret")) {
583626
client_and_auth <- build_azure_m2m_oauth_client(
627+
host,
584628
azure_client_id,
585629
azure_client_secret,
586630
azure_tenant_id
@@ -589,9 +633,6 @@ db_oauth_client <- function(
589633
client_and_auth <- build_databricks_u2m_oauth_client(host)
590634
}
591635

592-
# add option for client to be fetched via request helpers
593-
options(brickster_oauth_client = client_and_auth)
594-
595636
client_and_auth
596637
}
597638

R/request-helpers.R

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,7 @@ db_request <- function(
4747
if (!is.null(token)) {
4848
req <- httr2::req_auth_bearer_token(req = req, token = token)
4949
} else {
50-
# fetch client
51-
oauth_client <- getOption(
52-
x = "brickster_oauth_client",
53-
db_oauth_client(host = host)
54-
)
50+
oauth_client <- db_oauth_client(host = host)
5551

5652
if (oauth_client$is_m2m) {
5753
req <- httr2::req_oauth_client_credentials(

tests/testthat/test-auth.R

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,40 @@ local_clear_auth_env <- function() {
1010
withr::local_options(
1111
use_databrickscfg = FALSE,
1212
db_profile = NULL,
13-
brickster_oauth_client = NULL,
1413
.local_envir = parent.frame()
1514
)
1615
}
1716

17+
test_that("OAuth client cache identities are stable and workspace-specific", {
18+
local_clear_auth_env()
19+
20+
u2m_a <- db_oauth_client(
21+
host = "workspace-a.cloud.databricks.com",
22+
client_id = NULL,
23+
client_secret = NULL,
24+
auth_type = "oauth-u2m"
25+
)
26+
u2m_a_again <- db_oauth_client(
27+
host = "workspace-a.cloud.databricks.com",
28+
client_id = NULL,
29+
client_secret = NULL,
30+
auth_type = "oauth-u2m"
31+
)
32+
u2m_b <- db_oauth_client(
33+
host = "workspace-b.cloud.databricks.com",
34+
client_id = NULL,
35+
client_secret = NULL,
36+
auth_type = "oauth-u2m"
37+
)
38+
39+
expect_identical(u2m_a$client$name, u2m_a_again$client$name)
40+
expect_false(identical(u2m_a$client$name, u2m_b$client$name))
41+
expect_identical(
42+
u2m_b$auth_url,
43+
"https://workspace-b.cloud.databricks.com/oidc/v1/authorize"
44+
)
45+
})
46+
1847
test_that("auth functions - baseline behaviour", {
1948
local_clear_auth_env()
2049

tests/testthat/test-request-helpers.R

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ local_clear_auth_env <- function() {
1010
withr::local_options(
1111
use_databrickscfg = FALSE,
1212
db_profile = NULL,
13-
brickster_oauth_client = NULL,
1413
.local_envir = parent.frame()
1514
)
1615
}
@@ -111,8 +110,6 @@ test_that("request helpers - m2m auth flow", {
111110
DATABRICKS_CLIENT_ID = "client-id",
112111
DATABRICKS_CLIENT_SECRET = "client-secret"
113112
)
114-
withr::local_options(brickster_oauth_client = NULL)
115-
116113
req <- db_request(
117114
endpoint = endpoint,
118115
method = method,
@@ -136,6 +133,56 @@ test_that("request helpers - m2m auth flow", {
136133
)
137134
})
138135

136+
test_that("request helpers isolate OAuth clients and tokens by workspace", {
137+
local_clear_auth_env()
138+
139+
withr::local_envvar(
140+
DATABRICKS_CLIENT_ID = "client-id",
141+
DATABRICKS_CLIENT_SECRET = "client-secret"
142+
)
143+
144+
req_a <- db_request(
145+
endpoint = "clusters/list",
146+
method = "GET",
147+
version = "2.0",
148+
host = "workspace-a.cloud.databricks.com",
149+
token = NULL
150+
)
151+
req_b <- db_request(
152+
endpoint = "clusters/list",
153+
method = "GET",
154+
version = "2.0",
155+
host = "workspace-b.cloud.databricks.com",
156+
token = NULL
157+
)
158+
159+
client_a <- req_a$policies$auth_sign$params$flow_params$client
160+
client_b <- req_b$policies$auth_sign$params$flow_params$client
161+
expect_identical(
162+
client_a$token_url,
163+
"https://workspace-a.cloud.databricks.com/oidc/v1/token"
164+
)
165+
expect_identical(
166+
client_b$token_url,
167+
"https://workspace-b.cloud.databricks.com/oidc/v1/token"
168+
)
169+
expect_false(identical(client_a$name, client_b$name))
170+
171+
cache_a <- req_a$policies$auth_sign$cache
172+
cache_b <- req_b$policies$auth_sign$cache
173+
cache_a$clear()
174+
cache_b$clear()
175+
withr::defer(cache_a$clear())
176+
withr::defer(cache_b$clear())
177+
178+
cache_a$set(httr2::oauth_token(
179+
access_token = "workspace-a-token",
180+
expires_in = 3600
181+
))
182+
183+
expect_null(cache_b$get())
184+
})
185+
139186
test_that("request helpers - azure m2m auth flow", {
140187
local_clear_auth_env()
141188

@@ -150,8 +197,6 @@ test_that("request helpers - azure m2m auth flow", {
150197
ARM_CLIENT_SECRET = "azure-client-secret",
151198
ARM_TENANT_ID = "azure-tenant-id"
152199
)
153-
withr::local_options(brickster_oauth_client = NULL)
154-
155200
req <- db_request(
156201
endpoint = endpoint,
157202
method = method,

0 commit comments

Comments
 (0)