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&#160;auth&#160;login</text><text
 class="terminal-938154658-r2" x="341.6" y="20" textLength="24.4" 
clip-path="url(#terminal-938154658-line-0)">&#160;[</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)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</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)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</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)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</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&#160;to&#160;the&#160;metadata&#160;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)">,&#160;</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&#160;token&#160;to&#160;use&#160;for&#160;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&#160;URL&#160;of&#160;the&#160;metadata&#160;database&#160;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)">,&#160;</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)">&#160;[</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&#160;password&#160;to&#160;use&#160;for&#160;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&#160;storing&#160;credentials&#160;in&#160;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&#160;username&#160;to&#160;use&#160;for&#160;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&#160;auth&#160;login</text><text
 class="terminal-2514228680-r2" x="341.6" y="20" textLength="24.4" 
clip-path="url(#terminal-2514228680-line-0)">&#160;[</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)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</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)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</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)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</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&#160;to&#160;the&#160;metadata&#160;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)">,&#160;</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&#160;token&#160;to&#160;use&#160;for&#160;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&#160;URL&#160;of&#160;the&#160;metadata&#160;database&#160;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)">,&#160;</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&#160;password&#160;to&#160;use&#160;for&#160;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&#160;storing&#160;credentials&#160;in&#160;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&#160;username&#160;to&#160;use&#160;for&#160;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,

Reply via email to