From 70089dcd9b0d4391dfe95556c89371740140cfc2 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 29 Oct 2026 12:01:40 +0000 Subject: [PATCH 01/29] Add a non-interactive device code login When stdin is not a terminal, e.g. when an AI agent runs it, auth login --use-device-code stores the device code as a pending login in the token store and prints the URL and the user code. Rerunning the command with the hidden --user-code and --confirmation-code flags completes the login. The device code stays on the machine, so a confirmation code in an agent's transcript cannot complete the login elsewhere. The pending login is a new PendingLogin field of storage.Entry, and auth logout deletes it together with the token. The flow is picked with the new cmdio.IsInputTTY. The interactive login tests piped the code into stdin, which now starts the non-interactive login, so they move from acceptance tests to the terminal emulator. Co-authored-by: Isaac --- .../cmd/auth/login/device-code/nominal/script | 20 -- .../auth/login/device-code/nominal/test.toml | 1 - .../concurrent}/out.test.toml | 0 .../non-interactive/concurrent/output.txt | 60 +++++ .../non-interactive/concurrent/script | 23 ++ .../errors/home/.databrickscfg.tmpl | 4 + .../errors}/out.test.toml | 0 .../non-interactive/errors/output.txt | 90 ++++++++ .../device-code/non-interactive/errors/script | 56 +++++ .../flags/home/.databrickscfg.tmpl | 5 + .../non-interactive/flags/out.databrickscfg | 15 ++ .../non-interactive/flags/out.test.toml | 1 + .../non-interactive/flags/output.txt | 36 +++ .../device-code/non-interactive/flags/script | 16 ++ .../nominal/out.databrickscfg | 6 - .../non-interactive/nominal/out.test.toml | 1 + .../{ => non-interactive}/nominal/output.txt | 79 +++++-- .../non-interactive/nominal/script | 35 +++ .../non-interactive/script.prepare | 13 ++ .../device-code/non-interactive/test.toml | 3 + .../device-code/unsupported-host/output.txt | 2 +- .../login/device-code/unsupported-host/script | 2 +- .../login/device-code/wrong-code/output.txt | 40 ---- .../auth/login/device-code/wrong-code/script | 13 -- .../pending-device-code-login/out.test.toml | 1 + .../pending-device-code-login/output.txt | 19 ++ .../logout/pending-device-code-login/script | 38 ++++ cmd/auth/README.md | 34 +++ cmd/auth/login.go | 53 ++++- cmd/auth/login_device_code.go | 138 ++++++++++++ cmd/auth/login_device_code_test.go | 210 ++++++++++++++++-- cmd/auth/logout.go | 8 + docs/device-code-login.md | 61 ++++- libs/auth/storage/filestore.go | 5 +- libs/auth/storage/filestore_test.go | 22 ++ libs/auth/storage/keyring.go | 3 +- libs/auth/storage/keyring_test.go | 13 ++ libs/auth/storage/memory.go | 4 + libs/auth/storage/memory_test.go | 18 ++ libs/auth/storage/storage.go | 19 +- libs/auth/u2m/device_code.go | 6 +- libs/auth/u2m/device_code_test.go | 2 +- libs/cmdio/io.go | 9 + libs/cmdio/io_test.go | 13 ++ 44 files changed, 1066 insertions(+), 131 deletions(-) delete mode 100644 acceptance/cmd/auth/login/device-code/nominal/script delete mode 100644 acceptance/cmd/auth/login/device-code/nominal/test.toml rename acceptance/cmd/auth/login/device-code/{nominal => non-interactive/concurrent}/out.test.toml (100%) create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/concurrent/output.txt create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/concurrent/script create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/errors/home/.databrickscfg.tmpl rename acceptance/cmd/auth/login/device-code/{wrong-code => non-interactive/errors}/out.test.toml (100%) create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/errors/script create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/flags/home/.databrickscfg.tmpl create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/flags/out.databrickscfg create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/flags/out.test.toml create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/flags/output.txt create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/flags/script rename acceptance/cmd/auth/login/device-code/{ => non-interactive}/nominal/out.databrickscfg (68%) create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.test.toml rename acceptance/cmd/auth/login/device-code/{ => non-interactive}/nominal/output.txt (52%) create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/nominal/script create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/script.prepare create mode 100644 acceptance/cmd/auth/login/device-code/non-interactive/test.toml delete mode 100644 acceptance/cmd/auth/login/device-code/wrong-code/output.txt delete mode 100644 acceptance/cmd/auth/login/device-code/wrong-code/script create mode 100644 acceptance/cmd/auth/logout/pending-device-code-login/out.test.toml create mode 100644 acceptance/cmd/auth/logout/pending-device-code-login/output.txt create mode 100644 acceptance/cmd/auth/logout/pending-device-code-login/script diff --git a/acceptance/cmd/auth/login/device-code/nominal/script b/acceptance/cmd/auth/login/device-code/nominal/script deleted file mode 100644 index f1d6c04f59a..00000000000 --- a/acceptance/cmd/auth/login/device-code/nominal/script +++ /dev/null @@ -1,20 +0,0 @@ -sethome "./home" - -title "Login with a host\n" -printf "FTFA-QQKY\n" | trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code -trace $CLI auth token --profile test | gron.py | grep access_token -trace $CLI auth profiles - -title "Login again with the host from the saved profile\n" -printf "FTFA-QQKY\n" | trace $CLI auth login --profile test --use-device-code -trace $CLI auth token --profile test | gron.py | grep access_token -trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token - -# The token gets the scopes requested at device authorization, so --scopes is -# sent there and not with the token request. -title "Login with custom scopes\n" -printf "FTFA-QQKY\n" | trace $CLI auth login --host $DATABRICKS_HOST --profile scoped --use-device-code --scopes sql -trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token - -# Track the .databrickscfg file that was created to surface changes. -mv "./home/.databrickscfg" "./out.databrickscfg" diff --git a/acceptance/cmd/auth/login/device-code/nominal/test.toml b/acceptance/cmd/auth/login/device-code/nominal/test.toml deleted file mode 100644 index 159efe02696..00000000000 --- a/acceptance/cmd/auth/login/device-code/nominal/test.toml +++ /dev/null @@ -1 +0,0 @@ -RecordRequests = true diff --git a/acceptance/cmd/auth/login/device-code/nominal/out.test.toml b/acceptance/cmd/auth/login/device-code/non-interactive/concurrent/out.test.toml similarity index 100% rename from acceptance/cmd/auth/login/device-code/nominal/out.test.toml rename to acceptance/cmd/auth/login/device-code/non-interactive/concurrent/out.test.toml diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/concurrent/output.txt b/acceptance/cmd/auth/login/device-code/non-interactive/concurrent/output.txt new file mode 100644 index 00000000000..641900bd214 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/concurrent/output.txt @@ -0,0 +1,60 @@ + +=== A second login for the same profile replaces the first + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code + +>>> update_file.py home/.databricks/token-cache.json WDJBMJHT NEWRCODE + +>>> cache_keys +[ + "test#device-code" +] + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: a newer login for profile test replaced this one. Complete the newer login with its user code. Run the same command without --user-code and --confirmation-code to start a new login + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code NEWR-CODE --confirmation-code FTFA-QQKY +Profile test was successfully saved + +>>> print_requests.py --oneline //oidc/v1/token +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-2&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} + +=== Logins for different targets are independent + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code + +>>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code + +>>> cache_keys +[ + "a#device-code", + "b#device-code", + "[DATABRICKS_URL]", + "[DATABRICKS_URL]#device-code", + "test" +] + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Profile b was successfully saved + +>>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Profile a was successfully saved + +>>> cache_keys +[ + "a", + "b", + "[DATABRICKS_URL]", + "test" +] + +>>> print_requests.py --oneline //oidc/v1/token +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-4&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-5&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-3&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/concurrent/script b/acceptance/cmd/auth/login/device-code/non-interactive/concurrent/script new file mode 100644 index 00000000000..97ade6f94d2 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/concurrent/script @@ -0,0 +1,23 @@ +sethome "./home" + +# The real server issues a new user code for every login, while the fake server +# issues the same one. The script gives the second login its own user code. +title "A second login for the same profile replaces the first\n" +start_login LOG.first $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code +start_login LOG.second $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code +trace update_file.py home/.databricks/token-cache.json WDJBMJHT NEWRCODE +trace cache_keys +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code NEWR-CODE --confirmation-code FTFA-QQKY +trace print_requests.py --oneline //oidc/v1/token + +title "Logins for different targets are independent\n" +start_login LOG.a $CLI auth login --host $DATABRICKS_HOST --profile a --use-device-code +start_login LOG.b $CLI auth login --host $DATABRICKS_HOST --profile b --use-device-code +start_login LOG.host $CLI auth login --host $DATABRICKS_HOST --use-device-code +trace cache_keys +trace $CLI auth login --host $DATABRICKS_HOST --profile b --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace $CLI auth login --host $DATABRICKS_HOST --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace $CLI auth login --host $DATABRICKS_HOST --profile a --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace cache_keys +trace print_requests.py --oneline //oidc/v1/token diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/errors/home/.databrickscfg.tmpl b/acceptance/cmd/auth/login/device-code/non-interactive/errors/home/.databrickscfg.tmpl new file mode 100644 index 00000000000..3017709a305 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/errors/home/.databrickscfg.tmpl @@ -0,0 +1,4 @@ +[resources] +host = $DATABRICKS_HOST +resources = https://resource.test +auth_type = databricks-cli diff --git a/acceptance/cmd/auth/login/device-code/wrong-code/out.test.toml b/acceptance/cmd/auth/login/device-code/non-interactive/errors/out.test.toml similarity index 100% rename from acceptance/cmd/auth/login/device-code/wrong-code/out.test.toml rename to acceptance/cmd/auth/login/device-code/non-interactive/errors/out.test.toml diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt b/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt new file mode 100644 index 00000000000..a71936588a9 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/errors/output.txt @@ -0,0 +1,90 @@ + +=== Device code login is disabled by default + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code +Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it + +=== Flags the non-interactive login cannot honor + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --configure-cluster +Error: --configure-cluster is not supported by the non-interactive device code login. Set cluster_id on the profile after the login + +=== Missing host + +>>> musterr [CLI] auth login --use-device-code +Error: --use-device-code requires a workspace host. Pass it as an argument or with --host + +>>> musterr [CLI] auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: --use-device-code requires a workspace host. Pass it as an argument or with --host + +=== Incomplete flags of the second step + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --confirmation-code FTFA-QQKY +Error: --confirmation-code requires --user-code + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT +Error: --user-code requires --confirmation-code + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: --confirmation-code requires --use-device-code + +=== Account host + +>>> musterr [CLI] auth login --host https://accounts.cloud.databricks.test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Warn: [hostmetadata] failed to fetch host metadata for https://accounts.cloud.databricks.test, will skip for 1m0s +Error: --use-device-code is only supported for workspace hosts + +=== Profile scoped to resources + +>>> musterr [CLI] auth login --profile resources --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: --use-device-code cannot be used with profile resources because its login is scoped to resources. Log in without --use-device-code + +=== No pending login + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: no pending device code login for profile test. Run the same command without --user-code and --confirmation-code to start a new login + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: no pending device code login for host [DATABRICKS_URL]. Run the same command without --user-code and --confirmation-code to start a new login + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code + +=== Wrong confirmation code + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code AAAA-AAAA +Error: token exchange: Invalid confirmation code (error code: invalid_request). Check the confirmation code and approve the login if you haven't, then rerun this command. Run the same command without --user-code and --confirmation-code to start a new login if it still fails + +=== Rerun with the right confirmation code + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code wdjb-mjht --confirmation-code ftfa-qqky +Profile test was successfully saved + +=== Rerun after the login was completed + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: no pending device code login for profile test. Run the same command without --user-code and --confirmation-code to start a new login + +=== Device code unknown to the server + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code + +>>> update_file.py home/.databricks/token-cache.json device-code-2 unknown-device-code + +>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Error: token exchange: Invalid device code (error code: invalid_request). Run the same command without --user-code and --confirmation-code to start a new login + +>>> cache_keys +[ + "[DATABRICKS_URL]", + "test" +] + +>>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token +{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"} +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=AAAAAAAA&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} +{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"} +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=unknown-device-code&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/errors/script b/acceptance/cmd/auth/login/device-code/non-interactive/errors/script new file mode 100644 index 00000000000..15a8010930f --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/errors/script @@ -0,0 +1,56 @@ +sethome "./home" + +title "Device code login is disabled by default\n" +( + unset DATABRICKS_CLI_DEVICE_CODE + trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code + trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +) + +title "Flags the non-interactive login cannot honor\n" +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --configure-cluster + +# The non-interactive login cannot answer the profile picker or the host prompt. +title "Missing host\n" +trace musterr $CLI auth login --use-device-code +trace musterr $CLI auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +title "Incomplete flags of the second step\n" +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --confirmation-code FTFA-QQKY +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +title "Account host\n" +trace musterr $CLI auth login --host https://accounts.cloud.databricks.test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +envsubst < "./home/.databrickscfg.tmpl" > "./home/.databrickscfg" + +title "Profile scoped to resources\n" +trace musterr $CLI auth login --profile resources --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +title "No pending login\n" +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace musterr $CLI auth login --host $DATABRICKS_HOST --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +start_login LOG.wrong-code $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code + +title "Wrong confirmation code\n" +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code AAAA-AAAA + +# A wrong confirmation code leaves the device code valid, so the pending +# login is kept. +title "Rerun with the right confirmation code\n" +trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code wdjb-mjht --confirmation-code ftfa-qqky + +title "Rerun after the login was completed\n" +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +# The server discards the device code when the login expires. The pending +# login is then deleted. +title "Device code unknown to the server\n" +start_login LOG.expired $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code +trace update_file.py home/.databricks/token-cache.json device-code-2 unknown-device-code +trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace cache_keys + +trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/flags/home/.databrickscfg.tmpl b/acceptance/cmd/auth/login/device-code/non-interactive/flags/home/.databrickscfg.tmpl new file mode 100644 index 00000000000..d8e7fb8ab03 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/flags/home/.databrickscfg.tmpl @@ -0,0 +1,5 @@ +[scoped] +host = $DATABRICKS_HOST +scopes = sql +client_id = databricks-cli +auth_type = databricks-cli diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/flags/out.databrickscfg b/acceptance/cmd/auth/login/device-code/non-interactive/flags/out.databrickscfg new file mode 100644 index 00000000000..18fafc1c622 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/flags/out.databrickscfg @@ -0,0 +1,15 @@ +; The profile defined in the DEFAULT section is to be used as a fallback when no profile is explicitly specified. +[DEFAULT] + +[scoped] +host = [DATABRICKS_URL] +scopes = sql +auth_type = databricks-cli +workspace_id = [WORKSPACE_ID] +client_id = databricks-cli + +[serverless] +host = [DATABRICKS_URL] +serverless_compute_id = auto +workspace_id = [WORKSPACE_ID] +auth_type = databricks-cli diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/flags/out.test.toml b/acceptance/cmd/auth/login/device-code/non-interactive/flags/out.test.toml new file mode 100644 index 00000000000..18b1a88417e --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/flags/out.test.toml @@ -0,0 +1 @@ +Cloud = false diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/flags/output.txt b/acceptance/cmd/auth/login/device-code/non-interactive/flags/output.txt new file mode 100644 index 00000000000..a39e6a70953 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/flags/output.txt @@ -0,0 +1,36 @@ + +=== Scopes and client ID from the profile + +>>> [CLI] auth login --profile scoped --use-device-code +Using a browser on another device, visit: +[DATABRICKS_URL]/device/verification + +And enter the code: +WDJB-MJHT + +Once finished, complete the login by running the same command with +--user-code WDJB-MJHT and --confirmation-code set to the confirmation code +provided in your browser. + +>>> [CLI] auth login --profile scoped --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Profile scoped was successfully saved + +>>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token +{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+sql"} +{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} + +=== Serverless compute is configured in the second step + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile serverless --use-device-code --configure-serverless +Using a browser on another device, visit: +[DATABRICKS_URL]/device/verification + +And enter the code: +WDJB-MJHT + +Once finished, complete the login by running the same command with +--user-code WDJB-MJHT and --confirmation-code set to the confirmation code +provided in your browser. + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile serverless --use-device-code --configure-serverless --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Profile serverless was successfully saved diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/flags/script b/acceptance/cmd/auth/login/device-code/non-interactive/flags/script new file mode 100644 index 00000000000..4824729081c --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/flags/script @@ -0,0 +1,16 @@ +sethome "./home" +envsubst < "./home/.databrickscfg.tmpl" > "./home/.databrickscfg" + +# The rerun resolves the login from the same flags and profile as the first +# step, so the token exchange uses the client ID of the profile. +title "Scopes and client ID from the profile\n" +trace $CLI auth login --profile scoped --use-device-code +trace $CLI auth login --profile scoped --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token + +# Only the second step saves the profile. +title "Serverless compute is configured in the second step\n" +trace $CLI auth login --host $DATABRICKS_HOST --profile serverless --use-device-code --configure-serverless +trace $CLI auth login --host $DATABRICKS_HOST --profile serverless --use-device-code --configure-serverless --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +mv "./home/.databrickscfg" "./out.databrickscfg" diff --git a/acceptance/cmd/auth/login/device-code/nominal/out.databrickscfg b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.databrickscfg similarity index 68% rename from acceptance/cmd/auth/login/device-code/nominal/out.databrickscfg rename to acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.databrickscfg index 02b3737c466..cf14685849c 100644 --- a/acceptance/cmd/auth/login/device-code/nominal/out.databrickscfg +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.databrickscfg @@ -8,9 +8,3 @@ auth_type = databricks-cli [__settings__] default_profile = test - -[scoped] -host = [DATABRICKS_URL] -workspace_id = [WORKSPACE_ID] -scopes = sql -auth_type = databricks-cli diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.test.toml b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.test.toml new file mode 100644 index 00000000000..18b1a88417e --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/out.test.toml @@ -0,0 +1 @@ +Cloud = false diff --git a/acceptance/cmd/auth/login/device-code/nominal/output.txt b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt similarity index 52% rename from acceptance/cmd/auth/login/device-code/nominal/output.txt rename to acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt index 2216533b304..5a70dc2fd0c 100644 --- a/acceptance/cmd/auth/login/device-code/nominal/output.txt +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/output.txt @@ -1,5 +1,40 @@ -=== Login with a host +=== Start the login without a profile + +>>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code +Using a browser on another device, visit: +[DATABRICKS_URL]/device/verification + +And enter the code: +WDJB-MJHT + +Once finished, complete the login by running the same command with +--user-code WDJB-MJHT and --confirmation-code set to the confirmation code +provided in your browser. + +=== The first step stores the device code and saves no profile + +>>> find.py home +home/.databricks/token-cache.json + +>>> cache_keys +[ + "[DATABRICKS_URL]#device-code" +] + +=== Confirm the login + +>>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY + +>>> [CLI] auth token --host [DATABRICKS_URL] +json.access_token = "oauth-token"; + +>>> cache_keys +[ + "[DATABRICKS_URL]" +] + +=== Start the login with a profile >>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code Using a browser on another device, visit: @@ -8,8 +43,14 @@ Using a browser on another device, visit: And enter the code: WDJB-MJHT -Once finished, enter the confirmation code provided in your browser. -Confirmation code: Profile test was successfully saved +Once finished, complete the login by running the same command with +--user-code WDJB-MJHT and --confirmation-code set to the confirmation code +provided in your browser. + +=== Confirm the login + +>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Profile test was successfully saved >>> [CLI] auth token --profile test json.access_token = "oauth-token"; @@ -18,7 +59,7 @@ json.access_token = "oauth-token"; Name Host Valid test (Default) [DATABRICKS_URL] YES -=== Login again with the host from the saved profile +=== Start the login again with the host from the saved profile >>> [CLI] auth login --profile test --use-device-code Using a browser on another device, visit: @@ -27,30 +68,26 @@ Using a browser on another device, visit: And enter the code: WDJB-MJHT -Once finished, enter the confirmation code provided in your browser. -Confirmation code: Profile test was successfully saved +Once finished, complete the login by running the same command with +--user-code WDJB-MJHT and --confirmation-code set to the confirmation code +provided in your browser. >>> [CLI] auth token --profile test json.access_token = "oauth-token"; +>>> [CLI] auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +Profile test was successfully saved + +>>> cache_keys +[ + "[DATABRICKS_URL]", + "test" +] + >>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token {"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"} {"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} {"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"} {"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-2&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} - -=== Login with custom scopes - ->>> [CLI] auth login --host [DATABRICKS_URL] --profile scoped --use-device-code --scopes sql -Using a browser on another device, visit: -[DATABRICKS_URL]/device/verification - -And enter the code: -WDJB-MJHT - -Once finished, enter the confirmation code provided in your browser. -Confirmation code: Profile scoped was successfully saved - ->>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token -{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+sql"} +{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"} {"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-3&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"} diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script new file mode 100644 index 00000000000..2f00d282b27 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/nominal/script @@ -0,0 +1,35 @@ +sethome "./home" + +# Acceptance tests run the CLI without a terminal on stdin, so device code +# login runs non-interactively. +title "Start the login without a profile\n" +trace $CLI auth login --host $DATABRICKS_HOST --use-device-code + +title "The first step stores the device code and saves no profile\n" +trace find.py home +trace cache_keys + +title "Confirm the login\n" +trace $CLI auth login --host $DATABRICKS_HOST --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace $CLI auth token --host $DATABRICKS_HOST | gron.py | grep access_token +trace cache_keys + +title "Start the login with a profile\n" +trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code + +title "Confirm the login\n" +trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace $CLI auth token --profile test | gron.py | grep access_token +trace $CLI auth profiles + +# The pending login is stored next to the token, so the token keeps working +# until the login is confirmed. +title "Start the login again with the host from the saved profile\n" +trace $CLI auth login --profile test --use-device-code +trace $CLI auth token --profile test | gron.py | grep access_token +trace $CLI auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY +trace cache_keys +trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token + +# Track the .databrickscfg file that was created to surface changes. +mv "./home/.databrickscfg" "./out.databrickscfg" diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/script.prepare b/acceptance/cmd/auth/login/device-code/non-interactive/script.prepare new file mode 100644 index 00000000000..447c920c748 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/script.prepare @@ -0,0 +1,13 @@ +# cache_keys lists the keys in the plaintext token cache. +cache_keys() { + jq -S '.tokens | keys' "./home/.databricks/token-cache.json" +} + +# start_login traces the first step in "$@" and saves its output to file $1, +# for steps whose output was already shown. +start_login() { + local log="$1" + shift + >&2 printf "\n>>> %s\n" "$*" + "$@" &> "$log" +} diff --git a/acceptance/cmd/auth/login/device-code/non-interactive/test.toml b/acceptance/cmd/auth/login/device-code/non-interactive/test.toml new file mode 100644 index 00000000000..f493574d1e4 --- /dev/null +++ b/acceptance/cmd/auth/login/device-code/non-interactive/test.toml @@ -0,0 +1,3 @@ +# A dedicated server issues device codes in a known order, so that the scripts +# can confirm them. +RecordRequests = true diff --git a/acceptance/cmd/auth/login/device-code/unsupported-host/output.txt b/acceptance/cmd/auth/login/device-code/unsupported-host/output.txt index 7875f49ed6b..5d327ce2db2 100644 --- a/acceptance/cmd/auth/login/device-code/unsupported-host/output.txt +++ b/acceptance/cmd/auth/login/device-code/unsupported-host/output.txt @@ -2,7 +2,7 @@ === No host >>> musterr [CLI] auth login --profile new-profile --use-device-code -Error: the command is being run in a non-interactive environment, please specify a host using --host +Error: --use-device-code requires a workspace host. Pass it as an argument or with --host === Account host diff --git a/acceptance/cmd/auth/login/device-code/unsupported-host/script b/acceptance/cmd/auth/login/device-code/unsupported-host/script index d744aec1709..2d0a8949c8e 100644 --- a/acceptance/cmd/auth/login/device-code/unsupported-host/script +++ b/acceptance/cmd/auth/login/device-code/unsupported-host/script @@ -4,7 +4,7 @@ title "No host\n" # The profile does not exist yet and no host is given. auth login does not read # DATABRICKS_HOST, so the usual login would fall back to the browser-based # discovery flow. Device code login skips it and asks for the host instead, -# which fails without a terminal. +# which the non-interactive login cannot do. trace musterr $CLI auth login --profile new-profile --use-device-code envsubst < "./home/.databrickscfg.tmpl" > "./home/.databrickscfg" diff --git a/acceptance/cmd/auth/login/device-code/wrong-code/output.txt b/acceptance/cmd/auth/login/device-code/wrong-code/output.txt deleted file mode 100644 index 2ba7f3ea23e..00000000000 --- a/acceptance/cmd/auth/login/device-code/wrong-code/output.txt +++ /dev/null @@ -1,40 +0,0 @@ - -=== Login fails when no confirmation code is provided - ->>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code -Using a browser on another device, visit: -[DATABRICKS_URL]/device/verification - -And enter the code: -WDJB-MJHT - -Once finished, enter the confirmation code provided in your browser. -Confirmation code: Error: reading confirmation code: EOF - -=== Login fails after 3 wrong confirmation codes - ->>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code -Using a browser on another device, visit: -[DATABRICKS_URL]/device/verification - -And enter the code: -WDJB-MJHT - -Once finished, enter the confirmation code provided in your browser. -Confirmation code: Invalid confirmation code (error code: invalid_request). Enter the confirmation code again (2 attempts left). -Confirmation code: Invalid confirmation code (error code: invalid_request). Enter the confirmation code again (1 attempt left). -Confirmation code: Error: Invalid confirmation code (error code: invalid_request). Start login from scratch - -=== Login succeeds after re-entering the confirmation code - ->>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code -Using a browser on another device, visit: -[DATABRICKS_URL]/device/verification - -And enter the code: -WDJB-MJHT - -Once finished, enter the confirmation code provided in your browser. -Confirmation code: Invalid confirmation code (error code: invalid_request). Enter the confirmation code again (2 attempts left). -Confirmation code: Invalid confirmation code (error code: invalid_request). Enter the confirmation code again (1 attempt left). -Confirmation code: Profile test was successfully saved diff --git a/acceptance/cmd/auth/login/device-code/wrong-code/script b/acceptance/cmd/auth/login/device-code/wrong-code/script deleted file mode 100644 index 3adb0de81a3..00000000000 --- a/acceptance/cmd/auth/login/device-code/wrong-code/script +++ /dev/null @@ -1,13 +0,0 @@ -sethome "./home" - -title "Login fails when no confirmation code is provided\n" -trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code < /dev/null - -title "Login fails after 3 wrong confirmation codes\n" -printf "AAAA-AAAA\nBBBB-BBBB\nCCCC-CCCC\n" | trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code - -# A wrong confirmation code leaves the device code valid, so the user can -# enter it again. A blank line counts as a wrong confirmation code. -# The confirmation code is accepted the way the browser displays it, in any case. -title "Login succeeds after re-entering the confirmation code\n" -printf "AAAA-AAAA\n\nftfa-qqky\n" | trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code diff --git a/acceptance/cmd/auth/logout/pending-device-code-login/out.test.toml b/acceptance/cmd/auth/logout/pending-device-code-login/out.test.toml new file mode 100644 index 00000000000..18b1a88417e --- /dev/null +++ b/acceptance/cmd/auth/logout/pending-device-code-login/out.test.toml @@ -0,0 +1 @@ +Cloud = false diff --git a/acceptance/cmd/auth/logout/pending-device-code-login/output.txt b/acceptance/cmd/auth/logout/pending-device-code-login/output.txt new file mode 100644 index 00000000000..b347f1321d4 --- /dev/null +++ b/acceptance/cmd/auth/logout/pending-device-code-login/output.txt @@ -0,0 +1,19 @@ + +=== Logout and delete dev +>>> [CLI] auth logout --profile dev --auto-approve --delete +Logged out of and deleted profile "dev". + +=== Token cache keys after logout — the host keys are kept, staging shares them +[ + "[DATABRICKS_URL]", + "[DATABRICKS_URL]#device-code", + "staging", + "staging#device-code" +] + +=== Logout staging +>>> [CLI] auth logout --profile staging --auto-approve +Logged out of profile "staging". Use --delete to also remove it from the config file. + +=== Token cache keys after logout — no profile uses the host anymore +[] diff --git a/acceptance/cmd/auth/logout/pending-device-code-login/script b/acceptance/cmd/auth/logout/pending-device-code-login/script new file mode 100644 index 00000000000..1d62f2cb572 --- /dev/null +++ b/acceptance/cmd/auth/logout/pending-device-code-login/script @@ -0,0 +1,38 @@ +sethome "./home" + +cat > "./home/.databrickscfg" < "./home/.databricks/token-cache.json" <