diff --git a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py index aa23b4f2634f3..9d3f2fabe9e52 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import warnings from collections import defaultdict from collections.abc import Sequence from functools import cached_property @@ -27,6 +28,7 @@ from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager from airflow.cli.cli_config import CLICommand +from airflow.exceptions import AirflowProviderDeprecationWarning from airflow.providers.amazon.aws.auth_manager.avp.entities import AvpEntities from airflow.providers.amazon.aws.auth_manager.avp.facade import ( AwsAuthManagerAmazonVerifiedPermissionsFacade, @@ -158,6 +160,13 @@ def is_authorized_dag( def is_authorized_backfill( self, *, method: ResourceMethod, user: AwsAuthManagerUser, details: BackfillDetails | None = None ) -> bool: + # Method can be removed once the min Airflow version is >= 3.2.0. + warnings.warn( + "Use ``is_authorized_dag`` on ``DagAccessEntity.RUN`` instead for a dag level access control.", + AirflowProviderDeprecationWarning, + stacklevel=2, + ) + backfill_id = details.id if details else None return self.avp_facade.is_authorized( method=method, entity_type=AvpEntities.BACKFILL, user=user, entity_id=backfill_id diff --git a/providers/amazon/tests/unit/amazon/aws/auth_manager/test_aws_auth_manager.py b/providers/amazon/tests/unit/amazon/aws/auth_manager/test_aws_auth_manager.py index 8ee6fa8470b89..8e0ff06c18584 100644 --- a/providers/amazon/tests/unit/amazon/aws/auth_manager/test_aws_auth_manager.py +++ b/providers/amazon/tests/unit/amazon/aws/auth_manager/test_aws_auth_manager.py @@ -16,11 +16,14 @@ # under the License. from __future__ import annotations +from contextlib import ExitStack from typing import TYPE_CHECKING from unittest.mock import ANY, Mock, patch import pytest +from airflow.exceptions import AirflowProviderDeprecationWarning + from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS if not AIRFLOW_V_3_0_PLUS: @@ -226,8 +229,16 @@ def test_is_authorized_backfill( is_authorized = Mock(return_value=True) mock_avp_facade.is_authorized = is_authorized - method: ResourceMethod = "GET" - result = auth_manager.is_authorized_backfill(method=method, details=details, user=user) + with ExitStack() as stack: + stack.enter_context( + pytest.warns( + AirflowProviderDeprecationWarning, + match="Use ``is_authorized_dag`` on ``DagAccessEntity.RUN`` instead for a dag level access control.", + ) + ) + + method: ResourceMethod = "GET" + result = auth_manager.is_authorized_backfill(method=method, details=details, user=user) is_authorized.assert_called_once_with( method=method, entity_type=AvpEntities.BACKFILL, user=expected_user, entity_id=expected_entity_id 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 aebd4fa6f149c..14f7eb1f41f18 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 @@ -17,6 +17,7 @@ # under the License. from __future__ import annotations +import warnings from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any @@ -51,7 +52,7 @@ ) from airflow.api_fastapi.common.types import ExtraMenuItem, MenuItem from airflow.configuration import conf -from airflow.exceptions import AirflowConfigException +from airflow.exceptions import AirflowConfigException, AirflowProviderDeprecationWarning from airflow.models import Connection, DagModel, Pool, Variable from airflow.providers.common.compat.sdk import AirflowException from airflow.providers.fab.auth_manager.models import Permission, Role, User @@ -389,6 +390,12 @@ def is_authorized_backfill( user: User, details: BackfillDetails | None = None, ) -> bool: + # Method can be removed once the min Airflow version is >= 3.2.0. + warnings.warn( + "Use ``is_authorized_dag`` on ``DagAccessEntity.RUN`` instead for a dag level access control.", + AirflowProviderDeprecationWarning, + stacklevel=2, + ) return self._is_authorized(method=method, resource_type=RESOURCE_BACKFILL, user=user) def is_authorized_asset( diff --git a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py index 0170d14923f99..d9c7bbc9f915e 100644 --- a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py +++ b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py @@ -17,7 +17,7 @@ from __future__ import annotations import time -from contextlib import contextmanager, suppress +from contextlib import ExitStack, contextmanager, suppress from itertools import chain from typing import TYPE_CHECKING from unittest import mock @@ -29,7 +29,7 @@ from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX from airflow.api_fastapi.common.types import MenuItem -from airflow.exceptions import AirflowConfigException +from airflow.exceptions import AirflowConfigException, AirflowProviderDeprecationWarning from airflow.providers.fab.www.app import create_app from airflow.providers.fab.www.utils import get_fab_auth_manager from airflow.providers.standard.operators.empty import EmptyOperator @@ -328,10 +328,19 @@ def test_create_token_wrong_values(self, username, password, auth_manager_with_a def test_is_authorized(self, api_name, method, user_permissions, expected_result, auth_manager): user = Mock() user.perms = user_permissions - result = getattr(auth_manager, api_name)( - method=method, - user=user, - ) + + with ExitStack() as stack: + if api_name == "is_authorized_backfill": + stack.enter_context( + pytest.warns( + AirflowProviderDeprecationWarning, + match="Use ``is_authorized_dag`` on ``DagAccessEntity.RUN`` instead for a dag level access control.", + ) + ) + result = getattr(auth_manager, api_name)( + method=method, + user=user, + ) assert result == expected_result @pytest.mark.parametrize( 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 dc8de7c25954e..746a5a428cbb0 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 @@ -19,6 +19,7 @@ import json import logging import time +import warnings from base64 import urlsafe_b64decode from typing import TYPE_CHECKING, Any from urllib.parse import urljoin @@ -32,6 +33,7 @@ from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager +from airflow.exceptions import AirflowProviderDeprecationWarning try: from airflow.api_fastapi.auth.managers.base_auth_manager import ExtendedResourceMethod @@ -220,6 +222,13 @@ def is_authorized_dag( def is_authorized_backfill( self, *, method: ResourceMethod, user: KeycloakAuthManagerUser, details: BackfillDetails | None = None ) -> bool: + # Method can be removed once the min Airflow version is >= 3.2.0. + warnings.warn( + "Use ``is_authorized_dag`` on ``DagAccessEntity.RUN`` instead for a dag level access control.", + AirflowProviderDeprecationWarning, + stacklevel=2, + ) + backfill_id = str(details.id) if details else None return self._is_authorized( method=method, resource_type=KeycloakResource.BACKFILL, user=user, resource_id=backfill_id diff --git a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py index 27747f506961b..faf954eb2eda8 100644 --- a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py +++ b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py @@ -17,6 +17,7 @@ from __future__ import annotations import json +from contextlib import ExitStack from unittest.mock import Mock, patch import pytest @@ -36,6 +37,7 @@ VariableDetails, ) from airflow.api_fastapi.common.types import MenuItem +from airflow.exceptions import AirflowProviderDeprecationWarning from airflow.providers.common.compat.sdk import AirflowException from airflow.providers.keycloak.auth_manager.constants import ( CONF_CLIENT_ID_KEY, @@ -263,7 +265,16 @@ def test_is_authorized( mock_response.status_code = status_code auth_manager.http_session.post = Mock(return_value=mock_response) - result = getattr(auth_manager, function)(method=method, user=user, details=details) + with ExitStack() as stack: + if function == "is_authorized_backfill": + stack.enter_context( + pytest.warns( + AirflowProviderDeprecationWarning, + match="Use ``is_authorized_dag`` on ``DagAccessEntity.RUN`` instead for a dag level access control.", + ) + ) + + result = getattr(auth_manager, function)(method=method, user=user, details=details) token_url = auth_manager._get_token_url("server_url", "realm") payload = auth_manager._get_payload("client_id", permission, attributes) @@ -291,10 +302,19 @@ def test_is_authorized_failure(self, function, auth_manager, user): resp.status_code = 500 auth_manager.http_session.post = Mock(return_value=resp) - with pytest.raises(AirflowException) as e: - getattr(auth_manager, function)(method="GET", user=user) + with ExitStack() as stack: + if function == "is_authorized_backfill": + stack.enter_context( + pytest.warns( + AirflowProviderDeprecationWarning, + match="Use ``is_authorized_dag`` on ``DagAccessEntity.RUN`` instead for a dag level access control.", + ) + ) + + with pytest.raises(AirflowException) as e: + getattr(auth_manager, function)(method="GET", user=user) - assert "Unexpected error" in str(e.value) + assert "Unexpected error" in str(e.value) @pytest.mark.parametrize( "function", @@ -315,10 +335,19 @@ def test_is_authorized_invalid_request(self, function, auth_manager, user): resp.text = json.dumps({"error": "invalid_scope", "error_description": "Invalid scopes: GET"}) auth_manager.http_session.post = Mock(return_value=resp) - with pytest.raises(AirflowException) as e: - getattr(auth_manager, function)(method="GET", user=user) + with ExitStack() as stack: + if function == "is_authorized_backfill": + stack.enter_context( + pytest.warns( + AirflowProviderDeprecationWarning, + match="Use ``is_authorized_dag`` on ``DagAccessEntity.RUN`` instead for a dag level access control.", + ) + ) + + with pytest.raises(AirflowException) as e: + getattr(auth_manager, function)(method="GET", user=user) - assert "Request not recognized by Keycloak. invalid_scope. Invalid scopes: GET" in str(e.value) + assert "Request not recognized by Keycloak. invalid_scope. Invalid scopes: GET" in str(e.value) @pytest.mark.parametrize( ("method", "access_entity", "details", "permission", "attributes"),