This is an automated email from the ASF dual-hosted git repository.
turaga pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 4ae79ed70a4 airflowctl auth login: prompt for credentials
interactively when none are provided (#62549)
4ae79ed70a4 is described below
commit 4ae79ed70a450934da8e63d1761517cce0ecbeac
Author: Dheeraj Turaga <[email protected]>
AuthorDate: Tue Mar 3 12:46:25 2026 -0600
airflowctl auth login: prompt for credentials interactively when none are
provided (#62549)
* airflowctl auth login: prompt for credentials interactively when none are
provided
Previously the command exited with an error if no --api-token,
AIRFLOW_CLI_TOKEN, --username, or --password was supplied. Now, on an
interactive terminal, it prompts for username and password (showing the
target environment in each prompt as [env]). If only --username is given,
only the password is prompted. Non-interactive contexts (CI, pipes) retain
the existing error-and-exit behaviour.
* Bugras suggestions
---
airflow-ctl/docs/images/command_hashes.txt | 2 +-
airflow-ctl/docs/images/output_auth_login.svg | 92 +++++++-------
airflow-ctl/src/airflowctl/ctl/cli_config.py | 12 --
.../src/airflowctl/ctl/commands/auth_command.py | 35 ++++--
.../airflow_ctl/ctl/commands/test_auth_command.py | 136 ++++++++++++++++-----
5 files changed, 176 insertions(+), 101 deletions(-)
diff --git a/airflow-ctl/docs/images/command_hashes.txt
b/airflow-ctl/docs/images/command_hashes.txt
index 46f6f42baf0..5922aa473cf 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -11,4 +11,4 @@ pools:03fc7d948cbecf16ff8d640eb8f0ce43
providers:1c0afb2dff31d93ab2934b032a2250ab
variables:0354f8f4b0dde1c3771ed1568692c6ae
version:31f4efdf8de0dbaaa4fac71ff7efecc3
-auth login:f85e04072626ab4ae17ad17e4a077bf2
+auth login:9fe2bb1dd5c602beea2eefb33a2b20a8
diff --git a/airflow-ctl/docs/images/output_auth_login.svg
b/airflow-ctl/docs/images/output_auth_login.svg
index 006ed5bc266..754353b3c68 100644
--- a/airflow-ctl/docs/images/output_auth_login.svg
+++ b/airflow-ctl/docs/images/output_auth_login.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 933 464.79999999999995"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 440.4"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,107 +19,103 @@
font-weight: 700;
}
- .terminal-938154658-matrix {
+ .terminal-2514228680-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-938154658-title {
+ .terminal-2514228680-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-938154658-r1 { fill: #ff8700 }
-.terminal-938154658-r2 { fill: #c5c8c6 }
-.terminal-938154658-r3 { fill: #808080 }
-.terminal-938154658-r4 { fill: #68a0b3 }
-.terminal-938154658-r5 { fill: #00af87 }
+ .terminal-2514228680-r1 { fill: #ff8700 }
+.terminal-2514228680-r2 { fill: #c5c8c6 }
+.terminal-2514228680-r3 { fill: #808080 }
+.terminal-2514228680-r4 { fill: #68a0b3 }
+.terminal-2514228680-r5 { fill: #00af87 }
</style>
<defs>
- <clipPath id="terminal-938154658-clip-terminal">
- <rect x="0" y="0" width="914.0" height="413.79999999999995" />
+ <clipPath id="terminal-2514228680-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="389.4" />
</clipPath>
- <clipPath id="terminal-938154658-line-0">
+ <clipPath id="terminal-2514228680-line-0">
<rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-1">
+<clipPath id="terminal-2514228680-line-1">
<rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-2">
+<clipPath id="terminal-2514228680-line-2">
<rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-3">
+<clipPath id="terminal-2514228680-line-3">
<rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-4">
+<clipPath id="terminal-2514228680-line-4">
<rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-5">
+<clipPath id="terminal-2514228680-line-5">
<rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-6">
+<clipPath id="terminal-2514228680-line-6">
<rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-7">
+<clipPath id="terminal-2514228680-line-7">
<rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-8">
+<clipPath id="terminal-2514228680-line-8">
<rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-9">
+<clipPath id="terminal-2514228680-line-9">
<rect x="0" y="221.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-10">
+<clipPath id="terminal-2514228680-line-10">
<rect x="0" y="245.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-11">
+<clipPath id="terminal-2514228680-line-11">
<rect x="0" y="269.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-12">
+<clipPath id="terminal-2514228680-line-12">
<rect x="0" y="294.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-13">
+<clipPath id="terminal-2514228680-line-13">
<rect x="0" y="318.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-14">
+<clipPath id="terminal-2514228680-line-14">
<rect x="0" y="343.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-938154658-line-15">
- <rect x="0" y="367.5" width="915" height="24.65"/>
- </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="462.8" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="438.4" rx="8"/>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
- <g transform="translate(9, 41)"
clip-path="url(#terminal-938154658-clip-terminal)">
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-2514228680-clip-terminal)">
- <g class="terminal-938154658-matrix">
- <text class="terminal-938154658-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-938154658-line-0)">Usage:</text><text
class="terminal-938154658-r3" x="85.4" y="20" textLength="256.2"
clip-path="url(#terminal-938154658-line-0)">airflowctl auth login</text><text
class="terminal-938154658-r2" x="341.6" y="20" textLength="24.4"
clip-path="url(#terminal-938154658-line-0)"> [</text><text
class="terminal-938154658-r4" x="366" y="20" textLength="24.4" clip-path="ur
[...]
-</text><text class="terminal-938154658-r2" x="0" y="44.4" textLength="366"
clip-path="url(#terminal-938154658-line-1)">                             [</text><text
class="terminal-938154658-r4" x="366" y="44.4" textLength="109.8"
clip-path="url(#terminal-938154658-line-1)">--api-url</text><text
class="terminal-938154658-r5" x="488" y="44.4" textLe [...]
-</text><text class="terminal-938154658-r2" x="0" y="68.8" textLength="366"
clip-path="url(#terminal-938154658-line-2)">                             [</text><text
class="terminal-938154658-r4" x="366" y="68.8" textLength="122"
clip-path="url(#terminal-938154658-line-2)">--password</text><text
class="terminal-938154658-r2" x="488" y="68.8" textLen [...]
-</text><text class="terminal-938154658-r2" x="0" y="93.2" textLength="366"
clip-path="url(#terminal-938154658-line-3)">                             [</text><text
class="terminal-938154658-r4" x="366" y="93.2" textLength="122"
clip-path="url(#terminal-938154658-line-3)">--username</text><text
class="terminal-938154658-r5" x="500.2" y="93.2" textL [...]
-</text><text class="terminal-938154658-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-938154658-line-4)">
-</text><text class="terminal-938154658-r2" x="0" y="142" textLength="366"
clip-path="url(#terminal-938154658-line-5)">Login to the metadata database</text><text
class="terminal-938154658-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-938154658-line-5)">
-</text><text class="terminal-938154658-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-938154658-line-6)">
-</text><text class="terminal-938154658-r1" x="0" y="190.8" textLength="97.6"
clip-path="url(#terminal-938154658-line-7)">Options:</text><text
class="terminal-938154658-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-938154658-line-7)">
-</text><text class="terminal-938154658-r4" x="24.4" y="215.2"
textLength="24.4" clip-path="url(#terminal-938154658-line-8)">-h</text><text
class="terminal-938154658-r2" x="48.8" y="215.2" textLength="24.4"
clip-path="url(#terminal-938154658-line-8)">, </text><text
class="terminal-938154658-r4" x="73.2" y="215.2" textLength="73.2"
clip-path="url(#terminal-938154658-line-8)">--help</text><text
class="terminal-938154658-r2" x="292.8" y="215.2" textLength="378.2"
clip-path="url(#termina [...]
-</text><text class="terminal-938154658-r4" x="24.4" y="239.6"
textLength="134.2"
clip-path="url(#terminal-938154658-line-9)">--api-token</text><text
class="terminal-938154658-r5" x="170.8" y="239.6" textLength="109.8"
clip-path="url(#terminal-938154658-line-9)">API_TOKEN</text><text
class="terminal-938154658-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-938154658-line-9)">
-</text><text class="terminal-938154658-r2" x="292.8" y="264" textLength="427"
clip-path="url(#terminal-938154658-line-10)">The token to use for authentication</text><text
class="terminal-938154658-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-938154658-line-10)">
-</text><text class="terminal-938154658-r4" x="24.4" y="288.4"
textLength="109.8"
clip-path="url(#terminal-938154658-line-11)">--api-url</text><text
class="terminal-938154658-r5" x="146.4" y="288.4" textLength="85.4"
clip-path="url(#terminal-938154658-line-11)">API_URL</text><text
class="terminal-938154658-r2" x="292.8" y="288.4" textLength="439.2"
clip-path="url(#terminal-938154658-line-11)">The URL of the metadata database API</text><text
class="terminal-93 [...]
-</text><text class="terminal-938154658-r4" x="24.4" y="312.8"
textLength="24.4" clip-path="url(#terminal-938154658-line-12)">-e</text><text
class="terminal-938154658-r2" x="48.8" y="312.8" textLength="24.4"
clip-path="url(#terminal-938154658-line-12)">, </text><text
class="terminal-938154658-r4" x="73.2" y="312.8" textLength="61"
clip-path="url(#terminal-938154658-line-12)">--env</text><text
class="terminal-938154658-r5" x="146.4" y="312.8" textLength="36.6"
clip-path="url(#terminal [...]
-</text><text class="terminal-938154658-r4" x="24.4" y="337.2" textLength="122"
clip-path="url(#terminal-938154658-line-13)">--password</text><text
class="terminal-938154658-r2" x="146.4" y="337.2" textLength="24.4"
clip-path="url(#terminal-938154658-line-13)"> [</text><text
class="terminal-938154658-r5" x="170.8" y="337.2" textLength="97.6"
clip-path="url(#terminal-938154658-line-13)">PASSWORD</text><text
class="terminal-938154658-r2" x="268.4" y="337.2" textLength="12.2" clip-path=
[...]
-</text><text class="terminal-938154658-r2" x="292.8" y="361.6"
textLength="463.6"
clip-path="url(#terminal-938154658-line-14)">The password to use for authentication</text><text
class="terminal-938154658-r2" x="915" y="361.6" textLength="12.2"
clip-path="url(#terminal-938154658-line-14)">
-</text><text class="terminal-938154658-r4" x="24.4" y="386" textLength="170.8"
clip-path="url(#terminal-938154658-line-15)">--skip-keyring</text><text
class="terminal-938154658-r2" x="292.8" y="386" textLength="427"
clip-path="url(#terminal-938154658-line-15)">Skip storing credentials in keyring</text><text
class="terminal-938154658-r2" x="915" y="386" textLength="12.2"
clip-path="url(#terminal-938154658-line-15)">
-</text><text class="terminal-938154658-r4" x="24.4" y="410.4" textLength="122"
clip-path="url(#terminal-938154658-line-16)">--username</text><text
class="terminal-938154658-r5" x="158.6" y="410.4" textLength="97.6"
clip-path="url(#terminal-938154658-line-16)">USERNAME</text><text
class="terminal-938154658-r2" x="292.8" y="410.4" textLength="463.6"
clip-path="url(#terminal-938154658-line-16)">The username to use for authentication</text><text
class="terminal-93815 [...]
+ <g class="terminal-2514228680-matrix">
+ <text class="terminal-2514228680-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-2514228680-line-0)">Usage:</text><text
class="terminal-2514228680-r3" x="85.4" y="20" textLength="256.2"
clip-path="url(#terminal-2514228680-line-0)">airflowctl auth login</text><text
class="terminal-2514228680-r2" x="341.6" y="20" textLength="24.4"
clip-path="url(#terminal-2514228680-line-0)"> [</text><text
class="terminal-2514228680-r4" x="366" y="20" textLength="24.4" clip-p [...]
+</text><text class="terminal-2514228680-r2" x="0" y="44.4" textLength="366"
clip-path="url(#terminal-2514228680-line-1)">                             [</text><text
class="terminal-2514228680-r4" x="366" y="44.4" textLength="109.8"
clip-path="url(#terminal-2514228680-line-1)">--api-url</text><text
class="terminal-2514228680-r5" x="488" y="44.4" t [...]
+</text><text class="terminal-2514228680-r2" x="0" y="68.8" textLength="366"
clip-path="url(#terminal-2514228680-line-2)">                             [</text><text
class="terminal-2514228680-r4" x="366" y="68.8" textLength="122"
clip-path="url(#terminal-2514228680-line-2)">--password</text><text
class="terminal-2514228680-r5" x="500.2" y="68.8" [...]
+</text><text class="terminal-2514228680-r2" x="0" y="93.2" textLength="366"
clip-path="url(#terminal-2514228680-line-3)">                             [</text><text
class="terminal-2514228680-r4" x="366" y="93.2" textLength="122"
clip-path="url(#terminal-2514228680-line-3)">--username</text><text
class="terminal-2514228680-r5" x="500.2" y="93.2" [...]
+</text><text class="terminal-2514228680-r2" x="915" y="117.6"
textLength="12.2" clip-path="url(#terminal-2514228680-line-4)">
+</text><text class="terminal-2514228680-r2" x="0" y="142" textLength="366"
clip-path="url(#terminal-2514228680-line-5)">Login to the metadata database</text><text
class="terminal-2514228680-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-2514228680-line-5)">
+</text><text class="terminal-2514228680-r2" x="915" y="166.4"
textLength="12.2" clip-path="url(#terminal-2514228680-line-6)">
+</text><text class="terminal-2514228680-r1" x="0" y="190.8" textLength="97.6"
clip-path="url(#terminal-2514228680-line-7)">Options:</text><text
class="terminal-2514228680-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-2514228680-line-7)">
+</text><text class="terminal-2514228680-r4" x="24.4" y="215.2"
textLength="24.4" clip-path="url(#terminal-2514228680-line-8)">-h</text><text
class="terminal-2514228680-r2" x="48.8" y="215.2" textLength="24.4"
clip-path="url(#terminal-2514228680-line-8)">, </text><text
class="terminal-2514228680-r4" x="73.2" y="215.2" textLength="73.2"
clip-path="url(#terminal-2514228680-line-8)">--help</text><text
class="terminal-2514228680-r2" x="292.8" y="215.2" textLength="378.2"
clip-path="url(# [...]
+</text><text class="terminal-2514228680-r4" x="24.4" y="239.6"
textLength="134.2"
clip-path="url(#terminal-2514228680-line-9)">--api-token</text><text
class="terminal-2514228680-r5" x="170.8" y="239.6" textLength="109.8"
clip-path="url(#terminal-2514228680-line-9)">API_TOKEN</text><text
class="terminal-2514228680-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-2514228680-line-9)">
+</text><text class="terminal-2514228680-r2" x="292.8" y="264" textLength="427"
clip-path="url(#terminal-2514228680-line-10)">The token to use for authentication</text><text
class="terminal-2514228680-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-2514228680-line-10)">
+</text><text class="terminal-2514228680-r4" x="24.4" y="288.4"
textLength="109.8"
clip-path="url(#terminal-2514228680-line-11)">--api-url</text><text
class="terminal-2514228680-r5" x="146.4" y="288.4" textLength="85.4"
clip-path="url(#terminal-2514228680-line-11)">API_URL</text><text
class="terminal-2514228680-r2" x="292.8" y="288.4" textLength="439.2"
clip-path="url(#terminal-2514228680-line-11)">The URL of the metadata database API</text><text
class="termi [...]
+</text><text class="terminal-2514228680-r4" x="24.4" y="312.8"
textLength="24.4" clip-path="url(#terminal-2514228680-line-12)">-e</text><text
class="terminal-2514228680-r2" x="48.8" y="312.8" textLength="24.4"
clip-path="url(#terminal-2514228680-line-12)">, </text><text
class="terminal-2514228680-r4" x="73.2" y="312.8" textLength="61"
clip-path="url(#terminal-2514228680-line-12)">--env</text><text
class="terminal-2514228680-r5" x="146.4" y="312.8" textLength="36.6"
clip-path="url(#t [...]
+</text><text class="terminal-2514228680-r4" x="24.4" y="337.2"
textLength="122"
clip-path="url(#terminal-2514228680-line-13)">--password</text><text
class="terminal-2514228680-r5" x="158.6" y="337.2" textLength="97.6"
clip-path="url(#terminal-2514228680-line-13)">PASSWORD</text><text
class="terminal-2514228680-r2" x="292.8" y="337.2" textLength="463.6"
clip-path="url(#terminal-2514228680-line-13)">The password to use for authentication</text><text
class="terminal [...]
+</text><text class="terminal-2514228680-r4" x="24.4" y="361.6"
textLength="170.8"
clip-path="url(#terminal-2514228680-line-14)">--skip-keyring</text><text
class="terminal-2514228680-r2" x="292.8" y="361.6" textLength="427"
clip-path="url(#terminal-2514228680-line-14)">Skip storing credentials in keyring</text><text
class="terminal-2514228680-r2" x="915" y="361.6" textLength="12.2"
clip-path="url(#terminal-2514228680-line-14)">
+</text><text class="terminal-2514228680-r4" x="24.4" y="386" textLength="122"
clip-path="url(#terminal-2514228680-line-15)">--username</text><text
class="terminal-2514228680-r5" x="158.6" y="386" textLength="97.6"
clip-path="url(#terminal-2514228680-line-15)">USERNAME</text><text
class="terminal-2514228680-r2" x="292.8" y="386" textLength="463.6"
clip-path="url(#terminal-2514228680-line-15)">The username to use for authentication</text><text
class="terminal-25142 [...]
</text>
</g>
</g>
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index ff5fe3ab772..28dce22805d 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -23,7 +23,6 @@ from __future__ import annotations
import argparse
import ast
import datetime
-import getpass
import inspect
import os
from argparse import Namespace
@@ -190,15 +189,6 @@ def string_lower_type(val):
return val.strip().lower()
-class Password(argparse.Action):
- """Custom action to prompt for password input."""
-
- def __call__(self, parser, namespace, values, option_string=None):
- if values is None:
- values = getpass.getpass()
- setattr(namespace, self.dest, values)
-
-
# Common Positional Arguments
ARG_FILE = Arg(
flags=("file",),
@@ -255,8 +245,6 @@ ARG_AUTH_PASSWORD = Arg(
type=str,
dest="password",
help="The password to use for authentication",
- action=Password,
- nargs="?",
)
# Dag Commands Args
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py
b/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py
index 722219f52ba..809cde294e8 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py
@@ -18,6 +18,7 @@
from __future__ import annotations
+import getpass
import glob
import json
import os
@@ -36,8 +37,29 @@ from airflowctl.ctl.console_formatting import AirflowConsole
def login(args, api_client=NEW_API_CLIENT) -> None:
"""Login to a provider."""
success_message = "[green]Login successful! Welcome to airflowctl![/green]"
- # Check is username and password are passed
- if args.username and args.password:
+
+ username = args.username
+ password = args.password
+ token = args.api_token or os.environ.get("AIRFLOW_CLI_TOKEN")
+
+ # If credentials are incomplete, prompt interactively on a real terminal
+ if not token and not (username and password):
+ if not sys.stdin.isatty():
+ rich.print("[red]No credentials provided.[/red]")
+ rich.print(
+ "[green]Please pass:[/green] [blue]--api-token[/blue] or set "
+ "[blue]AIRFLOW_CLI_TOKEN[/blue] environment variable to login."
+ "[blue] Alternatively, you can use --username and --password
to login.[/blue]"
+ )
+ sys.exit(1)
+ rich.print(f"[blue]Credentials for[/blue] [bold]{args.env}[/bold]
[blue]({args.api_url})[/blue]")
+ if not username:
+ username = input(f"[{args.env}] Username: ")
+ if not password:
+ password = getpass.getpass(f"[{args.env}] Password: ")
+
+ # Username + password login (from args or interactively prompted)
+ if username and password:
if args.skip_keyring:
rich.print("[red]The --skip-keyring is not compatible with
username and password login.")
sys.exit(1)
@@ -52,8 +74,8 @@ def login(args, api_client=NEW_API_CLIENT) -> None:
api_client.refresh_base_url(base_url=args.api_url,
kind=ClientKind.AUTH)
login_response = api_client.login.login_with_username_and_password(
LoginBody(
- username=args.username,
- password=args.password,
+ username=username,
+ password=password,
)
)
credentials.api_token = login_response.access_token
@@ -64,9 +86,8 @@ def login(args, api_client=NEW_API_CLIENT) -> None:
rich.print(f"[red]Login failed: {e}")
sys.exit(1)
- # Check if token is passed or environment variable is set
- if not (token := args.api_token or os.environ.get("AIRFLOW_CLI_TOKEN")):
- # Exit
+ # Token-based login
+ if not token:
rich.print("[red]No token found.")
rich.print(
"[green]Please pass:[/green] [blue]--api-token[/blue] or set "
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
index 70d19d26657..e76fafc28ad 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
@@ -16,7 +16,6 @@
# under the License.
from __future__ import annotations
-import io
import json
import os
import tempfile
@@ -83,16 +82,10 @@ class TestCliAuthCommands:
)
mock_keyring.set_password.side_effect = NoKeyringError("no backend")
- with (
- patch("sys.stdin", io.StringIO("test_password")),
- patch("airflowctl.ctl.cli_config.getpass.getpass",
return_value="test_password"),
- ):
- auth_command.login(
- self.parser.parse_args(
- ["auth", "login", "--skip-keyring", "--api-url",
"http://localhost:8080"]
- ),
- api_client=api_client,
- )
+ auth_command.login(
+ self.parser.parse_args(["auth", "login", "--skip-keyring",
"--api-url", "http://localhost:8080"]),
+ api_client=api_client,
+ )
@patch("airflowctl.api.client.keyring")
def test_login_without_skip_keyring_raises_on_no_keyring(self,
mock_keyring, api_client_maker):
@@ -106,9 +99,10 @@ class TestCliAuthCommands:
)
mock_keyring.set_password.side_effect = NoKeyringError("no backend")
+ non_tty_stdin = mock.MagicMock()
+ non_tty_stdin.isatty.return_value = False
with (
- patch("sys.stdin", io.StringIO("test_password")),
- patch("airflowctl.ctl.cli_config.getpass.getpass",
return_value="test_password"),
+ patch("sys.stdin", non_tty_stdin),
pytest.raises(SystemExit, match="1"),
):
auth_command.login(
@@ -128,28 +122,107 @@ class TestCliAuthCommands:
mock_keyring.set_password = mock.MagicMock()
mock_keyring.get_password.return_value = None
+ auth_command.login(
+ self.parser.parse_args(
+ [
+ "auth",
+ "login",
+ "--api-url",
+ "http://localhost:8080",
+ "--username",
+ "test_user",
+ "--password",
+ "test_password",
+ ]
+ ),
+ api_client=api_client,
+ )
+ mock_keyring.set_password.assert_has_calls(
+ [
+ mock.call("airflowctl", "api_token_production", "TEST_TOKEN"),
+ ]
+ )
+
+ @patch("airflowctl.api.client.keyring")
+ def test_login_prompts_for_credentials_interactively(self, mock_keyring,
api_client_maker):
+ """Test that login prompts for username and password when no
credentials are supplied on a TTY."""
+ api_client = api_client_maker(
+ path="/auth/token/cli",
+ response_json=self.login_response.model_dump(),
+ expected_http_status_code=201,
+ kind=ClientKind.AUTH,
+ )
+
+ mock_keyring.set_password = mock.MagicMock()
+ mock_keyring.get_password.return_value = None
+
+ tty_stdin = mock.MagicMock()
+ tty_stdin.isatty.return_value = True
+
with (
- patch("sys.stdin", io.StringIO("test_password")),
- patch("airflowctl.ctl.cli_config.getpass.getpass",
return_value="test_password"),
+ patch("sys.stdin", tty_stdin),
+ patch("builtins.input", return_value="prompted_user"),
+ patch("airflowctl.ctl.commands.auth_command.getpass.getpass",
return_value="prompted_pass"),
):
auth_command.login(
self.parser.parse_args(
- [
- "auth",
- "login",
- "--api-url",
- "http://localhost:8080",
- "--username",
- "test_user",
- "--password",
- ]
+ ["auth", "login", "--api-url", "http://localhost:8080",
"--env", "staging"]
),
api_client=api_client,
)
- mock_keyring.set_password.assert_has_calls(
- [
- mock.call("airflowctl", "api_token_production",
"TEST_TOKEN"),
- ]
+
+ mock_keyring.set_password.assert_called_once_with("airflowctl",
"api_token_staging", "TEST_TOKEN")
+
+ @patch("airflowctl.api.client.keyring")
+ def test_login_prompts_for_password_when_username_provided(self,
mock_keyring, api_client_maker):
+ """Test that login prompts only for password when --username is
supplied but --password is not."""
+ api_client = api_client_maker(
+ path="/auth/token/cli",
+ response_json=self.login_response.model_dump(),
+ expected_http_status_code=201,
+ kind=ClientKind.AUTH,
+ )
+
+ mock_keyring.set_password = mock.MagicMock()
+ mock_keyring.get_password.return_value = None
+
+ tty_stdin = mock.MagicMock()
+ tty_stdin.isatty.return_value = True
+
+ with (
+ patch("sys.stdin", tty_stdin),
+ patch("builtins.input") as mock_input,
+ patch("airflowctl.ctl.commands.auth_command.getpass.getpass",
return_value="prompted_pass"),
+ ):
+ auth_command.login(
+ self.parser.parse_args(
+ ["auth", "login", "--api-url", "http://localhost:8080",
"--username", "known_user"]
+ ),
+ api_client=api_client,
+ )
+ mock_input.assert_not_called()
+
+ mock_keyring.set_password.assert_called_once_with("airflowctl",
"api_token_production", "TEST_TOKEN")
+
+ def test_login_no_credentials_non_interactive_exits(self,
api_client_maker):
+ """Test that login exits with an error when no credentials are
supplied in a non-interactive context."""
+ api_client = api_client_maker(
+ path="/auth/token/cli",
+ response_json=self.login_response.model_dump(),
+ expected_http_status_code=201,
+ kind=ClientKind.AUTH,
+ )
+
+ non_tty_stdin = mock.MagicMock()
+ non_tty_stdin.isatty.return_value = False
+
+ with (
+ patch("sys.stdin", non_tty_stdin),
+ pytest.raises(SystemExit, match="1"),
+ ):
+ auth_command.login(
+ self.parser.parse_args(["auth", "login", "--api-url",
"http://localhost:8080"]),
+ api_client=api_client,
)
@patch("airflowctl.api.client.keyring")
@@ -165,11 +238,7 @@ class TestCliAuthCommands:
)
mock_keyring.set_password.side_effect = NoKeyringError("no backend")
- with (
- patch("sys.stdin", io.StringIO("test_password")),
- patch("airflowctl.ctl.cli_config.getpass.getpass",
return_value="test_password"),
- pytest.raises(SystemExit, match="1"),
- ):
+ with pytest.raises(SystemExit, match="1"):
auth_command.login(
self.parser.parse_args(
[
@@ -180,6 +249,7 @@ class TestCliAuthCommands:
"--username",
"test_user",
"--password",
+ "test_password",
]
),
api_client=api_client,