Skip to content

Commit af5e469

Browse files
authored
Passwords from stdin in Keycloak Provider (#59119)
* implement cli password action * keycloak: get password from cli command or stdin * add password action in keycloak version_compat * Password action only for keycloak * remove unneeded code
1 parent 861cf54 commit af5e469

File tree

3 files changed

+102
-4
lines changed

3 files changed

+102
-4
lines changed

providers/keycloak/docs/auth-manager/manage/permissions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ There are two options to create the permissions:
3737
CLI commands take the following parameters:
3838

3939
* ``--username``: Keycloak admin username
40-
* ``--password``: Keycloak admin password
40+
* ``--password``: Keycloak admin password. Specifying the parameter without a value will prompt for the password securely.
4141
* ``--user-realm``: Keycloak user realm
4242
* ``--client-id``: Keycloak client id (default: admin-cli)
4343

providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/definition.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,25 @@
1717

1818
from __future__ import annotations
1919

20+
import argparse
21+
import getpass
22+
2023
from airflow.cli.cli_config import (
2124
ActionCommand,
2225
Arg,
2326
lazy_load_command,
2427
)
2528

29+
30+
class Password(argparse.Action):
31+
"""Custom action to prompt for password input."""
32+
33+
def __call__(self, parser, namespace, values, option_string=None):
34+
if values is None:
35+
values = getpass.getpass(prompt="Password: ")
36+
setattr(namespace, self.dest, values)
37+
38+
2639
############
2740
# # ARGS # #
2841
############
@@ -33,7 +46,11 @@
3346
)
3447
ARG_PASSWORD = Arg(
3548
("--password",),
36-
help="Password associated to the user used to create resources",
49+
help="Password associated to the user used to create resources. If not provided, you will be prompted to enter it.",
50+
action=Password,
51+
nargs="?",
52+
dest="password",
53+
type=str,
3754
)
3855
ARG_USER_REALM = Arg(
3956
("--user-realm",), help="Realm name where the user used to create resources is", default="master"

providers/keycloak/tests/unit/keycloak/auth_manager/cli/test_definition.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,90 @@
1616
# under the License.
1717
from __future__ import annotations
1818

19-
from airflow.providers.keycloak.auth_manager.cli.definition import KEYCLOAK_AUTH_MANAGER_COMMANDS
19+
import argparse
20+
import importlib
21+
from unittest.mock import patch
22+
23+
import pytest
24+
25+
from airflow.cli import cli_parser
26+
from airflow.providers.keycloak.auth_manager.cli.definition import KEYCLOAK_AUTH_MANAGER_COMMANDS, Password
27+
28+
from tests_common.test_utils.config import conf_vars
2029

2130

2231
class TestKeycloakCliDefinition:
23-
def test_aws_auth_manager_cli_commands(self):
32+
@classmethod
33+
def setup_class(cls):
34+
with conf_vars(
35+
{
36+
(
37+
"core",
38+
"auth_manager",
39+
): "airflow.providers.keycloak.auth_manager.keycloak_auth_manager.KeycloakAuthManager",
40+
}
41+
):
42+
importlib.reload(cli_parser)
43+
cls.arg_parser = cli_parser.get_parser()
44+
45+
def test_keycloak_auth_manager_cli_commands(self):
2446
assert len(KEYCLOAK_AUTH_MANAGER_COMMANDS) == 4
47+
48+
@pytest.mark.parametrize(
49+
"command",
50+
["create-scopes", "create-resources", "create-permissions", "create-all"],
51+
)
52+
def test_password_with_explicit_value(self, command):
53+
"""Test commands are defined correctly to allow passing password explicitly via --password value."""
54+
params = [
55+
"keycloak-auth-manager",
56+
command,
57+
"--username",
58+
"test",
59+
"--password",
60+
"my_password",
61+
]
62+
args = self.arg_parser.parse_args(params)
63+
assert args.password == "my_password"
64+
65+
@pytest.mark.parametrize(
66+
"command",
67+
["create-scopes", "create-resources", "create-permissions", "create-all"],
68+
)
69+
@patch("getpass.getpass", return_value="stdin_password")
70+
def test_password_from_stdin(self, mock_getpass, command):
71+
"""Test commands are defined correctly to allow password prompting from stdin when --password has no value."""
72+
params = [
73+
"keycloak-auth-manager",
74+
command,
75+
"--username",
76+
"test",
77+
"--password",
78+
]
79+
args = self.arg_parser.parse_args(params)
80+
mock_getpass.assert_called_once_with(prompt="Password: ")
81+
assert args.password == "stdin_password"
82+
83+
84+
class TestPasswordAction:
85+
"""Tests for the Password argparse action."""
86+
87+
def test_password_with_explicit_value(self):
88+
"""Test passing password explicitly via --password value."""
89+
90+
parser = argparse.ArgumentParser()
91+
parser.add_argument("--password", action=Password, nargs="?", dest="password")
92+
93+
args = parser.parse_args(["--password", "my_password"])
94+
assert args.password == "my_password"
95+
96+
@patch("getpass.getpass", return_value="stdin_password")
97+
def test_password_from_stdin(self, mock_getpass):
98+
"""Test password prompted from stdin when --password has no value."""
99+
100+
parser = argparse.ArgumentParser()
101+
parser.add_argument("--password", action=Password, nargs="?", dest="password")
102+
103+
args = parser.parse_args(["--password"])
104+
mock_getpass.assert_called_once_with(prompt="Password: ")
105+
assert args.password == "stdin_password"

0 commit comments

Comments
 (0)