This is an automated email from the ASF dual-hosted git repository.

onikolas 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 b1a3b42880 Make auth managers provide their own airflow CLI commands 
(#33481)
b1a3b42880 is described below

commit b1a3b4288022c67db22cbc7d24b0c4b2b122453b
Author: RaphaĆ«l Vandon <[email protected]>
AuthorDate: Wed Aug 23 14:57:53 2023 -0700

    Make auth managers provide their own airflow CLI commands (#33481)
    
    * add a way for auth managers to define their own CLI commands
    * extract command definition to a different file for readability
    * static method to get cli commands + import optims
    * move import in method to save exec time
---
 airflow/auth/managers/base_auth_manager.py         |  11 +-
 .../managers/fab/cli_commands/__init__.py}         |  29 +--
 .../auth/managers/fab/cli_commands/definition.py   | 220 +++++++++++++++++++++
 .../managers/fab/cli_commands}/role_command.py     |  12 +-
 .../fab/cli_commands}/sync_perm_command.py         |   2 +-
 .../managers/fab/cli_commands}/user_command.py     |  15 +-
 .../managers/fab/cli_commands/utils.py}            |   0
 airflow/auth/managers/fab/fab_auth_manager.py      |  52 ++++-
 airflow/cli/cli_config.py                          | 206 +------------------
 airflow/cli/cli_parser.py                          |   8 +
 airflow/cli/commands/standalone_command.py         |   2 +-
 .../providers/celery/executors/celery_executor.py  |   2 +-
 airflow/www/extensions/init_auth_manager.py        |  20 +-
 .../auth/managers/fab/cli_commands/__init__.py     |  28 ---
 .../fab/cli_commands}/test_role_command.py         |  10 +-
 .../fab/cli_commands}/test_sync_perm_command.py    |   6 +-
 .../fab/cli_commands}/test_user_command.py         |   8 +-
 tests/auth/managers/fab/test_fab_auth_manager.py   |   8 +-
 18 files changed, 325 insertions(+), 314 deletions(-)

diff --git a/airflow/auth/managers/base_auth_manager.py 
b/airflow/auth/managers/base_auth_manager.py
index bcc5e13892..a512804b4c 100644
--- a/airflow/auth/managers/base_auth_manager.py
+++ b/airflow/auth/managers/base_auth_manager.py
@@ -20,11 +20,12 @@ from __future__ import annotations
 from abc import abstractmethod
 from typing import TYPE_CHECKING
 
-from airflow.auth.managers.models.base_user import BaseUser
 from airflow.exceptions import AirflowException
 from airflow.utils.log.logging_mixin import LoggingMixin
 
 if TYPE_CHECKING:
+    from airflow.auth.managers.models.base_user import BaseUser
+    from airflow.cli.cli_config import CLICommand
     from airflow.www.security import AirflowSecurityManager
 
 
@@ -38,6 +39,14 @@ class BaseAuthManager(LoggingMixin):
     def __init__(self):
         self._security_manager: AirflowSecurityManager | None = None
 
+    @staticmethod
+    def get_cli_commands() -> list[CLICommand]:
+        """Vends CLI commands to be included in Airflow CLI.
+
+        Override this method to expose commands via Airflow CLI to manage this 
auth manager.
+        """
+        return []
+
     @abstractmethod
     def get_user_name(self) -> str:
         """Return the username associated to the user in session."""
