Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions acceptance/cmd/api/workspace-id-from-w-query/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions acceptance/cmd/api/workspace-id-from-w-query/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{}

>>> print_requests.py --get //api/2.0/clusters/list
{
"headers": {
"Authorization": [
"Bearer [DATABRICKS_TOKEN]"
],
"User-Agent": [
"cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat"
],
"X-Databricks-Workspace-Id": [
"999"
]
},
"method": "GET",
"path": "/api/2.0/clusters/list",
"q": {
"w": "999"
}
}
2 changes: 2 additions & 0 deletions acceptance/cmd/api/workspace-id-from-w-query/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MSYS_NO_PATHCONV=1 $CLI api get "/api/2.0/clusters/list?w=999"
trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Workspace-Id" "999"
37 changes: 24 additions & 13 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ const (
// header for rollback safety.
workspaceIDHeader = "X-Databricks-Workspace-Id"

// orgIDQueryParam is the SPOG (single-page-of-glass) URL convention used
// by the Databricks UI: "?o=<workspace-id>" identifies the workspace a URL
// targets. When present on the path, we treat it as a per-call override
// for the workspace routing identifier so that pasted SPOG URLs route
// correctly without requiring --workspace-id.
orgIDQueryParam = "o"
// orgIDQueryParam and workspaceIDQueryParam are the SPOG
// (single-page-of-glass) URL convention used by the Databricks UI:
// "?o=<workspace-id>" or "?w=<workspace-id>" identifies the workspace a
// URL targets. When present on the path, we treat it as a per-call
// override for the workspace routing identifier so that pasted SPOG URLs
// route correctly without requiring --workspace-id. "w" is the new
// spelling that matches the X-Databricks-Workspace-Id header; "o" stays
// accepted for URLs already pasted from older UI builds, shell history,
// or committed databricks.yml files. "o" takes precedence when both are
// present to preserve the meaning of existing URLs.
orgIDQueryParam = "o"
workspaceIDQueryParam = "w"
)

// accountSegmentRe matches a non-empty segment immediately after "accounts/",
Expand Down Expand Up @@ -165,14 +171,19 @@ func hasAccountSegment(rawPath string) (bool, error) {
return accountSegmentRe.MatchString(p), nil
}

