This is an automated email from the ASF dual-hosted git repository. bbovenzi pushed a commit to branch remove-legacy-ui in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 25a79808aaac4f2d08afaab0402a3ef940485c53 Author: Jed Cunningham <[email protected]> AuthorDate: Wed Feb 19 19:19:52 2025 -0700 Remove airflow.www.security_manager --- airflow/www/security_manager.py | 314 --------------------- .../aws_security_manager_override.py | 14 +- tests/www/test_security_manager.py | 168 ----------- 3 files changed, 9 insertions(+), 487 deletions(-) diff --git a/airflow/www/security_manager.py b/airflow/www/security_manager.py deleted file mode 100644 index d02d7efcf77..00000000000 --- a/airflow/www/security_manager.py +++ /dev/null @@ -1,314 +0,0 @@ -# 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 - -from functools import cached_property -from typing import TYPE_CHECKING, Callable - -from flask import g -from flask_limiter import Limiter -from flask_limiter.util import get_remote_address -from sqlalchemy import select - -from airflow.api_fastapi.app import get_auth_manager -from airflow.auth.managers.models.resource_details import ( - AccessView, - ConnectionDetails, - DagAccessEntity, - DagDetails, - PoolDetails, - VariableDetails, -) -from airflow.auth.managers.utils.fab import ( - get_method_from_fab_action_map, -) -from airflow.exceptions import AirflowException -from airflow.models import Connection, DagRun, Pool, TaskInstance, Variable -from airflow.security.permissions import ( - RESOURCE_ADMIN_MENU, - RESOURCE_ASSET, - RESOURCE_AUDIT_LOG, - RESOURCE_BROWSE_MENU, - RESOURCE_CLUSTER_ACTIVITY, - RESOURCE_CONFIG, - RESOURCE_CONNECTION, - RESOURCE_DAG, - RESOURCE_DAG_CODE, - RESOURCE_DAG_DEPENDENCIES, - RESOURCE_DAG_RUN, - RESOURCE_DOCS, - RESOURCE_DOCS_MENU, - RESOURCE_JOB, - RESOURCE_PLUGIN, - RESOURCE_POOL, - RESOURCE_PROVIDER, - RESOURCE_SLA_MISS, - RESOURCE_TASK_INSTANCE, - RESOURCE_TASK_RESCHEDULE, - RESOURCE_TRIGGER, - RESOURCE_VARIABLE, - RESOURCE_XCOM, -) -from airflow.utils.log.logging_mixin import LoggingMixin -from airflow.www.utils import CustomSQLAInterface - -EXISTING_ROLES = { - "Admin", - "Viewer", - "User", - "Op", - "Public", -} - -if TYPE_CHECKING: - from airflow.auth.managers.models.base_user import BaseUser - - -class AirflowSecurityManagerV2(LoggingMixin): - """ - Custom security manager, which introduces a permission model adapted to Airflow. - - It's named V2 to differentiate it from the obsolete airflow.www.security.AirflowSecurityManager. - """ - - def __init__(self, appbuilder) -> None: - super().__init__() - self.appbuilder = appbuilder - - # Setup Flask-Limiter - self.limiter = self.create_limiter() - - # Go and fix up the SQLAInterface used from the stock one to our subclass. - # This is needed to support the "hack" where we had to edit - # FieldConverter.conversion_table in place in airflow.www.utils - for attr in dir(self): - if attr.endswith("view"): - view = getattr(self, attr, None) - if view and getattr(view, "datamodel", None): - view.datamodel = CustomSQLAInterface(view.datamodel.obj) - - @staticmethod - def before_request(): - """Run hook before request.""" - g.user = get_auth_manager().get_user() - - def create_limiter(self) -> Limiter: - app = self.appbuilder.get_app - limiter = Limiter(key_func=app.config.get("RATELIMIT_KEY_FUNC", get_remote_address)) - limiter.init_app(app) - return limiter - - def register_views(self): - """Allow auth managers to register their own views. By default, do nothing.""" - pass - - def has_access( - self, action_name: str, resource_name: str, user=None, resource_pk: str | None = None - ) -> bool: - """ - Verify whether a given user could perform a certain action on the given resource. - - Example actions might include can_read, can_write, can_delete, etc. - - This function is called by FAB when accessing a view. See - https://github.com/dpgaspar/Flask-AppBuilder/blob/c6fecdc551629e15467fde5d06b4437379d90592/flask_appbuilder/security/decorators.py#L134 - - :param action_name: action_name on resource (e.g can_read, can_edit). - :param resource_name: name of view-menu or resource. - :param user: user - :param resource_pk: the resource primary key (e.g. the connection ID) - :return: Whether user could perform certain action on the resource. - """ - if not user: - user = g.user - - is_authorized_method = self._get_auth_manager_is_authorized_method(resource_name) - return is_authorized_method(action_name, resource_pk, user) - - def create_admin_standalone(self) -> tuple[str | None, str | None]: - """ - Perform the required steps when initializing airflow for standalone mode. - - If necessary, returns the username and password to be printed in the console for users to log in. - """ - return None, None - - def add_limit_view(self, baseview): - if not baseview.limits: - return - - for limit in baseview.limits: - self.limiter.limit( - limit_value=limit.limit_value, - key_func=limit.key_func, - per_method=limit.per_method, - methods=limit.methods, - error_message=limit.error_message, - exempt_when=limit.exempt_when, - override_defaults=limit.override_defaults, - deduct_when=limit.deduct_when, - on_breach=limit.on_breach, - cost=limit.cost, - )(baseview.blueprint) - - @cached_property - def _auth_manager_is_authorized_map( - self, - ) -> dict[str, Callable[[str, str | None, BaseUser | None], bool]]: - """ - Return the map associating a FAB resource name to the corresponding auth manager is_authorized_ API. - - The function returned takes the FAB action name and the user as parameter. - """ - auth_manager = get_auth_manager() - methods = get_method_from_fab_action_map() - - session = self.appbuilder.session - - def get_connection_id(resource_pk): - if not resource_pk: - return None - conn_id = session.scalar(select(Connection.conn_id).where(Connection.id == resource_pk).limit(1)) - if not conn_id: - raise AirflowException("Connection not found") - return conn_id - - def get_dag_id_from_dagrun_id(resource_pk): - if not resource_pk: - return None - dag_id = session.scalar(select(DagRun.dag_id).where(DagRun.id == resource_pk).limit(1)) - if not dag_id: - raise AirflowException("DagRun not found") - return dag_id - - def get_dag_id_from_task_instance(resource_pk): - if not resource_pk: - return None - dag_id = session.scalar( - select(TaskInstance.dag_id).where(TaskInstance.id == resource_pk).limit(1) - ) - if not dag_id: - raise AirflowException("Task instance not found") - return dag_id - - def get_pool_name(resource_pk): - if not resource_pk: - return None - pool = session.scalar(select(Pool).where(Pool.id == resource_pk).limit(1)) - if not pool: - raise AirflowException("Pool not found") - return pool.pool - - def get_variable_key(resource_pk): - if not resource_pk: - return None - variable = session.scalar(select(Variable).where(Variable.id == resource_pk).limit(1)) - if not variable: - raise AirflowException("Variable not found") - return variable.key - - def _is_authorized_view(view_): - return lambda action, resource_pk, user: auth_manager.is_authorized_view( - access_view=view_, - user=user, - ) - - def _is_authorized_dag(entity_=None, details_func_=None): - return lambda action, resource_pk, user: auth_manager.is_authorized_dag( - method=methods[action], - access_entity=entity_, - details=DagDetails(id=details_func_(resource_pk)) if details_func_ else None, - user=user, - ) - - mapping = { - RESOURCE_CONFIG: lambda action, resource_pk, user: auth_manager.is_authorized_configuration( - method=methods[action], - user=user, - ), - RESOURCE_CONNECTION: lambda action, resource_pk, user: auth_manager.is_authorized_connection( - method=methods[action], - details=ConnectionDetails(conn_id=get_connection_id(resource_pk)), - user=user, - ), - RESOURCE_ASSET: lambda action, resource_pk, user: auth_manager.is_authorized_asset( - method=methods[action], - user=user, - ), - RESOURCE_POOL: lambda action, resource_pk, user: auth_manager.is_authorized_pool( - method=methods[action], - details=PoolDetails(name=get_pool_name(resource_pk)), - user=user, - ), - RESOURCE_VARIABLE: lambda action, resource_pk, user: auth_manager.is_authorized_variable( - method=methods[action], - details=VariableDetails(key=get_variable_key(resource_pk)), - user=user, - ), - } - for resource, entity, details_func in [ - (RESOURCE_DAG, None, None), - (RESOURCE_AUDIT_LOG, DagAccessEntity.AUDIT_LOG, None), - (RESOURCE_DAG_CODE, DagAccessEntity.CODE, None), - (RESOURCE_DAG_DEPENDENCIES, DagAccessEntity.DEPENDENCIES, None), - (RESOURCE_SLA_MISS, DagAccessEntity.SLA_MISS, None), - (RESOURCE_TASK_RESCHEDULE, DagAccessEntity.TASK_RESCHEDULE, None), - (RESOURCE_XCOM, DagAccessEntity.XCOM, None), - (RESOURCE_DAG_RUN, DagAccessEntity.RUN, get_dag_id_from_dagrun_id), - (RESOURCE_TASK_INSTANCE, DagAccessEntity.TASK_INSTANCE, get_dag_id_from_task_instance), - ]: - mapping[resource] = _is_authorized_dag(entity, details_func) - for resource, view in [ - (RESOURCE_CLUSTER_ACTIVITY, AccessView.CLUSTER_ACTIVITY), - (RESOURCE_DOCS, AccessView.DOCS), - (RESOURCE_PLUGIN, AccessView.PLUGINS), - (RESOURCE_JOB, AccessView.JOBS), - (RESOURCE_PROVIDER, AccessView.PROVIDERS), - (RESOURCE_TRIGGER, AccessView.TRIGGERS), - ]: - mapping[resource] = _is_authorized_view(view) - return mapping - - def _get_auth_manager_is_authorized_method(self, fab_resource_name: str) -> Callable: - is_authorized_method = self._auth_manager_is_authorized_map.get(fab_resource_name) - if is_authorized_method: - return is_authorized_method - elif fab_resource_name in [RESOURCE_DOCS_MENU, RESOURCE_ADMIN_MENU, RESOURCE_BROWSE_MENU]: - # Display the "Browse", "Admin" and "Docs" dropdowns in the menu if the user has access to at - # least one dropdown child - return self._is_authorized_category_menu(fab_resource_name) - else: - # The user is trying to access a page specific to the auth manager - # (e.g. the user list view in FabAuthManager) or a page defined in a plugin - return lambda action, resource_pk, user: get_auth_manager().is_authorized_custom_view( - method=get_method_from_fab_action_map().get(action, action), - resource_name=fab_resource_name, - user=user, - ) - - def _is_authorized_category_menu(self, category: str) -> Callable: - items = {item.name for item in self.appbuilder.menu.find(category).childs} - return lambda action, resource_pk, user: any( - self._get_auth_manager_is_authorized_method(fab_resource_name=item)(action, resource_pk, user) - for item in items - ) - - def add_permissions_view(self, base_action_names, resource_name): - pass - - def add_permissions_menu(self, resource_name): - pass diff --git a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/security_manager/aws_security_manager_override.py b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/security_manager/aws_security_manager_override.py index 0bbd50396e7..1e4421e7849 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/security_manager/aws_security_manager_override.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/security_manager/aws_security_manager_override.py @@ -17,13 +17,17 @@ from __future__ import annotations from airflow.exceptions import AirflowOptionalProviderFeatureException +from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS -try: +if AIRFLOW_V_3_0_PLUS: + try: + from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2 + except ImportError: + raise AirflowOptionalProviderFeatureException( + "Failed to import AirflowSecurityManagerV2 from the FAB provider. The AWS auth manager requires the FAB provider." + ) +else: from airflow.www.security_manager import AirflowSecurityManagerV2 -except ImportError: - raise AirflowOptionalProviderFeatureException( - "Failed to import AirflowSecurityManagerV2. This feature is only available in Airflow versions >= 2.8.0" - ) class AwsSecurityManagerOverride(AirflowSecurityManagerV2): diff --git a/tests/www/test_security_manager.py b/tests/www/test_security_manager.py deleted file mode 100644 index a9c13b8cd71..00000000000 --- a/tests/www/test_security_manager.py +++ /dev/null @@ -1,168 +0,0 @@ -# -# 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 json -from unittest import mock -from unittest.mock import Mock - -import pytest - -from airflow.security.permissions import ( - ACTION_CAN_READ, - RESOURCE_ADMIN_MENU, - RESOURCE_BROWSE_MENU, - RESOURCE_DOCS_MENU, - RESOURCE_VARIABLE, -) -from airflow.www import app as application - - [email protected] -def app(): - return application.create_app(testing=True) - - [email protected] -def app_builder(app): - return app.appbuilder - - [email protected] -def security_manager(app_builder): - return app_builder.sm - - [email protected]_test -class TestAirflowSecurityManagerV2: - @pytest.mark.parametrize( - "action_name, resource_name, auth_manager_methods, expected", - [ - (ACTION_CAN_READ, RESOURCE_VARIABLE, {"is_authorized_variable": True}, True), - (ACTION_CAN_READ, RESOURCE_VARIABLE, {"is_authorized_variable": False}, False), - (ACTION_CAN_READ, RESOURCE_DOCS_MENU, {"is_authorized_view": True}, True), - (ACTION_CAN_READ, RESOURCE_DOCS_MENU, {"is_authorized_view": False}, False), - ( - ACTION_CAN_READ, - RESOURCE_ADMIN_MENU, - { - "is_authorized_view": False, - "is_authorized_variable": False, - "is_authorized_connection": True, - "is_authorized_dag": False, - "is_authorized_configuration": False, - "is_authorized_pool": False, - }, - True, - ), - ( - ACTION_CAN_READ, - RESOURCE_ADMIN_MENU, - { - "is_authorized_view": False, - "is_authorized_variable": False, - "is_authorized_connection": False, - "is_authorized_dag": False, - "is_authorized_configuration": False, - "is_authorized_pool": False, - }, - False, - ), - ( - ACTION_CAN_READ, - RESOURCE_BROWSE_MENU, - { - "is_authorized_dag": False, - "is_authorized_view": False, - }, - False, - ), - ( - ACTION_CAN_READ, - RESOURCE_BROWSE_MENU, - { - "is_authorized_dag": False, - "is_authorized_view": True, - }, - True, - ), - ( - "can_not_a_default_action", - "custom_resource", - {"is_authorized_custom_view": False}, - False, - ), - ( - "can_not_a_default_action", - "custom_resource", - {"is_authorized_custom_view": True}, - True, - ), - ], - ) - @mock.patch("airflow.providers.fab.www.security_manager.get_auth_manager") - def test_has_access( - self, - mock_get_auth_manager, - security_manager, - action_name, - resource_name, - auth_manager_methods, - expected, - ): - user = Mock() - auth_manager = Mock() - for method_name, method_return in auth_manager_methods.items(): - method = Mock(return_value=method_return) - getattr(auth_manager, method_name).side_effect = method - mock_get_auth_manager.return_value = auth_manager - result = security_manager.has_access(action_name, resource_name, user=user) - assert result == expected - if len(auth_manager_methods) > 1 and not expected: - for method_name in auth_manager_methods: - getattr(auth_manager, method_name).assert_called() - - @mock.patch("airflow.utils.session.create_session") - @mock.patch("airflow.providers.fab.www.security_manager.get_auth_manager") - def test_manager_does_not_create_extra_db_sessions( - self, - _, - mock_create_session, - security_manager, - ): - """ - Test that the Security Manager doesn't create extra DB sessions and - instead uses the session already available through the appbuilder - object that is attached to it. - """ - with mock.patch.object(security_manager.appbuilder, "session") as mock_appbuilder_session: - action_name = ACTION_CAN_READ - resource_pk = "PK" - user = Mock() - for func in security_manager._auth_manager_is_authorized_map.values(): - try: - func(action_name, resource_pk, user) - except json.JSONDecodeError: - # The resource-retrieving function expects a "composite" - # PK as a JSON string. Provide a mocked one. - func(action_name, "[1, 1, 1, 1]", user) - mock_create_session.assert_not_called() - - # The Security Manager's `appbuilder.session` object should have been - # put to use by many of the functions tested above. - assert len(mock_appbuilder_session.method_calls) > 0
