diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py index 629d9862164d7..cd766652e2d96 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py @@ -66,7 +66,10 @@ # This cannot be in the TYPE_CHECKING block since some providers import it globally. # TODO: Move this inside once all providers drop Airflow 2.x support. -ResourceMethod = Literal["GET", "POST", "PUT", "DELETE", "MENU"] +# List of methods (or actions) a user can do against a resource +ResourceMethod = Literal["GET", "POST", "PUT", "DELETE"] +# Extends ``ResourceMethod`` to include "MENU". The method "MENU" is only supported with specific resources (menu items) +ExtendedResourceMethod = Literal["GET", "POST", "PUT", "DELETE", "MENU"] log = logging.getLogger(__name__) T = TypeVar("T", bound=BaseUser) diff --git a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/avp/facade.py b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/avp/facade.py index 9f0d25ad0e310..389fb3deaa426 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/avp/facade.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/avp/facade.py @@ -37,6 +37,13 @@ if TYPE_CHECKING: from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod + + try: + from airflow.api_fastapi.auth.managers.base_auth_manager import ExtendedResourceMethod + except ImportError: + from airflow.api_fastapi.auth.managers.base_auth_manager import ( + ResourceMethod as ExtendedResourceMethod, + ) from airflow.providers.amazon.aws.auth_manager.user import AwsAuthManagerUser @@ -48,7 +55,7 @@ class IsAuthorizedRequest(TypedDict, total=False): """Represent the parameters of ``is_authorized`` method in AVP facade.""" - method: ResourceMethod + method: ExtendedResourceMethod entity_type: AvpEntities entity_id: str | None context: dict | None diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py index 2367fd1f8ebe7..830641ba2985f 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py @@ -34,6 +34,12 @@ from airflow import __version__ as airflow_version from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager + +try: + from airflow.api_fastapi.auth.managers.base_auth_manager import ExtendedResourceMethod +except ImportError: + from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod as ExtendedResourceMethod + from airflow.api_fastapi.auth.managers.models.resource_details import ( AccessView, BackfillDetails, @@ -382,7 +388,7 @@ def is_authorized_variable( def is_authorized_view(self, *, access_view: AccessView, user: User) -> bool: # "Docs" are only links in the menu, there is no page associated - method: ResourceMethod = "MENU" if access_view == AccessView.DOCS else "GET" + method: ExtendedResourceMethod = "MENU" if access_view == AccessView.DOCS else "GET" return self._is_authorized( method=method, resource_type=_MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[access_view], @@ -523,7 +529,7 @@ def get_db_manager() -> str | None: def _is_authorized( self, *, - method: ResourceMethod, + method: ExtendedResourceMethod, resource_type: str, user: User, ) -> bool: @@ -594,7 +600,7 @@ def _is_authorized_dag_run( return len(authorized_dags) > 0 @staticmethod - def _get_fab_action(method: ResourceMethod) -> str: + def _get_fab_action(method: ExtendedResourceMethod) -> str: """ Convert the method to a FAB action. diff --git a/providers/fab/src/airflow/providers/fab/www/utils.py b/providers/fab/src/airflow/providers/fab/www/utils.py index 3bde1300ec8a8..ce936cc307ea5 100644 --- a/providers/fab/src/airflow/providers/fab/www/utils.py +++ b/providers/fab/src/airflow/providers/fab/www/utils.py @@ -42,11 +42,16 @@ if TYPE_CHECKING: from sqlalchemy.orm.session import Session - from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod + try: + from airflow.api_fastapi.auth.managers.base_auth_manager import ExtendedResourceMethod + except ImportError: + from airflow.api_fastapi.auth.managers.base_auth_manager import ( + ResourceMethod as ExtendedResourceMethod, + ) from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager # Convert methods to FAB action name -_MAP_METHOD_NAME_TO_FAB_ACTION_NAME: dict[ResourceMethod, str] = { +_MAP_METHOD_NAME_TO_FAB_ACTION_NAME: dict[ExtendedResourceMethod, str] = { "POST": ACTION_CAN_CREATE, "GET": ACTION_CAN_READ, "PUT": ACTION_CAN_EDIT, diff --git a/providers/fab/www-hash.txt b/providers/fab/www-hash.txt index d287c0e948fc4..9ac61b6cd0166 100644 --- a/providers/fab/www-hash.txt +++ b/providers/fab/www-hash.txt @@ -1 +1 @@ -52c1ebe16934a7ef8ad42e05a675a3c6e662476a2d47fea3c142ff1b27305e45 +bba05295e6d4ef8f0bfe766b77cef4d90d62e86f3a2162de32d6e94979b236c7 diff --git a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/commands.py b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/commands.py index 07b1ef2e27513..59bd60ef5bad4 100644 --- a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/commands.py +++ b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/cli/commands.py @@ -23,6 +23,11 @@ from keycloak import KeycloakAdmin, KeycloakError from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod + +try: + from airflow.api_fastapi.auth.managers.base_auth_manager import ExtendedResourceMethod +except ImportError: + from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod as ExtendedResourceMethod from airflow.api_fastapi.common.types import MenuItem from airflow.configuration import conf from airflow.providers.keycloak.auth_manager.constants import ( @@ -109,6 +114,7 @@ def _get_client_uuid(args): def _create_scopes(client: KeycloakAdmin, client_uuid: str): scopes = [{"name": method} for method in get_args(ResourceMethod)] + scopes.append({"name": "MENU"}) for scope in scopes: client.create_client_authz_scopes(client_id=client_uuid, payload=scope) @@ -173,7 +179,7 @@ def _create_read_only_permission(client: KeycloakAdmin, client_uuid: str): def _create_admin_permission(client: KeycloakAdmin, client_uuid: str): all_scopes = client.get_client_authz_scopes(client_uuid) - scopes = [scope["id"] for scope in all_scopes if scope["name"] in get_args(ResourceMethod)] + scopes = [scope["id"] for scope in all_scopes if scope["name"] in get_args(ExtendedResourceMethod)] payload = { "name": "Admin", "type": "scope", diff --git a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py index 46406598a0fe5..c113f0a8d272c 100644 --- a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py +++ b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py @@ -27,6 +27,12 @@ from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager + +try: + from airflow.api_fastapi.auth.managers.base_auth_manager import ExtendedResourceMethod +except ImportError: + from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod as ExtendedResourceMethod + from airflow.api_fastapi.common.types import MenuItem from airflow.cli.cli_config import CLICommand, GroupCommand from airflow.configuration import conf @@ -201,7 +207,9 @@ def filter_authorized_menu_items( self, menu_items: list[MenuItem], *, user: KeycloakAuthManagerUser ) -> list[MenuItem]: authorized_menus = self._is_batch_authorized( - permissions=[(cast("ResourceMethod", "MENU"), menu_item.value) for menu_item in menu_items], + permissions=[ + (cast("ExtendedResourceMethod", "MENU"), menu_item.value) for menu_item in menu_items + ], user=user, ) return [MenuItem(menu[1]) for menu in authorized_menus] @@ -285,9 +293,9 @@ def _is_authorized( def _is_batch_authorized( self, *, - permissions: list[tuple[ResourceMethod, str]], + permissions: list[tuple[ExtendedResourceMethod, str]], user: KeycloakAuthManagerUser, - ) -> set[tuple[ResourceMethod, str]]: + ) -> set[tuple[ExtendedResourceMethod, str]]: client_id = conf.get(CONF_SECTION_NAME, CONF_CLIENT_ID_KEY) realm = conf.get(CONF_SECTION_NAME, CONF_REALM_KEY) server_url = conf.get(CONF_SECTION_NAME, CONF_SERVER_URL_KEY) @@ -326,7 +334,7 @@ def _get_payload(client_id: str, permission: str, attributes: dict[str, str] | N return payload @staticmethod - def _get_batch_payload(client_id: str, permissions: list[tuple[ResourceMethod, str]]): + def _get_batch_payload(client_id: str, permissions: list[tuple[ExtendedResourceMethod, str]]): payload: dict[str, Any] = { "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "audience": client_id,