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
26 changes: 26 additions & 0 deletions acceptance/bin/browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""
This script fetches a URL.
It follows redirects if applicable.

Usage: browser.py <url>
"""

import urllib.request
import sys

if len(sys.argv) < 2:
sys.stderr.write("Usage: browser.py <url>\n")
sys.exit(1)

url = sys.argv[1]
try:
response = urllib.request.urlopen(url)
if response.status != 200:
sys.stderr.write(f"Failed to fetch URL: {url} (status {response.status})\n")
sys.exit(1)
except Exception as e:
sys.stderr.write(f"Failed to fetch URL: {url} ({e})\n")
sys.exit(1)

sys.exit(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not needed.

6 changes: 6 additions & 0 deletions acceptance/cmd/auth/login/out.databrickscfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
; The profile defined in the DEFAULT section is to be used as a fallback when no profile is explicitly specified.
[DEFAULT]

[test]
host = [DATABRICKS_URL]
auth_type = databricks-cli
5 changes: 5 additions & 0 deletions acceptance/cmd/auth/login/out.test.toml

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

7 changes: 7 additions & 0 deletions acceptance/cmd/auth/login/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

>>> [CLI] auth login --host [DATABRICKS_URL] --profile test
Profile test was successfully saved

>>> [CLI] auth profiles
Name Host Valid
test [DATABRICKS_URL] YES
11 changes: 11 additions & 0 deletions acceptance/cmd/auth/login/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
sethome "./home"

# Use a fake browser that performs a GET on the authorization URL
# and follows the redirect back to localhost.
export BROWSER="browser.py"

trace $CLI auth login --host $DATABRICKS_HOST --profile test
trace $CLI auth profiles

# Track the .databrickscfg file that was created to surface changes.
mv "./home/.databrickscfg" "./out.databrickscfg"
3 changes: 3 additions & 0 deletions acceptance/cmd/auth/login/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Ignore = [
"home"
]
11 changes: 11 additions & 0 deletions acceptance/script.prepare
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,14 @@ envsubst() {
print_telemetry_bool_values() {
jq -r 'select(.path? == "/telemetry-ext") | (.body.protoLogs // [])[] | fromjson | ( (.entry // .) | (.databricks_cli_log.bundle_deploy_event.experimental.bool_values // []) ) | map("\(.key) \(.value)") | .[]' out.requests.txt | sort
}

sethome() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why add it here if it's only used in one test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent a few iterations figuring out why something was failing on Windows only to find out that the wrong $HOME-equivalent environment variable was set. With a helper it is more likely that the right thing is copy/pasted into other acceptance tests.

local home="$1"
mkdir -p "$home"

# For macOS and Linux, use HOME.
export HOME="$home"

# For Windows, use USERPROFILE.
export USERPROFILE="$home"
}
44 changes: 43 additions & 1 deletion cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import (
"github.com/databricks/cli/libs/databrickscfg"
"github.com/databricks/cli/libs/databrickscfg/cfgpickers"
"github.com/databricks/cli/libs/databrickscfg/profile"
"github.com/databricks/cli/libs/env"
"github.com/databricks/cli/libs/exec"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/credentials/u2m"
browserpkg "github.com/pkg/browser"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -136,7 +139,11 @@ depends on the existing profiles you have set in your configuration file
if err != nil {
return err
}
persistentAuth, err := u2m.NewPersistentAuth(ctx, u2m.WithOAuthArgument(oauthArgument))
persistentAuthOpts := []u2m.PersistentAuthOption{
u2m.WithOAuthArgument(oauthArgument),
}
persistentAuthOpts = append(persistentAuthOpts, u2m.WithBrowser(getBrowserFunc(cmd)))
persistentAuth, err := u2m.NewPersistentAuth(ctx, persistentAuthOpts...)
if err != nil {
return err
}
Expand Down Expand Up @@ -288,3 +295,38 @@ func loadProfileByName(ctx context.Context, profileName string, profiler profile
}
return nil, nil
}

// getBrowserFunc returns a function that opens the given URL in the browser.
// It respects the BROWSER environment variable:
// - empty string: uses the default browser
// - "none": prints the URL to stdout without opening a browser
// - custom command: executes the specified command with the URL as argument
func getBrowserFunc(cmd *cobra.Command) func(url string) error {
browser := env.Get(cmd.Context(), "BROWSER")
switch browser {
case "":
return browserpkg.OpenURL
case "none":
return func(url string) error {
fmt.Fprintf(cmd.OutOrStdout(), "Please open %s in the browser to continue authentication\n", url)
return nil
}
default:
return func(url string) error {
// Run the browser command via a shell.
// It can be a script or a binary and scripts cannot be executed directly on Windows.
e, err := exec.NewCommandExecutor(".")
if err != nil {
return err
}

e.WithInheritOutput()
cmd, err := e.StartCommand(cmd.Context(), fmt.Sprintf("%q %q", browser, url))
if err != nil {
return err
}

return cmd.Wait()
}
}
}
60 changes: 60 additions & 0 deletions libs/testserver/fake_oidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package testserver

import (
"net/http"
"net/url"
)

// FakeOidc holds OAuth state for acceptance tests.
type FakeOidc struct {
url string
}

func (s *FakeOidc) OidcEndpoints() Response {
return Response{
Body: map[string]string{
"authorization_endpoint": s.url + "/oidc/v1/authorize",
"token_endpoint": s.url + "/oidc/v1/token",
},
}
}

func (s *FakeOidc) OidcAuthorize(req Request) Response {
redirectURI, err := url.Parse(req.URL.Query().Get("redirect_uri"))
if err != nil {
return Response{
StatusCode: http.StatusBadRequest,
Body: err.Error(),
}
}

// Compile query parameters for the redirect URL.
q := make(url.Values)

// Include an opaque authorization code that will be used to exchange for a token.
q.Set("code", "oauth-code")

// Include the state parameter from the original request.
q.Set("state", req.URL.Query().Get("state"))

// Update the redirect URI with the new query parameters.
redirectURI.RawQuery = q.Encode()

return Response{
StatusCode: http.StatusMovedPermanently,
Headers: map[string][]string{
"Location": {redirectURI.String()},
},
}
}

func (s *FakeOidc) OidcToken(req Request) Response {
return Response{
Body: map[string]string{
"access_token": "oauth-token",
"expires_in": "3600",
"scope": "all-apis",
"token_type": "Bearer",
},
}
}
18 changes: 7 additions & 11 deletions libs/testserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,19 +222,15 @@ func AddDefaultHandlers(server *Server) {
})

server.Handle("GET", "/oidc/.well-known/oauth-authorization-server", func(_ Request) any {
return map[string]string{
"authorization_endpoint": server.URL + "oidc/v1/authorize",
"token_endpoint": server.URL + "/oidc/v1/token",
}
return server.fakeOidc.OidcEndpoints()
})

server.Handle("POST", "/oidc/v1/token", func(_ Request) any {
return map[string]string{
"access_token": "oauth-token",
"expires_in": "3600",
"scope": "all-apis",
"token_type": "Bearer",
}
server.Handle("GET", "/oidc/v1/authorize", func(req Request) any {
return server.fakeOidc.OidcAuthorize(req)
})

server.Handle("POST", "/oidc/v1/token", func(req Request) any {
return server.fakeOidc.OidcToken(req)
})

server.Handle("POST", "/telemetry-ext", func(_ Request) any {
Expand Down
2 changes: 2 additions & 0 deletions libs/testserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Server struct {
t testutil.TestingT

fakeWorkspaces map[string]*FakeWorkspace
fakeOidc *FakeOidc
mu sync.Mutex

RequestCallback func(request *Request)
Expand Down Expand Up @@ -190,6 +191,7 @@ func New(t testutil.TestingT) *Server {
Router: router,
t: t,
fakeWorkspaces: map[string]*FakeWorkspace{},
fakeOidc: &FakeOidc{url: server.URL},
}

// Set up the not found handler as fallback
Expand Down
Loading