diff --git a/airflow/www/extensions/init_auth_manager.py 
b/airflow/auth/managers/fab/cli_commands/__init__.py
similarity index 50%
copy from airflow/www/extensions/init_auth_manager.py
copy to airflow/auth/managers/fab/cli_commands/__init__.py
index a53fdf304b..217e5db960 100644
--- a/airflow/www/extensions/init_auth_manager.py
+++ b/airflow/auth/managers/fab/cli_commands/__init__.py
@@ -1,3 +1,4 @@
+#
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
@@ -14,31 +15,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-
-from airflow.compat.functools import cache
-from airflow.configuration import conf
-from airflow.exceptions import AirflowConfigException
-
-if TYPE_CHECKING:
-    from airflow.auth.managers.base_auth_manager import BaseAuthManager
-
-
-@cache
-def get_auth_manager() -> BaseAuthManager:
-    """
-    Initialize auth manager.
-
-    Import the user manager class, instantiate it and return it.
-    """
-    auth_manager_cls = conf.getimport(section="core", key="auth_manager")
-
-    if not auth_manager_cls:
-        raise AirflowConfigException(
-            "No auth manager defined in the config. "
-            "Please specify one using section/key [core/auth_manager]."
-        )
-
-    return auth_manager_cls()
diff --git a/airflow/auth/managers/fab/cli_commands/definition.py 
b/airflow/auth/managers/fab/cli_commands/definition.py
new file mode 100644
index 0000000000..478f6d8d30
--- /dev/null
+++ b/airflow/auth/managers/fab/cli_commands/definition.py
@@ -0,0 +1,220 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import textwrap
+
+from airflow.cli.cli_config import (
+    ARG_OUTPUT,
+    ARG_VERBOSE,
+    ActionCommand,
+    Arg,
+    lazy_load_command,
+)
+
+############
+# # ARGS # #
+############
+
+# users
+ARG_USERNAME = Arg(("-u", "--username"), help="Username of the user", 
required=True, type=str)
+ARG_USERNAME_OPTIONAL = Arg(("-u", "--username"), help="Username of the user", 
type=str)
+ARG_FIRSTNAME = Arg(("-f", "--firstname"), help="First name of the user", 
required=True, type=str)
+ARG_LASTNAME = Arg(("-l", "--lastname"), help="Last name of the user", 
required=True, type=str)
+ARG_ROLE = Arg(
+    ("-r", "--role"),
+    help="Role of the user. Existing roles include Admin, User, Op, Viewer, 
and Public",
+    required=True,
+    type=str,
+)
+ARG_EMAIL = Arg(("-e", "--email"), help="Email of the user", required=True, 
type=str)
+ARG_EMAIL_OPTIONAL = Arg(("-e", "--email"), help="Email of the user", type=str)
+ARG_PASSWORD = Arg(
+    ("-p", "--password"),
+    help="Password of the user, required to create a user without 
--use-random-password",
+    type=str,
+)
+ARG_USE_RANDOM_PASSWORD = Arg(
+    ("--use-random-password",),
+    help="Do not prompt for password. Use random string instead."
+    " Required to create a user without --password ",
+    default=False,
+    action="store_true",
+)
+ARG_USER_IMPORT = Arg(
+    ("import",),
+    metavar="FILEPATH",
+    help="Import users from JSON file. Example format::\n"
+    + textwrap.indent(
+        textwrap.dedent(
+            """
+            [
+                {
+                    "email": "[email protected]",
+                    "firstname": "Jon",
+                    "lastname": "Doe",
+                    "roles": ["Public"],
+                    "username": "jondoe"
+                }
+            ]"""
+        ),
+        " " * 4,
+    ),
+)
+ARG_USER_EXPORT = Arg(("export",), metavar="FILEPATH", help="Export all users 
to JSON file")
+
+# roles
+ARG_CREATE_ROLE = Arg(("-c", "--create"), help="Create a new role", 
action="store_true")
+ARG_LIST_ROLES = Arg(("-l", "--list"), help="List roles", action="store_true")
+ARG_ROLES = Arg(("role",), help="The name of a role", nargs="*")
+ARG_PERMISSIONS = Arg(("-p", "--permission"), help="Show role permissions", 
action="store_true")
+ARG_ROLE_RESOURCE = Arg(("-r", "--resource"), help="The name of permissions", 
nargs="*", required=True)
+ARG_ROLE_ACTION = Arg(("-a", "--action"), help="The action of permissions", 
nargs="*")
+ARG_ROLE_ACTION_REQUIRED = Arg(("-a", "--action"), help="The action of 
permissions", nargs="*", required=True)
+
+ARG_ROLE_IMPORT = Arg(("file",), help="Import roles from JSON file", 
nargs=None)
+ARG_ROLE_EXPORT = Arg(("file",), help="Export all roles to JSON file", 
nargs=None)
+ARG_ROLE_EXPORT_FMT = Arg(
+    ("-p", "--pretty"),
+    help="Format output JSON file by sorting role names and indenting by 4 
spaces",
+    action="store_true",
+)
+
+# sync-perm
+ARG_INCLUDE_DAGS = Arg(
+    ("--include-dags",), help="If passed, DAG specific permissions will also 
be synced.", action="store_true"
+)
+
+################
+# # COMMANDS # #
+################
+
+USERS_COMMANDS = (
+    ActionCommand(
+        name="list",
+        help="List users",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.user_command.users_list"),
+        args=(ARG_OUTPUT, ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="create",
+        help="Create a user",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.user_command.users_create"),
+        args=(
+            ARG_ROLE,
+            ARG_USERNAME,
+            ARG_EMAIL,
+            ARG_FIRSTNAME,
+            ARG_LASTNAME,
+            ARG_PASSWORD,
+            ARG_USE_RANDOM_PASSWORD,
+            ARG_VERBOSE,
+        ),
+        epilog=(
+            "examples:\n"
+            'To create an user with "Admin" role and username equals to 
"admin", run:\n'
+            "\n"
+            "    $ airflow users create \\\n"
+            "          --username admin \\\n"
+            "          --firstname FIRST_NAME \\\n"
+            "          --lastname LAST_NAME \\\n"
+            "          --role Admin \\\n"
+            "          --email [email protected]"
+        ),
+    ),
+    ActionCommand(
+        name="delete",
+        help="Delete a user",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.user_command.users_delete"),
+        args=(ARG_USERNAME_OPTIONAL, ARG_EMAIL_OPTIONAL, ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="add-role",
+        help="Add role to a user",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.user_command.add_role"),
+        args=(ARG_USERNAME_OPTIONAL, ARG_EMAIL_OPTIONAL, ARG_ROLE, 
ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="remove-role",
+        help="Remove role from a user",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.user_command.remove_role"),
+        args=(ARG_USERNAME_OPTIONAL, ARG_EMAIL_OPTIONAL, ARG_ROLE, 
ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="import",
+        help="Import users",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.user_command.users_import"),
+        args=(ARG_USER_IMPORT, ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="export",
+        help="Export all users",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.user_command.users_export"),
+        args=(ARG_USER_EXPORT, ARG_VERBOSE),
+    ),
+)
+ROLES_COMMANDS = (
+    ActionCommand(
+        name="list",
+        help="List roles",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.role_command.roles_list"),
+        args=(ARG_PERMISSIONS, ARG_OUTPUT, ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="create",
+        help="Create role",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.role_command.roles_create"),
+        args=(ARG_ROLES, ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="delete",
+        help="Delete role",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.role_command.roles_delete"),
+        args=(ARG_ROLES, ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="add-perms",
+        help="Add roles permissions",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.role_command.roles_add_perms"),
+        args=(ARG_ROLES, ARG_ROLE_RESOURCE, ARG_ROLE_ACTION_REQUIRED, 
ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="del-perms",
+        help="Delete roles permissions",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.role_command.roles_del_perms"),
+        args=(ARG_ROLES, ARG_ROLE_RESOURCE, ARG_ROLE_ACTION, ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="export",
+        help="Export roles (without permissions) from db to JSON file",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.role_command.roles_export"),
+        args=(ARG_ROLE_EXPORT, ARG_ROLE_EXPORT_FMT, ARG_VERBOSE),
+    ),
+    ActionCommand(
+        name="import",
+        help="Import roles (without permissions) from JSON file to db",
+        
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.role_command.roles_import"),
+        args=(ARG_ROLE_IMPORT, ARG_VERBOSE),
+    ),
+)
+
+SYNC_PERM_COMMAND = ActionCommand(
+    name="sync-perm",
+    help="Update permissions for existing roles and optionally DAGs",
+    
func=lazy_load_command("airflow.auth.managers.fab.cli_commands.sync_perm_command.sync_perm"),
+    args=(ARG_INCLUDE_DAGS, ARG_VERBOSE),
+)
diff --git a/airflow/cli/commands/role_command.py 
b/airflow/auth/managers/fab/cli_commands/role_command.py
similarity index 94%
rename from airflow/cli/commands/role_command.py
rename to airflow/auth/managers/fab/cli_commands/role_command.py
index a582b33195..34ea8fb9d3 100644
--- a/airflow/cli/commands/role_command.py
+++ b/airflow/auth/managers/fab/cli_commands/role_command.py
@@ -23,6 +23,7 @@ import itertools
 import json
 import os
 
+from airflow.auth.managers.fab.cli_commands.utils import 
get_application_builder
 from airflow.auth.managers.fab.models import Action, Permission, Resource, Role
 from airflow.cli.simple_table import AirflowConsole
 from airflow.utils import cli as cli_utils
@@ -35,8 +36,6 @@ from airflow.www.security import EXISTING_ROLES
 @providers_configuration_loaded
 def roles_list(args):
     """List all existing roles."""
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         roles = appbuilder.sm.get_all_roles()
 
@@ -63,8 +62,6 @@ def roles_list(args):
 @providers_configuration_loaded
 def roles_create(args):
     """Create new empty role in DB."""
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         for role_name in args.role:
             appbuilder.sm.add_role(role_name)
@@ -76,8 +73,6 @@ def roles_create(args):
 @providers_configuration_loaded
 def roles_delete(args):
     """Delete role in DB."""
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         for role_name in args.role:
             role = appbuilder.sm.find_role(role_name)
@@ -90,8 +85,6 @@ def roles_delete(args):
 
 
 def __roles_add_or_remove_permissions(args):
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         is_add: bool = args.subcommand.startswith("add")
 
@@ -165,8 +158,6 @@ def roles_export(args):
     Note, this function does not export the permissions associated for each 
role.
     Strictly, it exports the role names into the passed role json file.
     """
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         roles = appbuilder.sm.get_all_roles()
         exporting_roles = [role.name for role in roles if role.name not in 
EXISTING_ROLES]
@@ -196,7 +187,6 @@ def roles_import(args):
     except ValueError as e:
         print(f"File '{json_file}' is not a valid JSON file. Error: {e}")
         exit(1)
-    from airflow.utils.cli_app_builder import get_application_builder
 
     with get_application_builder() as appbuilder:
         existing_roles = [role.name for role in appbuilder.sm.get_all_roles()]
diff --git a/airflow/cli/commands/sync_perm_command.py 
b/airflow/auth/managers/fab/cli_commands/sync_perm_command.py
similarity index 94%
rename from airflow/cli/commands/sync_perm_command.py
rename to airflow/auth/managers/fab/cli_commands/sync_perm_command.py
index 4d4e280637..14b6e58bbb 100644
--- a/airflow/cli/commands/sync_perm_command.py
+++ b/airflow/auth/managers/fab/cli_commands/sync_perm_command.py
@@ -26,7 +26,7 @@ from airflow.utils.providers_configuration_loader import 
providers_configuration
 @providers_configuration_loaded
 def sync_perm(args):
     """Update permissions for existing roles and DAGs."""
-    from airflow.utils.cli_app_builder import get_application_builder
+    from airflow.auth.managers.fab.cli_commands.utils import 
get_application_builder
 
     with get_application_builder() as appbuilder:
         print("Updating actions and resources for all existing roles")
diff --git a/airflow/cli/commands/user_command.py 
b/airflow/auth/managers/fab/cli_commands/user_command.py
similarity index 95%
rename from airflow/cli/commands/user_command.py
rename to airflow/auth/managers/fab/cli_commands/user_command.py
index bc982719c9..84e6318e40 100644
--- a/airflow/cli/commands/user_command.py
+++ b/airflow/auth/managers/fab/cli_commands/user_command.py
@@ -29,6 +29,7 @@ import re2
 from marshmallow import Schema, fields, validate
 from marshmallow.exceptions import ValidationError
 
+from airflow.auth.managers.fab.cli_commands.utils import 
get_application_builder
 from airflow.cli.simple_table import AirflowConsole
 from airflow.utils import cli as cli_utils
 from airflow.utils.cli import suppress_logs_and_warning
@@ -50,8 +51,6 @@ class UserSchema(Schema):
 @providers_configuration_loaded
 def users_list(args):
     """List users at the command line."""
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         users = appbuilder.sm.get_all_users()
         fields = ["id", "username", "email", "first_name", "last_name", 
"roles"]
@@ -65,8 +64,6 @@ def users_list(args):
 @providers_configuration_loaded
 def users_create(args):
     """Create new user in the DB."""
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         role = appbuilder.sm.find_role(args.role)
         if not role:
@@ -101,8 +98,6 @@ def _find_user(args):
     if args.username and args.email:
         raise SystemExit("Conflicting args: must supply either --username or 
--email, but not both")
 
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         user = appbuilder.sm.find_user(username=args.username, 
email=args.email)
         if not user:
@@ -119,8 +114,6 @@ def users_delete(args):
     # Clear the associated user roles first.
     user.roles.clear()
 
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         if appbuilder.sm.del_register_user(user):
             print(f'User "{user.username}" deleted')
@@ -134,8 +127,6 @@ def users_manage_role(args, remove=False):
     """Delete or appends user roles."""
     user = _find_user(args)
 
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         role = appbuilder.sm.find_role(args.role)
         if not role:
@@ -161,8 +152,6 @@ def users_manage_role(args, remove=False):
 @providers_configuration_loaded
 def users_export(args):
     """Export all users to the json file."""
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         users = appbuilder.sm.get_all_users()
         fields = ["id", "username", "email", "first_name", "last_name", 
"roles"]
@@ -211,8 +200,6 @@ def users_import(args):
 
 
 def _import_users(users_list: list[dict[str, Any]]):
-    from airflow.utils.cli_app_builder import get_application_builder
-
     with get_application_builder() as appbuilder:
         users_created = []
         users_updated = []
diff --git a/airflow/utils/cli_app_builder.py 
b/airflow/auth/managers/fab/cli_commands/utils.py
similarity index 100%
rename from airflow/utils/cli_app_builder.py
rename to airflow/auth/managers/fab/cli_commands/utils.py
diff --git a/airflow/auth/managers/fab/fab_auth_manager.py 
b/airflow/auth/managers/fab/fab_auth_manager.py
index 848f5ff188..def0590b1f 100644
--- a/airflow/auth/managers/fab/fab_auth_manager.py
+++ b/airflow/auth/managers/fab/fab_auth_manager.py
@@ -17,13 +17,22 @@
 # under the License.
 from __future__ import annotations
 
-from flask import url_for
-from flask_login import current_user
+from typing import TYPE_CHECKING
 
 from airflow import AirflowException
 from airflow.auth.managers.base_auth_manager import BaseAuthManager
-from airflow.auth.managers.fab.models import User
-from airflow.auth.managers.fab.security_manager.override import 
FabAirflowSecurityManagerOverride
+from airflow.auth.managers.fab.cli_commands.definition import (
+    ROLES_COMMANDS,
+    SYNC_PERM_COMMAND,
+    USERS_COMMANDS,
+)
+from airflow.cli.cli_config import (
+    CLICommand,
+    GroupCommand,
+)
+
+if TYPE_CHECKING:
+    from airflow.auth.managers.fab.models import User
 
 
 class FabAuthManager(BaseAuthManager):
@@ -33,6 +42,23 @@ class FabAuthManager(BaseAuthManager):
     This auth manager is responsible for providing a backward compatible user 
management experience to users.
     """
 
+    @staticmethod
+    def get_cli_commands() -> list[CLICommand]:
+        """Vends CLI commands to be included in Airflow CLI."""
+        return [
+            GroupCommand(
+                name="users",
+                help="Manage users",
+                subcommands=USERS_COMMANDS,
+            ),
+            GroupCommand(
+                name="roles",
+                help="Manage roles",
+                subcommands=ROLES_COMMANDS,
+            ),
+            SYNC_PERM_COMMAND,  # not in a command group
+        ]
+
     def get_user_name(self) -> str:
         """
         Return the username associated to the user in session.
@@ -47,6 +73,8 @@ class FabAuthManager(BaseAuthManager):
 
     def get_user(self) -> User:
         """Return the user associated to the user in session."""
+        from flask_login import current_user
+
         return current_user
 
     def get_user_id(self) -> str:
@@ -59,25 +87,33 @@ class FabAuthManager(BaseAuthManager):
 
     def get_security_manager_override_class(self) -> type:
         """Return the security manager override."""
+        from airflow.auth.managers.fab.security_manager.override import 
FabAirflowSecurityManagerOverride
+
         return FabAirflowSecurityManagerOverride
 
+    def url_for(self, *args, **kwargs):
+        """Wrapper to allow mocking without having to import at the top of the 
file."""
+        from flask import url_for
+
+        return url_for(*args, **kwargs)
+
     def get_url_login(self, **kwargs) -> str:
         """Return the login page url."""
         if not self.security_manager.auth_view:
             raise AirflowException("`auth_view` not defined in the security 
manager.")
         if "next_url" in kwargs and kwargs["next_url"]:
-            return 
url_for(f"{self.security_manager.auth_view.endpoint}.login", 
next=kwargs["next_url"])
+            return 
self.url_for(f"{self.security_manager.auth_view.endpoint}.login", 
next=kwargs["next_url"])
         else:
-            return url_for(f"{self.security_manager.auth_view.endpoint}.login")
+            return 
self.url_for(f"{self.security_manager.auth_view.endpoint}.login")
 
     def get_url_logout(self):
         """Return the logout page url."""
         if not self.security_manager.auth_view:
             raise AirflowException("`auth_view` not defined in the security 
manager.")
-        return url_for(f"{self.security_manager.auth_view.endpoint}.logout")
+        return 
self.url_for(f"{self.security_manager.auth_view.endpoint}.logout")
 
     def get_url_user_profile(self) -> str | None:
         """Return the url to a page displaying info about the current user."""
         if not self.security_manager.user_view:
             return None
-        return url_for(f"{self.security_manager.user_view.endpoint}.userinfo")
+        return 
self.url_for(f"{self.security_manager.user_view.endpoint}.userinfo")
diff --git a/airflow/cli/cli_config.py b/airflow/cli/cli_config.py
index de1d43be6f..c06bd32d96 100644
--- a/airflow/cli/cli_config.py
+++ b/airflow/cli/cli_config.py
@@ -229,6 +229,12 @@ ARG_REVISION_RANGE = Arg(
     ),
     default=None,
 )
+ARG_SKIP_SERVE_LOGS = Arg(
+    ("-s", "--skip-serve-logs"),
+    default=False,
+    help="Don't start the serve logs process along with the workers",
+    action="store_true",
+)
 
 # list_dag_runs
 ARG_DAG_ID_REQ_FLAG = Arg(
@@ -878,76 +884,6 @@ ARG_FULL = Arg(
     action="store_true",
 )
 
-# users
-ARG_USERNAME = Arg(("-u", "--username"), help="Username of the user", 
required=True, type=str)
-ARG_USERNAME_OPTIONAL = Arg(("-u", "--username"), help="Username of the user", 
type=str)
-ARG_FIRSTNAME = Arg(("-f", "--firstname"), help="First name of the user", 
required=True, type=str)
-ARG_LASTNAME = Arg(("-l", "--lastname"), help="Last name of the user", 
required=True, type=str)
-ARG_ROLE = Arg(
-    ("-r", "--role"),
-    help="Role of the user. Existing roles include Admin, User, Op, Viewer, 
and Public",
-    required=True,
-    type=str,
-)
-ARG_EMAIL = Arg(("-e", "--email"), help="Email of the user", required=True, 
type=str)
-ARG_EMAIL_OPTIONAL = Arg(("-e", "--email"), help="Email of the user", type=str)
-ARG_PASSWORD = Arg(
-    ("-p", "--password"),
-    help="Password of the user, required to create a user without 
--use-random-password",
-    type=str,
-)
-ARG_USE_RANDOM_PASSWORD = Arg(
-    ("--use-random-password",),
-    help="Do not prompt for password. Use random string instead."
-    " Required to create a user without --password ",
-    default=False,
-    action="store_true",
-)
-ARG_USER_IMPORT = Arg(
-    ("import",),
-    metavar="FILEPATH",
-    help="Import users from JSON file. Example format::\n"
-    + textwrap.indent(
-        textwrap.dedent(
-            """
-            [
-                {
-                    "email": "[email protected]",
-                    "firstname": "Jon",
-                    "lastname": "Doe",
-                    "roles": ["Public"],
-                    "username": "jondoe"
-                }
-            ]"""
-        ),
-        " " * 4,
-    ),
-)
-ARG_USER_EXPORT = Arg(("export",), metavar="FILEPATH", help="Export all users 
to JSON file")
-
-# roles
-ARG_CREATE_ROLE = Arg(("-c", "--create"), help="Create a new role", 
action="store_true")
-ARG_LIST_ROLES = Arg(("-l", "--list"), help="List roles", action="store_true")
-ARG_ROLES = Arg(("role",), help="The name of a role", nargs="*")
-ARG_PERMISSIONS = Arg(("-p", "--permission"), help="Show role permissions", 
action="store_true")
-ARG_ROLE_RESOURCE = Arg(("-r", "--resource"), help="The name of permissions", 
nargs="*", required=True)
-ARG_ROLE_ACTION = Arg(("-a", "--action"), help="The action of permissions", 
nargs="*")
-ARG_ROLE_ACTION_REQUIRED = Arg(("-a", "--action"), help="The action of 
permissions", nargs="*", required=True)
-ARG_AUTOSCALE = Arg(("-a", "--autoscale"), help="Minimum and Maximum number of 
worker to autoscale")
-ARG_SKIP_SERVE_LOGS = Arg(
-    ("-s", "--skip-serve-logs"),
-    default=False,
-    help="Don't start the serve logs process along with the workers",
-    action="store_true",
-)
-ARG_ROLE_IMPORT = Arg(("file",), help="Import roles from JSON file", 
nargs=None)
-ARG_ROLE_EXPORT = Arg(("file",), help="Export all roles to JSON file", 
nargs=None)
-ARG_ROLE_EXPORT_FMT = Arg(
-    ("-p", "--pretty"),
-    help="Format output JSON file by sorting role names and indenting by 4 
spaces",
-    action="store_true",
-)
-
 # info
 ARG_ANONYMIZE = Arg(
     ("--anonymize",),
@@ -1024,11 +960,6 @@ ARG_ALLOW_MULTIPLE = Arg(
     help="If passed, this command will be successful even if multiple matching 
alive jobs are found.",
 )
 
-# sync-perm
-ARG_INCLUDE_DAGS = Arg(
-    ("--include-dags",), help="If passed, DAG specific permissions will also 
be synced.", action="store_true"
-)
-
 # triggerer
 ARG_CAPACITY = Arg(
     ("--capacity",),
@@ -1839,115 +1770,6 @@ PROVIDERS_COMMANDS = (
 )
 
 
-USERS_COMMANDS = (
-    ActionCommand(
-        name="list",
-        help="List users",
-        func=lazy_load_command("airflow.cli.commands.user_command.users_list"),
-        args=(ARG_OUTPUT, ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="create",
-        help="Create a user",
-        
func=lazy_load_command("airflow.cli.commands.user_command.users_create"),
-        args=(
-            ARG_ROLE,
-            ARG_USERNAME,
-            ARG_EMAIL,
-            ARG_FIRSTNAME,
-            ARG_LASTNAME,
-            ARG_PASSWORD,
-            ARG_USE_RANDOM_PASSWORD,
-            ARG_VERBOSE,
-        ),
-        epilog=(
-            "examples:\n"
-            'To create an user with "Admin" role and username equals to 
"admin", run:\n'
-            "\n"
-            "    $ airflow users create \\\n"
-            "          --username admin \\\n"
-            "          --firstname FIRST_NAME \\\n"
-            "          --lastname LAST_NAME \\\n"
-            "          --role Admin \\\n"
-            "          --email [email protected]"
-        ),
-    ),
-    ActionCommand(
-        name="delete",
-        help="Delete a user",
-        
func=lazy_load_command("airflow.cli.commands.user_command.users_delete"),
-        args=(ARG_USERNAME_OPTIONAL, ARG_EMAIL_OPTIONAL, ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="add-role",
-        help="Add role to a user",
-        func=lazy_load_command("airflow.cli.commands.user_command.add_role"),
-        args=(ARG_USERNAME_OPTIONAL, ARG_EMAIL_OPTIONAL, ARG_ROLE, 
ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="remove-role",
-        help="Remove role from a user",
-        
func=lazy_load_command("airflow.cli.commands.user_command.remove_role"),
-        args=(ARG_USERNAME_OPTIONAL, ARG_EMAIL_OPTIONAL, ARG_ROLE, 
ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="import",
-        help="Import users",
-        
func=lazy_load_command("airflow.cli.commands.user_command.users_import"),
-        args=(ARG_USER_IMPORT, ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="export",
-        help="Export all users",
-        
func=lazy_load_command("airflow.cli.commands.user_command.users_export"),
-        args=(ARG_USER_EXPORT, ARG_VERBOSE),
-    ),
-)
-ROLES_COMMANDS = (
-    ActionCommand(
-        name="list",
-        help="List roles",
-        func=lazy_load_command("airflow.cli.commands.role_command.roles_list"),
-        args=(ARG_PERMISSIONS, ARG_OUTPUT, ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="create",
-        help="Create role",
-        
func=lazy_load_command("airflow.cli.commands.role_command.roles_create"),
-        args=(ARG_ROLES, ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="delete",
-        help="Delete role",
-        
func=lazy_load_command("airflow.cli.commands.role_command.roles_delete"),
-        args=(ARG_ROLES, ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="add-perms",
-        help="Add roles permissions",
-        
func=lazy_load_command("airflow.cli.commands.role_command.roles_add_perms"),
-        args=(ARG_ROLES, ARG_ROLE_RESOURCE, ARG_ROLE_ACTION_REQUIRED, 
ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="del-perms",
-        help="Delete roles permissions",
-        
func=lazy_load_command("airflow.cli.commands.role_command.roles_del_perms"),
-        args=(ARG_ROLES, ARG_ROLE_RESOURCE, ARG_ROLE_ACTION, ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="export",
-        help="Export roles (without permissions) from db to JSON file",
-        
func=lazy_load_command("airflow.cli.commands.role_command.roles_export"),
-        args=(ARG_ROLE_EXPORT, ARG_ROLE_EXPORT_FMT, ARG_VERBOSE),
-    ),
-    ActionCommand(
-        name="import",
-        help="Import roles (without permissions) from JSON file to db",
-        
func=lazy_load_command("airflow.cli.commands.role_command.roles_import"),
-        args=(ARG_ROLE_IMPORT, ARG_VERBOSE),
-    ),
-)
-
 CONFIG_COMMANDS = (
     ActionCommand(
         name="get-value",
@@ -2171,22 +1993,6 @@ core_commands: list[CLICommand] = [
         help="Display providers",
         subcommands=PROVIDERS_COMMANDS,
     ),
-    GroupCommand(
-        name="users",
-        help="Manage users",
-        subcommands=USERS_COMMANDS,
-    ),
-    GroupCommand(
-        name="roles",
-        help="Manage roles",
-        subcommands=ROLES_COMMANDS,
-    ),
-    ActionCommand(
-        name="sync-perm",
-        help="Update permissions for existing roles and optionally DAGs",
-        
func=lazy_load_command("airflow.cli.commands.sync_perm_command.sync_perm"),
-        args=(ARG_INCLUDE_DAGS, ARG_VERBOSE),
-    ),
     ActionCommand(
         name="rotate-fernet-key",
         
func=lazy_load_command("airflow.cli.commands.rotate_fernet_key_command.rotate_fernet_key"),
diff --git a/airflow/cli/cli_parser.py b/airflow/cli/cli_parser.py
index ee1e60f1b2..07c7695f2d 100644
--- a/airflow/cli/cli_parser.py
+++ b/airflow/cli/cli_parser.py
@@ -46,6 +46,7 @@ from airflow.cli.utils import CliConflictError
 from airflow.exceptions import AirflowException
 from airflow.executors.executor_loader import ExecutorLoader
 from airflow.utils.helpers import partition
+from airflow.www.extensions.init_auth_manager import get_auth_manager_cls
 
 airflow_commands = core_commands.copy()  # make a copy to prevent bad 
interactions in tests
 
@@ -64,6 +65,13 @@ except Exception:
     # Do not re-raise the exception since we want the CLI to still function for
     # other commands.
 
+try:
+    auth_mgr = get_auth_manager_cls()
+    airflow_commands.extend(auth_mgr.get_cli_commands())
+except Exception:
+    log.exception("cannot load CLI commands from auth manager")
+    # do not re-raise for the same reason as above
+
 
 ALL_COMMANDS_DICT: dict[str, CLICommand] = {sp.name: sp for sp in 
airflow_commands}
 
diff --git a/airflow/cli/commands/standalone_command.py 
b/airflow/cli/commands/standalone_command.py
index ceae1c6dca..0beacb71d1 100644
--- a/airflow/cli/commands/standalone_command.py
+++ b/airflow/cli/commands/standalone_command.py
@@ -182,7 +182,7 @@ class StandaloneCommand:
         # server. Thus, we make a random password and store it in AIRFLOW_HOME,
         # with the reasoning that if you can read that directory, you can see
         # the database credentials anyway.
-        from airflow.utils.cli_app_builder import get_application_builder
+        from airflow.auth.managers.fab.cli_commands.utils import 
get_application_builder
 
         with get_application_builder() as appbuilder:
             user_exists = appbuilder.sm.find_user("admin")
diff --git a/airflow/providers/celery/executors/celery_executor.py 
b/airflow/providers/celery/executors/celery_executor.py
index 8bdff2a25e..cc1b6e8122 100644
--- a/airflow/providers/celery/executors/celery_executor.py
+++ b/airflow/providers/celery/executors/celery_executor.py
@@ -37,7 +37,6 @@ from celery import states as celery_states
 
 try:
     from airflow.cli.cli_config import (
-        ARG_AUTOSCALE,
         ARG_DAEMON,
         ARG_LOG_FILE,
         ARG_PID,
@@ -143,6 +142,7 @@ ARG_FLOWER_BASIC_AUTH = Arg(
 )
 
 # worker cli args
+ARG_AUTOSCALE = Arg(("-a", "--autoscale"), help="Minimum and Maximum number of 
worker to autoscale")
 ARG_QUEUES = Arg(
     ("-q", "--queues"),
     help="Comma delimited list of queues to serve",
diff --git a/airflow/www/extensions/init_auth_manager.py 
b/airflow/www/extensions/init_auth_manager.py
index a53fdf304b..24ae020862 100644
--- a/airflow/www/extensions/init_auth_manager.py
+++ b/airflow/www/extensions/init_auth_manager.py
@@ -26,12 +26,10 @@ if TYPE_CHECKING:
     from airflow.auth.managers.base_auth_manager import BaseAuthManager
 
 
-@cache
-def get_auth_manager() -> BaseAuthManager:
-    """
-    Initialize auth manager.
+def get_auth_manager_cls() -> type[BaseAuthManager]:
+    """Returns just the auth manager class without initializing it.
 
-    Import the user manager class, instantiate it and return it.
+    Useful to save execution time if only static methods need to be called.
     """
     auth_manager_cls = conf.getimport(section="core", key="auth_manager")
 
@@ -41,4 +39,16 @@ def get_auth_manager() -> BaseAuthManager:
             "Please specify one using section/key [core/auth_manager]."
         )
 
+    return auth_manager_cls
+
+
+@cache
+def get_auth_manager() -> BaseAuthManager:
+    """
+    Initialize auth manager.
+
+    Import the user manager class, instantiate it and return it.
+    """
+    auth_manager_cls = get_auth_manager_cls()
+
     return auth_manager_cls()
diff --git a/airflow/www/extensions/init_auth_manager.py 
b/tests/auth/managers/fab/cli_commands/__init__.py
similarity index 50%
copy from airflow/www/extensions/init_auth_manager.py
copy to tests/auth/managers/fab/cli_commands/__init__.py
index a53fdf304b..13a83393a9 100644
--- a/airflow/www/extensions/init_auth_manager.py
+++ b/tests/auth/managers/fab/cli_commands/__init__.py
@@ -14,31 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-
-from airflow.compat.functools import cache
-from airflow.configuration import conf
-from airflow.exceptions import AirflowConfigException
-
-if TYPE_CHECKING:
-    from airflow.auth.managers.base_auth_manager import BaseAuthManager
-
-
-@cache
-def get_auth_manager() -> BaseAuthManager:
-    """
-    Initialize auth manager.
-
-    Import the user manager class, instantiate it and return it.
-    """
-    auth_manager_cls = conf.getimport(section="core", key="auth_manager")
-
-    if not auth_manager_cls:
-        raise AirflowConfigException(
-            "No auth manager defined in the config. "
-            "Please specify one using section/key [core/auth_manager]."
-        )
-
-    return auth_manager_cls()
diff --git a/tests/cli/commands/test_role_command.py 
b/tests/auth/managers/fab/cli_commands/test_role_command.py
similarity index 96%
rename from tests/cli/commands/test_role_command.py
rename to tests/auth/managers/fab/cli_commands/test_role_command.py
index 544d8e9560..5259ca8482 100644
--- a/tests/cli/commands/test_role_command.py
+++ b/tests/auth/managers/fab/cli_commands/test_role_command.py
@@ -23,10 +23,11 @@ from contextlib import redirect_stdout
 
 import pytest
 
+from airflow.auth.managers.fab.cli_commands import role_command
+from airflow.auth.managers.fab.cli_commands.utils import 
get_application_builder
 from airflow.auth.managers.fab.models import Role
-from airflow.cli.commands import role_command
+from airflow.cli import cli_parser
 from airflow.security import permissions
-from airflow.utils.cli_app_builder import get_application_builder
 
 TEST_USER1_EMAIL = "[email protected]"
 TEST_USER2_EMAIL = "[email protected]"
@@ -34,9 +35,8 @@ TEST_USER2_EMAIL = "[email protected]"
 
 class TestCliRoles:
     @pytest.fixture(autouse=True)
-    def _set_attrs(self, dagbag, parser):
-        self.dagbag = dagbag
-        self.parser = parser
+    def _set_attrs(self):
+        self.parser = cli_parser.get_parser()
         with get_application_builder() as appbuilder:
             self.appbuilder = appbuilder
             self.clear_roles_and_roles()
diff --git a/tests/cli/commands/test_sync_perm_command.py 
b/tests/auth/managers/fab/cli_commands/test_sync_perm_command.py
similarity index 89%
rename from tests/cli/commands/test_sync_perm_command.py
rename to tests/auth/managers/fab/cli_commands/test_sync_perm_command.py
index d55d4e3724..d59fb34a8c 100644
--- a/tests/cli/commands/test_sync_perm_command.py
+++ b/tests/auth/managers/fab/cli_commands/test_sync_perm_command.py
@@ -19,8 +19,8 @@ from __future__ import annotations
 
 from unittest import mock
 
+from airflow.auth.managers.fab.cli_commands import sync_perm_command
 from airflow.cli import cli_parser
-from airflow.cli.commands import sync_perm_command
 
 
 class TestCliSyncPerm:
@@ -28,7 +28,7 @@ class TestCliSyncPerm:
     def setup_class(cls):
         cls.parser = cli_parser.get_parser()
 
-    @mock.patch("airflow.utils.cli_app_builder.get_application_builder")
+    
@mock.patch("airflow.auth.managers.fab.cli_commands.utils.get_application_builder")
     def test_cli_sync_perm(self, mock_get_application_builder):
         mock_appbuilder = mock.MagicMock()
         mock_get_application_builder.return_value.__enter__.return_value = 
mock_appbuilder
@@ -40,7 +40,7 @@ class TestCliSyncPerm:
         mock_appbuilder.sm.sync_roles.assert_called_once_with()
         mock_appbuilder.sm.create_dag_specific_permissions.assert_not_called()
 
-    @mock.patch("airflow.utils.cli_app_builder.get_application_builder")
+    
@mock.patch("airflow.auth.managers.fab.cli_commands.utils.get_application_builder")
     def test_cli_sync_perm_include_dags(self, mock_get_application_builder):
         mock_appbuilder = mock.MagicMock()
         mock_get_application_builder.return_value.__enter__.return_value = 
mock_appbuilder
diff --git a/tests/cli/commands/test_user_command.py 
b/tests/auth/managers/fab/cli_commands/test_user_command.py
similarity index 98%
rename from tests/cli/commands/test_user_command.py
rename to tests/auth/managers/fab/cli_commands/test_user_command.py
index 32f2e5db88..4daf0e6e36 100644
--- a/tests/cli/commands/test_user_command.py
+++ b/tests/auth/managers/fab/cli_commands/test_user_command.py
@@ -25,7 +25,8 @@ from contextlib import redirect_stdout
 
 import pytest
 
-from airflow.cli.commands import user_command
+from airflow.auth.managers.fab.cli_commands import user_command
+from airflow.cli import cli_parser
 from tests.test_utils.api_connexion_utils import delete_users
 
 TEST_USER1_EMAIL = "[email protected]"
@@ -44,10 +45,9 @@ def _does_user_belong_to_role(appbuilder, email, rolename):
 
 class TestCliUsers:
     @pytest.fixture(autouse=True)
-    def _set_attrs(self, app, dagbag, parser):
+    def _set_attrs(self, app):
         self.app = app
-        self.dagbag = dagbag
-        self.parser = parser
+        self.parser = cli_parser.get_parser()
         self.appbuilder = self.app.appbuilder
         delete_users(app)
         yield
diff --git a/tests/auth/managers/fab/test_fab_auth_manager.py 
b/tests/auth/managers/fab/test_fab_auth_manager.py
index cb4ef8ebc2..66c279510e 100644
--- a/tests/auth/managers/fab/test_fab_auth_manager.py
+++ b/tests/auth/managers/fab/test_fab_auth_manager.py
@@ -85,14 +85,14 @@ class TestFabAuthManager:
         with pytest.raises(AirflowException, match="`auth_view` not defined in 
the security manager."):
             auth_manager.get_url_login()
 
-    @mock.patch("airflow.auth.managers.fab.fab_auth_manager.url_for")
+    @mock.patch.object(FabAuthManager, "url_for")
     def test_get_url_login(self, mock_url_for, auth_manager):
         auth_manager.security_manager.auth_view = Mock()
         auth_manager.security_manager.auth_view.endpoint = "test_endpoint"
         auth_manager.get_url_login()
         mock_url_for.assert_called_once_with("test_endpoint.login")
 
-    @mock.patch("airflow.auth.managers.fab.fab_auth_manager.url_for")
+    @mock.patch.object(FabAuthManager, "url_for")
     def test_get_url_login_with_next(self, mock_url_for, auth_manager):
         auth_manager.security_manager.auth_view = Mock()
         auth_manager.security_manager.auth_view.endpoint = "test_endpoint"
@@ -103,7 +103,7 @@ class TestFabAuthManager:
         with pytest.raises(AirflowException, match="`auth_view` not defined in 
the security manager."):
             auth_manager.get_url_logout()
 
-    @mock.patch("airflow.auth.managers.fab.fab_auth_manager.url_for")
+    @mock.patch.object(FabAuthManager, "url_for")
     def test_get_url_logout(self, mock_url_for, auth_manager):
         auth_manager.security_manager.auth_view = Mock()
         auth_manager.security_manager.auth_view.endpoint = "test_endpoint"
@@ -113,7 +113,7 @@ class TestFabAuthManager:
     def test_get_url_user_profile_when_auth_view_not_defined(self, 
auth_manager):
         assert auth_manager.get_url_user_profile() is None
 
-    @mock.patch("airflow.auth.managers.fab.fab_auth_manager.url_for")
+    @mock.patch.object(FabAuthManager, "url_for")
     def test_get_url_user_profile(self, mock_url_for, auth_manager):
         expected_url = "test_url"
         mock_url_for.return_value = expected_url


Reply via email to