Skip to content

Commit df305f2

Browse files
maksim-zuev-dbIsaac
andauthored
Add a non-interactive device code login (#7220)
## Changes When stdin is not a terminal, `auth login --use-device-code` runs in two steps. The first step stores the device code as a pending login and prints the URL and the user code. Running the same command with the hidden `--user-code` and `--confirmation-code` flags exchanges the device code and saves the login. In a terminal, the login stays interactive. A new login for the same profile or host replaces its pending login. The second step checks the user code, and the host, client ID and scopes the login was started with. `auth logout` also deletes pending logins. Every `auth login`, not only the device code login, now rejects a profile name that ends with `#device-code`, because the token of that profile would share its key with the pending login of another profile. The pending login is a new `PendingLogin` field of `storage.Entry`, stored under the cache key with a `#device-code` suffix. Piped stdin no longer completes the login: it starts the two-step login instead. ## Why An AI agent or a script that runs the login as a one-off command cannot type the confirmation code into a running `auth login`. ## Tests Acceptance tests under `device-code/non-interactive` and `logout/pending-device-code-login`, unit tests for the stores and `IsInputTTY`, and `termtest` tests for the second step in a terminal. The `device-code/nominal` and `device-code/wrong-code` acceptance tests piped the code into stdin, so they move to `termtest` tests. No changelog fragment: `--use-device-code` is still behind `DATABRICKS_CLI_DEVICE_CODE`. This pull request and its description were written by Isaac. --------- Co-authored-by: Isaac <no-reply@databricks.com>
1 parent 801825c commit df305f2

49 files changed

Lines changed: 1466 additions & 148 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎acceptance/cmd/auth/login/device-code/nominal/script‎

Lines changed: 0 additions & 20 deletions
This file was deleted.

‎acceptance/cmd/auth/login/device-code/nominal/test.toml‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

acceptance/cmd/auth/login/device-code/nominal/out.test.toml renamed to acceptance/cmd/auth/login/device-code/non-interactive/concurrent/out.test.toml

File renamed without changes.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
=== A second login for the same profile replaces the first
3+
4+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
5+
6+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
7+
8+
>>> cache_keys
9+
[
10+
"test#device-code"
11+
]
12+
13+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
14+
Error: --user-code does not match the pending login for profile test. Check the user code. If you started a newer login, complete it with its user code. Run the same command without --user-code and --confirmation-code to start a new login
15+
16+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0002 --confirmation-code FTFA-QQKY
17+
Profile test was successfully saved
18+
19+
>>> print_requests.py --oneline //oidc/v1/token
20+
{"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"}
21+
22+
=== Logins for different targets are independent
23+
24+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code
25+
26+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code
27+
28+
>>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code
29+
30+
>>> cache_keys
31+
[
32+
"a#device-code",
33+
"b#device-code",
34+
"[DATABRICKS_URL]",
35+
"[DATABRICKS_URL]#device-code",
36+
"test"
37+
]
38+
39+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code --user-code WDJB-0004 --confirmation-code FTFA-QQKY
40+
Profile b was successfully saved
41+
42+
>>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-0005 --confirmation-code FTFA-QQKY
43+
44+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code --user-code WDJB-0003 --confirmation-code FTFA-QQKY
45+
Profile a was successfully saved
46+
47+
>>> cache_keys
48+
[
49+
"a",
50+
"b",
51+
"[DATABRICKS_URL]",
52+
"test"
53+
]
54+
55+
>>> print_requests.py --oneline //oidc/v1/token
56+
{"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"}
57+
{"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"}
58+
{"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"}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
sethome "./home"
2+
3+
# The fake server issues the user codes WDJB0001, WDJB0002, ... in order.
4+
title "A second login for the same profile replaces the first\n"
5+
start_login LOG.first $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
6+
start_login LOG.second $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
7+
trace cache_keys
8+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
9+
trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0002 --confirmation-code FTFA-QQKY
10+
trace print_requests.py --oneline //oidc/v1/token
11+
12+
title "Logins for different targets are independent\n"
13+
start_login LOG.a $CLI auth login --host $DATABRICKS_HOST --profile a --use-device-code
14+
start_login LOG.b $CLI auth login --host $DATABRICKS_HOST --profile b --use-device-code
15+
start_login LOG.host $CLI auth login --host $DATABRICKS_HOST --use-device-code
16+
trace cache_keys
17+
trace $CLI auth login --host $DATABRICKS_HOST --profile b --use-device-code --user-code WDJB-0004 --confirmation-code FTFA-QQKY
18+
trace $CLI auth login --host $DATABRICKS_HOST --use-device-code --user-code WDJB-0005 --confirmation-code FTFA-QQKY
19+
trace $CLI auth login --host $DATABRICKS_HOST --profile a --use-device-code --user-code WDJB-0003 --confirmation-code FTFA-QQKY
20+
trace cache_keys
21+
trace print_requests.py --oneline //oidc/v1/token
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[resources]
2+
host = $DATABRICKS_HOST
3+
resources = https://resource.test
4+
auth_type = databricks-cli

acceptance/cmd/auth/login/device-code/wrong-code/out.test.toml renamed to acceptance/cmd/auth/login/device-code/non-interactive/errors/out.test.toml

File renamed without changes.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
2+
=== Device code login is disabled by default
3+
4+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
5+
Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it
6+
7+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
8+
Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it
9+
10+
=== Flags the non-interactive login cannot honor
11+
12+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --configure-cluster
13+
Error: --configure-cluster is not supported by the non-interactive device code login. Set cluster_id on the profile after the login
14+
15+
=== Missing host
16+
17+
>>> musterr [CLI] auth login --use-device-code
18+
Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
19+
20+
>>> musterr [CLI] auth login --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
21+
Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
22+
23+
=== Incomplete flags of the second step
24+
25+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --confirmation-code FTFA-QQKY
26+
Error: --confirmation-code requires --user-code
27+
28+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0001
29+
Error: --user-code requires --confirmation-code
30+
31+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --user-code WDJB-0001 --confirmation-code FTFA-QQKY
32+
Error: --confirmation-code requires --use-device-code
33+
34+
=== Profile name that marks a pending login
35+
36+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test#device-code --use-device-code
37+
Error: profile name "test#device-code" must not end with #device-code, which marks pending device code logins
38+
39+
=== Account host
40+
41+
>>> musterr [CLI] auth login --host https://accounts.cloud.databricks.test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
42+
Warn: [hostmetadata] failed to fetch host metadata for https://accounts.cloud.databricks.test, will skip for 1m0s
43+
Error: --use-device-code is only supported for workspace hosts
44+
45+
=== Profile scoped to resources
46+
47+
>>> musterr [CLI] auth login --profile resources --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
48+
Error: --use-device-code cannot be used with profile resources because its login is scoped to resources. Log in without --use-device-code
49+
50+
=== No pending login
51+
52+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
53+
Error: no pending device code login for profile test. Run the same command without --user-code and --confirmation-code to start a new login
54+
55+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
56+
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
57+
58+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
59+
60+
=== Wrong confirmation code
61+
62+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0001 --confirmation-code AAAA-AAAA
63+
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
64+
65+
=== Rerun with the right confirmation code
66+
67+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code wdjb-0001 --confirmation-code ftfa-qqky
68+
Profile test was successfully saved
69+
70+
=== Rerun after the login was completed
71+
72+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
73+
Error: no pending device code login for profile test. Run the same command without --user-code and --confirmation-code to start a new login
74+
75+
=== Device code unknown to the server
76+
77+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
78+
79+
>>> cp home/.databricks/token-cache.json home/token-cache.pending.json
80+
81+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0002 --confirmation-code FTFA-QQKY
82+
Profile test was successfully saved
83+
84+
>>> cp home/token-cache.pending.json home/.databricks/token-cache.json
85+
86+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0002 --confirmation-code FTFA-QQKY
87+
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
88+
89+
>>> cache_keys
90+
[
91+
"[DATABRICKS_URL]",
92+
"test"
93+
]
94+
95+
=== Scopes changed between the steps
96+
97+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --scopes sql
98+
99+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-0003 --confirmation-code FTFA-QQKY
100+
Error: the login for profile test was started with different scopes. Complete it with the same command that started it. Run the same command without --user-code and --confirmation-code to start a new login
101+
102+
>>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token
103+
{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"}
104+
{"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"}
105+
{"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"}
106+
{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"}
107+
{"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"}
108+
{"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"}
109+
{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+sql"}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
sethome "./home"
2+
3+
title "Device code login is disabled by default\n"
4+
(
5+
unset DATABRICKS_CLI_DEVICE_CODE
6+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
7+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
8+
)
9+
10+
title "Flags the non-interactive login cannot honor\n"
11+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --configure-cluster
12+
13+
# The non-interactive login cannot answer the profile picker or the host prompt.
14+
title "Missing host\n"
15+
trace musterr $CLI auth login --use-device-code
16+
trace musterr $CLI auth login --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
17+
18+
title "Incomplete flags of the second step\n"
19+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --confirmation-code FTFA-QQKY
20+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0001
21+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --user-code WDJB-0001 --confirmation-code FTFA-QQKY
22+
23+
title "Profile name that marks a pending login\n"
24+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile 'test#device-code' --use-device-code
25+
26+
title "Account host\n"
27+
trace musterr $CLI auth login --host https://accounts.cloud.databricks.test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
28+
29+
envsubst < "./home/.databrickscfg.tmpl" > "./home/.databrickscfg"
30+
31+
title "Profile scoped to resources\n"
32+
trace musterr $CLI auth login --profile resources --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
33+
34+
title "No pending login\n"
35+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
36+
trace musterr $CLI auth login --host $DATABRICKS_HOST --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
37+
38+
start_login LOG.wrong-code $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
39+
40+
title "Wrong confirmation code\n"
41+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0001 --confirmation-code AAAA-AAAA
42+
43+
# A wrong confirmation code leaves the device code valid, so the pending
44+
# login is kept.
45+
title "Rerun with the right confirmation code\n"
46+
trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code wdjb-0001 --confirmation-code ftfa-qqky
47+
48+
title "Rerun after the login was completed\n"
49+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0001 --confirmation-code FTFA-QQKY
50+
51+
# The server discards the device code when the login expires. The fake server
52+
# only discards a device code once it is exchanged, so a pending login restored
53+
# from before the exchange stands in for an expired one. The pending login is
54+
# then deleted.
55+
title "Device code unknown to the server\n"
56+
start_login LOG.expired $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
57+
trace cp home/.databricks/token-cache.json home/token-cache.pending.json
58+
trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0002 --confirmation-code FTFA-QQKY
59+
trace cp home/token-cache.pending.json home/.databricks/token-cache.json
60+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0002 --confirmation-code FTFA-QQKY
61+
trace cache_keys
62+
63+
# The token gets the scopes of the device authorization, so the second step
64+
# cannot change them.
65+
title "Scopes changed between the steps\n"
66+
start_login LOG.scopes $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --scopes sql
67+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-0003 --confirmation-code FTFA-QQKY
68+
69+
trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[scoped]
2+
host = $DATABRICKS_HOST
3+
scopes = sql
4+
client_id = databricks-cli
5+
auth_type = databricks-cli

0 commit comments

Comments
 (0)