This is an automated email from the ASF dual-hosted git repository.
jscheffl 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 0fce3b6047 Add "airflow users reset-password" command (#37044)
0fce3b6047 is described below
commit 0fce3b6047dcae037cfd8a5bd0638894c36509ab
Author: Aritra Basu <[email protected]>
AuthorDate: Sun Jan 28 17:37:16 2024 +0530
Add "airflow users reset-password" command (#37044)
Added support for reset-password command which
can either randomly generate a password, or
take a password from user.
---
.../fab/auth_manager/cli_commands/definition.py | 21 +++++++++
.../fab/auth_manager/cli_commands/user_command.py | 37 ++++++++++-----
.../fab/auth_manager/security_manager/override.py | 7 +--
.../auth_manager/cli_commands/test_definition.py | 2 +-
.../auth_manager/cli_commands/test_user_command.py | 52 ++++++++++++++++++++++
5 files changed, 105 insertions(+), 14 deletions(-)
diff --git a/airflow/providers/fab/auth_manager/cli_commands/definition.py
b/airflow/providers/fab/auth_manager/cli_commands/definition.py
index 1ed36e58d7..c7be5270d5 100644
--- a/airflow/providers/fab/auth_manager/cli_commands/definition.py
+++ b/airflow/providers/fab/auth_manager/cli_commands/definition.py
@@ -136,6 +136,27 @@ USERS_COMMANDS = (
" --email [email protected]"
),
),
+ ActionCommand(
+ name="reset-password",
+ help="Reset a user's password",
+ func=lazy_load_command(
+
"airflow.providers.fab.auth_manager.cli_commands.user_command.user_reset_password"
+ ),
+ args=(
+ ARG_USERNAME_OPTIONAL,
+ ARG_EMAIL_OPTIONAL,
+ ARG_PASSWORD,
+ ARG_USE_RANDOM_PASSWORD,
+ ARG_VERBOSE,
+ ),
+ epilog=(
+ "examples:\n"
+ 'To reset an user with username equals to "admin", run:\n'
+ "\n"
+ " $ airflow users reset-password \\\n"
+ " --username admin"
+ ),
+ ),
ActionCommand(
name="delete",
help="Delete a user",
diff --git a/airflow/providers/fab/auth_manager/cli_commands/user_command.py
b/airflow/providers/fab/auth_manager/cli_commands/user_command.py
index a53425ca5d..a9b48f79d9 100644
--- a/airflow/providers/fab/auth_manager/cli_commands/user_command.py
+++ b/airflow/providers/fab/auth_manager/cli_commands/user_command.py
@@ -70,16 +70,7 @@ def users_create(args):
if not role:
valid_roles = appbuilder.sm.get_all_roles()
raise SystemExit(f"{args.role} is not a valid role. Valid roles
are: {valid_roles}")
- if args.use_random_password:
- password = "".join(random.choices(string.printable, k=16))
- elif args.password:
- password = args.password
- else:
- password = getpass.getpass("Password:")
- password_confirmation = getpass.getpass("Repeat for confirmation:")
- if password != password_confirmation:
- raise SystemExit("Passwords did not match")
-
+ password = _create_password(args)
if appbuilder.sm.find_user(args.username):
print(f"{args.username} already exist in the db")
return
@@ -106,6 +97,32 @@ def _find_user(args):
return user
+@cli_utils.action_cli
+@providers_configuration_loaded
+def user_reset_password(args):
+ """Reset user password user from DB."""
+ user = _find_user(args)
+ password = _create_password(args)
+ with get_application_builder() as appbuilder:
+ if appbuilder.sm.reset_password(user.id, password):
+ print(f'User "{user.username}" password reset successfully')
+ else:
+ raise SystemExit("Failed to reset user password")
+
+
+def _create_password(args):
+ if args.use_random_password:
+ password = "".join(random.choices(string.printable, k=16))
+ elif args.password:
+ password = args.password
+ else:
+ password = getpass.getpass("Password:")
+ password_confirmation = getpass.getpass("Repeat for confirmation:")
+ if password != password_confirmation:
+ raise SystemExit("Passwords did not match")
+ return password
+
+
@cli_utils.action_cli
@providers_configuration_loaded
def users_delete(args):
diff --git a/airflow/providers/fab/auth_manager/security_manager/override.py
b/airflow/providers/fab/auth_manager/security_manager/override.py
index be626a1af5..d800e8fc35 100644
--- a/airflow/providers/fab/auth_manager/security_manager/override.py
+++ b/airflow/providers/fab/auth_manager/security_manager/override.py
@@ -458,7 +458,7 @@ class
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
jwt_manager.init_app(self.appbuilder.app)
jwt_manager.user_lookup_loader(self.load_user_jwt)
- def reset_password(self, userid, password):
+ def reset_password(self, userid: int, password: str) -> bool:
"""
Change/Reset a user's password for auth db.
@@ -470,7 +470,7 @@ class
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
user = self.get_user_by_id(userid)
user.password = generate_password_hash(password)
self.reset_user_sessions(user)
- self.update_user(user)
+ return self.update_user(user)
def reset_user_sessions(self, user: User) -> None:
if isinstance(self.appbuilder.get_app.session_interface,
AirflowDatabaseSessionInterface):
@@ -1536,7 +1536,7 @@ class
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
.limit(1)
)
- def update_user(self, user):
+ def update_user(self, user: User) -> bool:
try:
self.get_session.merge(user)
self.get_session.commit()
@@ -1545,6 +1545,7 @@ class
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
log.error(const.LOGMSG_ERR_SEC_UPD_USER, e)
self.get_session.rollback()
return False
+ return True
def del_register_user(self, register_user):
"""
diff --git a/tests/providers/fab/auth_manager/cli_commands/test_definition.py
b/tests/providers/fab/auth_manager/cli_commands/test_definition.py
index 696179354a..b34f92875b 100644
--- a/tests/providers/fab/auth_manager/cli_commands/test_definition.py
+++ b/tests/providers/fab/auth_manager/cli_commands/test_definition.py
@@ -25,7 +25,7 @@ from
airflow.providers.fab.auth_manager.cli_commands.definition import (
class TestCliDefinition:
def test_users_commands(self):
- assert len(USERS_COMMANDS) == 7
+ assert len(USERS_COMMANDS) == 8
def test_roles_commands(self):
assert len(ROLES_COMMANDS) == 7
diff --git a/tests/providers/fab/auth_manager/cli_commands/test_user_command.py
b/tests/providers/fab/auth_manager/cli_commands/test_user_command.py
index 47016a5ddf..0e038310a4 100644
--- a/tests/providers/fab/auth_manager/cli_commands/test_user_command.py
+++ b/tests/providers/fab/auth_manager/cli_commands/test_user_command.py
@@ -472,3 +472,55 @@ class TestCliUsers:
def test_cli_import_users_exceptions(self, user, message):
with pytest.raises(SystemExit, match=re.escape(message)):
self._import_users_from_file([user])
+
+ def test_cli_reset_user_password(self):
+ args = self.parser.parse_args(
+ [
+ "users",
+ "create",
+ "--username",
+ "test3",
+ "--lastname",
+ "doe",
+ "--firstname",
+ "jon",
+ "--email",
+ "[email protected]",
+ "--role",
+ "Viewer",
+ "--use-random-password",
+ ]
+ )
+ user_command.users_create(args)
+ args = self.parser.parse_args(
+ ["users", "reset-password", "--username", "test3",
"--use-random-password"]
+ )
+ with redirect_stdout(StringIO()) as stdout:
+ user_command.user_reset_password(args)
+ assert 'User "test3" password reset successfully' in stdout.getvalue()
+
+ def test_cli_reset_user_password_with_email(self):
+ args = self.parser.parse_args(
+ [
+ "users",
+ "create",
+ "--username",
+ "test3",
+ "--lastname",
+ "doe",
+ "--firstname",
+ "jon",
+ "--email",
+ "[email protected]",
+ "--role",
+ "Viewer",
+ "--use-random-password",
+ ]
+ )
+ user_command.users_create(args)
+ args = self.parser.parse_args(
+ ["users", "reset-password", "--email", "[email protected]",
"--password", "s3cr3t"]
+ )
+ with redirect_stdout(StringIO()) as stdout:
+ user_command.user_reset_password(args)
+ assert 'User "test3" password reset successfully' in stdout.getvalue()