// extractOrgIDFromQuery returns the value of the "o" query parameter on path
// (the SPOG URL convention), or "" if absent or empty.
func extractOrgIDFromQuery(rawPath string) (string, error) {
// extractWorkspaceIDFromQuery returns the workspace ID encoded in the path's
// query string (the SPOG URL convention). It checks "o" first, then "w";
// returns "" if neither is present or non-empty.
func extractWorkspaceIDFromQuery(rawPath string) (string, error) {
u, err := url.Parse(rawPath)
if err != nil {
return "", fmt.Errorf("parse path: %w", err)
}
return u.Query().Get(orgIDQueryParam), nil
q := u.Query()
if v := q.Get(orgIDQueryParam); v != "" {
return v, nil
}
return q.Get(workspaceIDQueryParam), nil
}

// resolveOrgID picks the value (if any) for the workspace routing identifier
Expand All @@ -197,12 +208,12 @@ func resolveOrgID(
}
return workspaceIDFlag, nil
}
orgIDFromQuery, err := extractOrgIDFromQuery(path)
workspaceIDFromQuery, err := extractWorkspaceIDFromQuery(path)
if err != nil {
return "", err
}
if orgIDFromQuery != "" {
return orgIDFromQuery, nil
if workspaceIDFromQuery != "" {
return workspaceIDFromQuery, nil
}
isAccount, err := hasAccountSegment(path)
if err != nil {
Expand Down
30 changes: 28 additions & 2 deletions cmd/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestHasAccountSegment(t *testing.T) {
}
}

func TestExtractOrgIDFromQuery(t *testing.T) {
func TestExtractWorkspaceIDFromQuery(t *testing.T) {
cases := []struct {
name string
path string
Expand All @@ -60,10 +60,15 @@ func TestExtractOrgIDFromQuery(t *testing.T) {
{"unrelated o-prefixed param ignored", "/api/2.0/clusters/list?other=1", ""},
{"absolute URL", "https://example.com/api/2.0/clusters/list?o=42", "42"},
{"first value wins on duplicate", "/api/2.0/clusters/list?o=1&o=2", "1"},
{"w param present", "/api/2.2/jobs/list?w=7474644166319138", "7474644166319138"},
{"w param empty", "/api/2.0/clusters/list?w=", ""},
{"w among other params", "/api/2.0/clusters/list?foo=bar&w=123", "123"},
{"o wins over w when both present", "/api/2.0/clusters/list?o=111&w=222", "111"},
{"w used when o is empty", "/api/2.0/clusters/list?o=&w=222", "222"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, err := extractOrgIDFromQuery(c.path)
got, err := extractWorkspaceIDFromQuery(c.path)
require.NoError(t, err)
assert.Equal(t, c.want, got)
})
Expand All @@ -76,6 +81,7 @@ func TestResolveOrgID(t *testing.T) {
accountPath = "/api/2.0/accounts/abc-123/network-policies"
proxyPath = "/api/2.0/preview/accounts/access-control/rule-sets"
spogPath = "/api/2.2/jobs/list?o=7474644166319138"
spogPathW = "/api/2.2/jobs/list?w=7474644166319138"
spogAccountPath = "/api/2.0/accounts/abc-123/network-policies?o=7474644166319138"
spogWorkspaceID = "7474644166319138"
resolvedWSID = "900800700600"
Expand Down Expand Up @@ -189,6 +195,26 @@ func TestResolveOrgID(t *testing.T) {
path: spogAccountPath,
want: spogWorkspaceID,
},
{
name: "?w=<id> sets identifier when no flag and no profile WorkspaceID",
cfgWorkspaceID: "",
path: spogPathW,
want: spogWorkspaceID,
},
{
name: "?w=<id> overrides profile WorkspaceID",
cfgWorkspaceID: resolvedWSID,
path: spogPathW,
want: spogWorkspaceID,
},
{
name: "--workspace-id wins over ?w=",
workspaceIDFlag: flagWSID,
flagSet: true,
cfgWorkspaceID: resolvedWSID,
path: spogPathW,
want: flagWSID,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down
6 changes: 4 additions & 2 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ use the flags directly to specify both.
The host URL may include query parameters to set the workspace and account ID:
databricks auth login --host "https://<host>?o=<workspace_id>&account_id=<id>"
databricks auth login --host "https://<host>?w=<workspace_id>&account_id=<id>"
Note: URLs containing "?" must be quoted to prevent shell interpretation.
The workspace ID may be passed as ?w= (preferred), ?o= (legacy), or
?workspace_id=. Note: URLs containing "?" must be quoted to prevent shell
interpretation.
If a profile with the given name already exists, it is updated. Otherwise
a new profile is created.
Expand Down
20 changes: 15 additions & 5 deletions libs/auth/hostparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ type HostParams struct {
// Host is the URL with query parameters stripped.
Host string

// WorkspaceID extracted from ?o= or ?workspace_id=.
// Empty if not present or not numeric.
// WorkspaceID extracted from ?o=, ?w=, or ?workspace_id=.
// Empty if not present. ?o= and ?workspace_id= are legacy spellings that
// remain numeric-only; ?w= is the new spelling and is passed through
// unchanged so non-numeric connection-style identifiers reach the server.
WorkspaceID string

// AccountID extracted from ?a= or ?account_id=.
Expand All @@ -21,9 +23,15 @@ type HostParams struct {
}

// ExtractHostQueryParams parses recognized query parameters from a host URL.
// Recognized parameters: o (workspace_id), workspace_id, a (account_id), account_id.
// Workspace IDs must be numeric; non-numeric values are ignored.
// The returned Host has all query parameters and fragments stripped.
// Recognized parameters: o (workspace_id), w (workspace_id), workspace_id,
// a (account_id), account_id. The "w" spelling matches the new
// X-Databricks-Workspace-Id routing header and accepts any non-empty value
// (including non-numeric connection-style identifiers). The legacy "o" and
// "workspace_id" spellings remain numeric-only — they predate the broader
// identifier shapes and historical URLs carrying those forms are always
// numeric. When more than one spelling is present, "o" wins to preserve the
// meaning of existing URLs. The returned Host has all query parameters and
// fragments stripped.
func ExtractHostQueryParams(host string) HostParams {
u, err := url.Parse(host)
if err != nil || u.RawQuery == "" {
Expand All @@ -37,6 +45,8 @@ func ExtractHostQueryParams(host string) HostParams {
if _, err := strconv.ParseInt(v, 10, 64); err == nil {
workspaceID = v
}
} else if v := q.Get("w"); v != "" {
workspaceID = v
} else if v := q.Get("workspace_id"); v != "" {
if _, err := strconv.ParseInt(v, 10, 64); err == nil {
workspaceID = v
Expand Down
29 changes: 27 additions & 2 deletions libs/auth/hostparams_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,51 @@ func TestExtractHostQueryParams(t *testing.T) {
host: "https://spog.example.com/?account_id=abc",
want: HostParams{Host: "https://spog.example.com", AccountID: "abc"},
},
{
name: "extract workspace_id from ?w=",
host: "https://spog.example.com/?w=12345",
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "12345"},
},
{
name: "extract workspace_id from ?workspace_id=",
host: "https://spog.example.com/?workspace_id=99999",
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "99999"},
},
{
name: "?o= wins over ?w= when both present",
host: "https://spog.example.com/?o=11111&w=22222",
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "11111"},
},
{
name: "?w= wins over ?workspace_id= when both present",
host: "https://spog.example.com/?w=11111&workspace_id=22222",
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "11111"},
},
{
name: "no query params leaves host unchanged",
host: "https://spog.example.com",
want: HostParams{Host: "https://spog.example.com"},
},
{
name: "non-numeric ?o= is skipped",
name: "non-numeric ?o= is skipped (legacy spelling stays numeric-only)",
host: "https://spog.example.com/?o=abc",
want: HostParams{Host: "https://spog.example.com"},
},
{
name: "non-numeric ?workspace_id= is skipped",
name: "non-numeric ?w= is passed through",
host: "https://spog.example.com/?w=abc",
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"},
},
{
name: "non-numeric ?workspace_id= is skipped (legacy spelling stays numeric-only)",
host: "https://spog.example.com/?workspace_id=abc",
want: HostParams{Host: "https://spog.example.com"},
},
{
name: "connection-id-style ?w= value passed through",
host: "https://spog.example.com/?w=123e4567-e89b-12d3-a456-426614174000",
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "123e4567-e89b-12d3-a456-426614174000"},
},
{
name: "invalid URL is left unchanged",
host: "not a valid url ://???",
Expand Down
Loading