diff --git a/airflow/api_connexion/endpoints/asset_endpoint.py b/airflow/api_connexion/endpoints/asset_endpoint.py index 64930b1249468..30bb2d513aeda 100644 --- a/airflow/api_connexion/endpoints/asset_endpoint.py +++ b/airflow/api_connexion/endpoints/asset_endpoint.py @@ -43,6 +43,7 @@ queued_event_collection_schema, queued_event_schema, ) +from airflow.api_fastapi.app import get_auth_manager from airflow.assets.manager import asset_manager from airflow.models.asset import AssetDagRunQueue, AssetEvent, AssetModel from airflow.utils import timezone @@ -50,7 +51,6 @@ from airflow.utils.db import get_query_count from airflow.utils.session import NEW_SESSION, provide_session from airflow.www.decorators import action_logging -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/endpoints/dag_endpoint.py b/airflow/api_connexion/endpoints/dag_endpoint.py index bb5d419691592..8c82b86467855 100644 --- a/airflow/api_connexion/endpoints/dag_endpoint.py +++ b/airflow/api_connexion/endpoints/dag_endpoint.py @@ -37,6 +37,7 @@ dag_schema, dags_collection_schema, ) +from airflow.api_fastapi.app import get_auth_manager from airflow.exceptions import AirflowException, DagNotFound from airflow.models.dag import DagModel, DagTag from airflow.utils.airflow_flask_app import get_airflow_app @@ -44,7 +45,6 @@ from airflow.utils.db import get_query_count from airflow.utils.session import NEW_SESSION, provide_session from airflow.www.decorators import action_logging -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/endpoints/dag_parsing.py b/airflow/api_connexion/endpoints/dag_parsing.py index 1580529bcc445..c6fde8d851b61 100644 --- a/airflow/api_connexion/endpoints/dag_parsing.py +++ b/airflow/api_connexion/endpoints/dag_parsing.py @@ -26,12 +26,12 @@ from airflow.api_connexion import security from airflow.api_connexion.exceptions import NotFound, PermissionDenied +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import DagDetails from airflow.models.dag import DagModel from airflow.models.dagbag import DagPriorityParsingRequest from airflow.utils.api_migration import mark_fastapi_migration_done from airflow.utils.session import NEW_SESSION, provide_session -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/endpoints/dag_run_endpoint.py b/airflow/api_connexion/endpoints/dag_run_endpoint.py index 985efc7fc898d..4574a7b77c76c 100644 --- a/airflow/api_connexion/endpoints/dag_run_endpoint.py +++ b/airflow/api_connexion/endpoints/dag_run_endpoint.py @@ -59,6 +59,7 @@ TaskInstanceReferenceCollection, task_instance_reference_collection_schema, ) +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import DagAccessEntity from airflow.exceptions import ParamValidationError from airflow.models import DagModel, DagRun @@ -71,7 +72,6 @@ from airflow.utils.state import DagRunState from airflow.utils.types import DagRunTriggeredByType, DagRunType from airflow.www.decorators import action_logging -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/endpoints/dag_source_endpoint.py b/airflow/api_connexion/endpoints/dag_source_endpoint.py index 1c6e34fc2b1a7..c1c4929aed9e5 100644 --- a/airflow/api_connexion/endpoints/dag_source_endpoint.py +++ b/airflow/api_connexion/endpoints/dag_source_endpoint.py @@ -26,12 +26,12 @@ from airflow.api_connexion import security from airflow.api_connexion.exceptions import NotFound, PermissionDenied from airflow.api_connexion.schemas.dag_source_schema import dag_source_schema +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import DagAccessEntity, DagDetails from airflow.models.dag import DagModel from airflow.models.dag_version import DagVersion from airflow.utils.api_migration import mark_fastapi_migration_done from airflow.utils.session import NEW_SESSION, provide_session -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/endpoints/dag_stats_endpoint.py b/airflow/api_connexion/endpoints/dag_stats_endpoint.py index c4d8701f8d3c0..05489dd27a7c6 100644 --- a/airflow/api_connexion/endpoints/dag_stats_endpoint.py +++ b/airflow/api_connexion/endpoints/dag_stats_endpoint.py @@ -25,12 +25,12 @@ from airflow.api_connexion.schemas.dag_stats_schema import ( dag_stats_collection_schema, ) +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import DagAccessEntity from airflow.models.dag import DagRun from airflow.utils.api_migration import mark_fastapi_migration_done from airflow.utils.session import NEW_SESSION, provide_session from airflow.utils.state import DagRunState -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/endpoints/import_error_endpoint.py b/airflow/api_connexion/endpoints/import_error_endpoint.py index 76fad6cb92d4e..d2780fe941b13 100644 --- a/airflow/api_connexion/endpoints/import_error_endpoint.py +++ b/airflow/api_connexion/endpoints/import_error_endpoint.py @@ -29,12 +29,12 @@ import_error_collection_schema, import_error_schema, ) +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import AccessView, DagDetails from airflow.models.dag import DagModel from airflow.models.errors import ParseImportError from airflow.utils.api_migration import mark_fastapi_migration_done from airflow.utils.session import NEW_SESSION, provide_session -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/endpoints/task_instance_endpoint.py b/airflow/api_connexion/endpoints/task_instance_endpoint.py index b599f24ab311c..8751187a70ab6 100644 --- a/airflow/api_connexion/endpoints/task_instance_endpoint.py +++ b/airflow/api_connexion/endpoints/task_instance_endpoint.py @@ -47,6 +47,7 @@ task_instance_schema, ) from airflow.api_connexion.security import get_readable_dags +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import DagAccessEntity, DagDetails from airflow.exceptions import TaskNotFound from airflow.models.dagrun import DagRun as DR @@ -58,7 +59,6 @@ from airflow.utils.session import NEW_SESSION, provide_session from airflow.utils.state import DagRunState, TaskInstanceState from airflow.www.decorators import action_logging -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/endpoints/xcom_endpoint.py b/airflow/api_connexion/endpoints/xcom_endpoint.py index cb3faf7379e0a..3951e8be467d6 100644 --- a/airflow/api_connexion/endpoints/xcom_endpoint.py +++ b/airflow/api_connexion/endpoints/xcom_endpoint.py @@ -31,13 +31,13 @@ xcom_schema_native, xcom_schema_string, ) +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import DagAccessEntity from airflow.models import DagRun as DR, XCom from airflow.settings import conf from airflow.utils.api_migration import mark_fastapi_migration_done from airflow.utils.db import get_query_count from airflow.utils.session import NEW_SESSION, provide_session -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from sqlalchemy.orm import Session diff --git a/airflow/api_connexion/security.py b/airflow/api_connexion/security.py index 1098de3a1f474..3601116fdb440 100644 --- a/airflow/api_connexion/security.py +++ b/airflow/api_connexion/security.py @@ -22,6 +22,7 @@ from flask import Response, g from airflow.api_connexion.exceptions import PermissionDenied, Unauthenticated +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import ( AccessView, AssetDetails, @@ -33,7 +34,6 @@ VariableDetails, ) from airflow.utils.airflow_flask_app import get_airflow_app -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from airflow.auth.managers.base_auth_manager import ResourceMethod diff --git a/airflow/api_fastapi/app.py b/airflow/api_fastapi/app.py index b90993571b4bd..3c9aa39ae707e 100644 --- a/airflow/api_fastapi/app.py +++ b/airflow/api_fastapi/app.py @@ -68,9 +68,9 @@ def create_app(apps: str = "all") -> FastAPI: init_dag_bag(app) init_views(app) init_plugins(app) + init_auth_manager(app) init_flask_plugins(app) init_error_handlers(app) - init_auth_manager() if "execution" in apps_list or "all" in apps_list: task_exec_api_app = create_task_execution_api_app(app) @@ -112,34 +112,28 @@ def get_auth_manager_cls() -> type[BaseAuthManager]: return auth_manager_cls -def init_auth_manager() -> BaseAuthManager: - """ - Initialize the auth manager. - - Import the user manager class and instantiate it. - """ +def create_auth_manager() -> BaseAuthManager: + """Create the auth manager.""" global auth_manager auth_manager_cls = get_auth_manager_cls() auth_manager = auth_manager_cls() - auth_manager.init() return auth_manager +def init_auth_manager(app: FastAPI | None = None) -> BaseAuthManager: + """Initialize the auth manager.""" + am = create_auth_manager() + am.init() + + if app and (auth_manager_fastapi_app := am.get_fastapi_app()): + app.mount("/auth", auth_manager_fastapi_app) + + return am + + def get_auth_manager() -> BaseAuthManager: """Return the auth manager, provided it's been initialized before.""" global auth_manager - if auth_manager is None: - """ - The auth manager can be init in the main Flask application but also in the mini Flask application - in Fab provider. - This is temporary, the goal is to remove the main Flask application from core Airflow. Once that done, - we'll be able to remove this if because the auth manager will be only init in the min Flask - application defined in Fab provider. - """ - from airflow.www.extensions.init_auth_manager import get_auth_manager as get_auth_manager_flask - - if auth_manager_flask := get_auth_manager_flask(): - auth_manager = auth_manager_flask if auth_manager is None: raise RuntimeError( diff --git a/airflow/auth/managers/base_auth_manager.py b/airflow/auth/managers/base_auth_manager.py index ea0c921c98c00..4ebf13a612b0d 100644 --- a/airflow/auth/managers/base_auth_manager.py +++ b/airflow/auth/managers/base_auth_manager.py @@ -36,6 +36,7 @@ from airflow.utils.session import NEW_SESSION, provide_session if TYPE_CHECKING: + from fastapi import FastAPI from flask import Blueprint from sqlalchemy.orm import Session @@ -55,7 +56,6 @@ VariableDetails, ) from airflow.cli.cli_config import CLICommand - from airflow.www.extensions.init_appbuilder import AirflowAppBuilder from airflow.www.security_manager import AirflowSecurityManagerV2 ResourceMethod = Literal["GET", "POST", "PUT", "DELETE", "MENU"] @@ -68,14 +68,8 @@ class BaseAuthManager(Generic[T], LoggingMixin): Class to derive in order to implement concrete auth managers. Auth managers are responsible for any user management related operation such as login, logout, authz, ... - - :param appbuilder: the flask app builder """ - def __init__(self, appbuilder: AirflowAppBuilder | None = None) -> None: - super().__init__() - self.appbuilder = appbuilder - def init(self) -> None: """ Run operations when Airflow is initializing. @@ -440,7 +434,7 @@ def security_manager(self) -> AirflowSecurityManagerV2: """ from airflow.www.security_manager import AirflowSecurityManagerV2 - return AirflowSecurityManagerV2(self.appbuilder) + return AirflowSecurityManagerV2(getattr(self, "appbuilder")) @staticmethod def get_cli_commands() -> list[CLICommand]: @@ -453,6 +447,15 @@ def get_cli_commands() -> list[CLICommand]: def get_api_endpoints(self) -> None | Blueprint: """Return API endpoint(s) definition for the auth manager.""" + # TODO: Remove this method when legacy Airflow 2 UI is gone + return None + + def get_fastapi_app(self) -> FastAPI | None: + """ + Specify a sub FastAPI application specific to the auth manager. + + This sub application, if specified, is mounted in the main FastAPI application. + """ return None def register_views(self) -> None: diff --git a/airflow/auth/managers/simple/simple_auth_manager.py b/airflow/auth/managers/simple/simple_auth_manager.py index 67eb373ced7d0..3d53d1d097b20 100644 --- a/airflow/auth/managers/simple/simple_auth_manager.py +++ b/airflow/auth/managers/simple/simple_auth_manager.py @@ -43,6 +43,7 @@ PoolDetails, VariableDetails, ) + from airflow.www.extensions.init_appbuilder import AirflowAppBuilder class SimpleAuthManagerRole(namedtuple("SimpleAuthManagerRole", "name order"), Enum): @@ -78,6 +79,9 @@ class SimpleAuthManager(BaseAuthManager[SimpleAuthManagerUser]): # Cache containing the password associated to a username passwords: dict[str, str] = {} + # TODO: Needs to be deleted when Airflow 2 legacy UI is gone + appbuilder: AirflowAppBuilder | None = None + @staticmethod def get_generated_password_file() -> str: return os.path.join( diff --git a/airflow/auth/managers/simple/views/auth.py b/airflow/auth/managers/simple/views/auth.py index 6bf92cf0bc717..58d5f008cc40a 100644 --- a/airflow/auth/managers/simple/views/auth.py +++ b/airflow/auth/managers/simple/views/auth.py @@ -21,12 +21,12 @@ from flask import redirect, request, session, url_for from flask_appbuilder import expose +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.simple.user import SimpleAuthManagerUser from airflow.configuration import conf from airflow.utils.jwt_signer import JWTSigner from airflow.utils.state import State from airflow.www.app import csrf -from airflow.www.extensions.init_auth_manager import get_auth_manager from airflow.www.views import AirflowBaseView diff --git a/airflow/cli/cli_parser.py b/airflow/cli/cli_parser.py index a48e6b438d390..d2f9a73653b8a 100644 --- a/airflow/cli/cli_parser.py +++ b/airflow/cli/cli_parser.py @@ -36,6 +36,7 @@ import lazy_object_proxy from rich_argparse import RawTextRichHelpFormatter, RichHelpFormatter +from airflow.api_fastapi.app import get_auth_manager_cls from airflow.cli.cli_config import ( DAG_CLI_DICT, ActionCommand, @@ -47,7 +48,6 @@ 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 if TYPE_CHECKING: from airflow.cli.cli_config import ( diff --git a/airflow/www/auth.py b/airflow/www/auth.py index 2b864273e5fa0..101b1463596e9 100644 --- a/airflow/www/auth.py +++ b/airflow/www/auth.py @@ -30,6 +30,7 @@ PERMISSION_PREFIX, ) +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import ( AccessView, ConnectionDetails, @@ -40,7 +41,6 @@ ) from airflow.configuration import conf from airflow.utils.net import get_hostname -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from airflow.auth.managers.base_auth_manager import ResourceMethod diff --git a/airflow/www/decorators.py b/airflow/www/decorators.py index fc0ad893369f5..0651820ce78d7 100644 --- a/airflow/www/decorators.py +++ b/airflow/www/decorators.py @@ -29,10 +29,10 @@ from flask import after_this_request, request from pendulum.parsing.exceptions import ParserError +from airflow.api_fastapi.app import get_auth_manager from airflow.models import Log from airflow.utils.log import secrets_masker from airflow.utils.session import create_session -from airflow.www.extensions.init_auth_manager import get_auth_manager T = TypeVar("T", bound=Callable) diff --git a/airflow/www/extensions/init_appbuilder.py b/airflow/www/extensions/init_appbuilder.py index 9f8ef4b602866..e985f318a4514 100644 --- a/airflow/www/extensions/init_appbuilder.py +++ b/airflow/www/extensions/init_appbuilder.py @@ -39,8 +39,8 @@ from flask_appbuilder.views import IndexView, UtilView from airflow import settings +from airflow.api_fastapi.app import create_auth_manager, get_auth_manager from airflow.configuration import conf -from airflow.www.extensions.init_auth_manager import get_auth_manager, init_auth_manager if TYPE_CHECKING: from flask import Flask @@ -208,7 +208,9 @@ def init_app(self, app, session): self._addon_managers = app.config["ADDON_MANAGERS"] self.session = session - auth_manager = init_auth_manager(self) + auth_manager = create_auth_manager() + auth_manager.appbuilder = self + auth_manager.init() self.sm = auth_manager.security_manager self.bm = BabelManager(self) self._add_global_static() diff --git a/airflow/www/extensions/init_auth_manager.py b/airflow/www/extensions/init_auth_manager.py deleted file mode 100644 index 6e6f1f8af1b75..0000000000000 --- a/airflow/www/extensions/init_auth_manager.py +++ /dev/null @@ -1,73 +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 typing import TYPE_CHECKING - -from airflow.configuration import conf -from airflow.exceptions import AirflowConfigException - -if TYPE_CHECKING: - from airflow.auth.managers.base_auth_manager import BaseAuthManager - from airflow.www.extensions.init_appbuilder import AirflowAppBuilder - -auth_manager: BaseAuthManager | None = None - - -def get_auth_manager_cls() -> type[BaseAuthManager]: - """ - Return just the auth manager class without initializing it. - - Useful to save execution time if only static methods need to be called. - """ - 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 - - -def init_auth_manager(appbuilder: AirflowAppBuilder) -> BaseAuthManager: - """ - Initialize the auth manager. - - Import the user manager class and instantiate it. - """ - global auth_manager - auth_manager_cls = get_auth_manager_cls() - auth_manager = auth_manager_cls(appbuilder) - auth_manager.init() - return auth_manager - - -def get_auth_manager() -> BaseAuthManager: - """Return the auth manager, provided it's been initialized before.""" - if auth_manager is None: - raise RuntimeError( - "Auth Manager has not been initialized yet. " - "The `init_auth_manager` method needs to be called first." - ) - return auth_manager - - -def is_auth_manager_initialized() -> bool: - """Return whether the auth manager has been initialized.""" - return auth_manager is not None diff --git a/airflow/www/extensions/init_jinja_globals.py b/airflow/www/extensions/init_jinja_globals.py index a64212ef7b37f..60761a9b3b8bf 100644 --- a/airflow/www/extensions/init_jinja_globals.py +++ b/airflow/www/extensions/init_jinja_globals.py @@ -21,11 +21,11 @@ import pendulum import airflow +from airflow.api_fastapi.app import get_auth_manager from airflow.configuration import conf from airflow.settings import IS_K8S_OR_K8SCELERY_EXECUTOR, STATE_COLORS from airflow.utils.net import get_hostname from airflow.utils.platform import get_airflow_git_version -from airflow.www.extensions.init_auth_manager import get_auth_manager logger = logging.getLogger(__name__) diff --git a/airflow/www/extensions/init_views.py b/airflow/www/extensions/init_views.py index faed1761c1939..a540bdd3a6765 100644 --- a/airflow/www/extensions/init_views.py +++ b/airflow/www/extensions/init_views.py @@ -27,11 +27,11 @@ from flask import request from airflow.api_connexion.exceptions import common_error_handler +from airflow.api_fastapi.app import get_auth_manager from airflow.configuration import conf from airflow.security import permissions from airflow.utils.yaml import safe_load from airflow.www.constants import SWAGGER_BUNDLE, SWAGGER_ENABLED -from airflow.www.extensions.init_auth_manager import get_auth_manager if TYPE_CHECKING: from flask import Flask diff --git a/airflow/www/security_manager.py b/airflow/www/security_manager.py index 6d782410c3dbc..d02d7efcf774a 100644 --- a/airflow/www/security_manager.py +++ b/airflow/www/security_manager.py @@ -24,6 +24,7 @@ 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, @@ -63,7 +64,6 @@ RESOURCE_XCOM, ) from airflow.utils.log.logging_mixin import LoggingMixin -from airflow.www.extensions.init_auth_manager import get_auth_manager from airflow.www.utils import CustomSQLAInterface EXISTING_ROLES = { diff --git a/airflow/www/utils.py b/airflow/www/utils.py index 8a74a74d5a8d9..727139a9a6b39 100644 --- a/airflow/www/utils.py +++ b/airflow/www/utils.py @@ -40,6 +40,7 @@ from sqlalchemy import delete, func, select, types from sqlalchemy.ext.associationproxy import AssociationProxy +from airflow.api_fastapi.app import get_auth_manager from airflow.models.dagrun import DagRun from airflow.models.dagwarning import DagWarning from airflow.models.errors import ParseImportError @@ -50,7 +51,6 @@ from airflow.utils.json import WebEncoder from airflow.utils.sqlalchemy import tuple_in_condition from airflow.utils.state import State, TaskInstanceState -from airflow.www.extensions.init_auth_manager import get_auth_manager from airflow.www.forms import DateTimeWithTimezoneField from airflow.www.widgets import AirflowDateTimePickerWidget diff --git a/airflow/www/views.py b/airflow/www/views.py index d25db66616750..a654c9ad720b4 100644 --- a/airflow/www/views.py +++ b/airflow/www/views.py @@ -88,6 +88,7 @@ set_dag_run_state_to_success, set_state, ) +from airflow.api_fastapi.app import get_auth_manager from airflow.auth.managers.models.resource_details import AccessView, DagAccessEntity, DagDetails from airflow.configuration import AIRFLOW_CONFIG, conf from airflow.exceptions import ( @@ -138,7 +139,6 @@ from airflow.version import version from airflow.www import auth, utils as wwwutils from airflow.www.decorators import action_logging, gzipped -from airflow.www.extensions.init_auth_manager import get_auth_manager, is_auth_manager_initialized from airflow.www.forms import ( DagRunEditForm, DateTimeForm, @@ -671,9 +671,6 @@ def method_not_allowed(error): def show_traceback(error): """Show Traceback for a given error.""" - if not is_auth_manager_initialized(): - # this is the case where internal API component is used and auth manager is not used/initialized - return ("Error calling the API", 500) is_logged_in = get_auth_manager().is_logged_in() return ( render_template( diff --git a/newsfragments/aip-79.significant.rst b/newsfragments/aip-79.significant.rst new file mode 100644 index 0000000000000..18a3884054c1f --- /dev/null +++ b/newsfragments/aip-79.significant.rst @@ -0,0 +1,11 @@ +Remove Flask App Builder from core Airflow dependencies. + +As part of this change the following breaking changes have occurred: + +- The auth manager interface ``base_auth_manager`` have been updated with some breaking changes: + + - The constructor no longer take ``appbuilder`` as parameter. The constructor takes no parameter + + - A new abstract method ``deserialize_user`` needs to be implemented + + - A new abstract method ``serialize_user`` needs to be implemented diff --git a/providers/src/airflow/providers/amazon/CHANGELOG.rst b/providers/src/airflow/providers/amazon/CHANGELOG.rst index 47572b55e4cf6..3a1d9589aaa2d 100644 --- a/providers/src/airflow/providers/amazon/CHANGELOG.rst +++ b/providers/src/airflow/providers/amazon/CHANGELOG.rst @@ -30,6 +30,14 @@ Changelog This release of provider is only available for Airflow 2.9+ as explained in the `Apache Airflow providers support policy `_. +9.3.0 +..... + +Misc +~~~~ + +* ``The experimental AWS auth manager is no longer compatible with Airflow 2`` + 9.2.0 ..... diff --git a/providers/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py b/providers/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py index 378b7d49d8893..37da993deabcf 100644 --- a/providers/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py +++ b/providers/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py @@ -25,6 +25,15 @@ from flask import session, url_for +from airflow.auth.managers.base_auth_manager import BaseAuthManager, ResourceMethod +from airflow.auth.managers.models.resource_details import ( + AccessView, + ConnectionDetails, + DagAccessEntity, + DagDetails, + PoolDetails, + VariableDetails, +) from airflow.cli.cli_config import CLICommand, DefaultHelpParser, GroupCommand from airflow.exceptions import AirflowOptionalProviderFeatureException, AirflowProviderDeprecationWarning from airflow.providers.amazon.aws.auth_manager.avp.entities import AvpEntities @@ -39,21 +48,7 @@ AwsSecurityManagerOverride, ) from airflow.providers.amazon.aws.auth_manager.views.auth import AwsAuthManagerAuthenticationViews - -try: - from airflow.auth.managers.base_auth_manager import BaseAuthManager, ResourceMethod - from airflow.auth.managers.models.resource_details import ( - AccessView, - ConnectionDetails, - DagAccessEntity, - DagDetails, - PoolDetails, - VariableDetails, - ) -except ImportError: - raise AirflowOptionalProviderFeatureException( - "Failed to import BaseUser. This feature is only available in Airflow versions >= 2.8.0" - ) +from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS if TYPE_CHECKING: from flask_appbuilder.menu import MenuItem @@ -76,12 +71,17 @@ class AwsAuthManager(BaseAuthManager): Leverages AWS services such as Amazon Identity Center and Amazon Verified Permissions to perform authentication and authorization in Airflow. - - :param appbuilder: the flask app builder """ - def __init__(self, appbuilder: AirflowAppBuilder) -> None: - super().__init__(appbuilder) + appbuilder: AirflowAppBuilder | None = None + + def __init__(self) -> None: + if not AIRFLOW_V_3_0_PLUS: + raise AirflowOptionalProviderFeatureException( + "AWS auth manager is only compatible with Airflow versions >= 3.0.0" + ) + + super().__init__() self._check_avp_schema_version() @cached_property diff --git a/providers/src/airflow/providers/fab/auth_manager/fab_auth_manager.py b/providers/src/airflow/providers/fab/auth_manager/fab_auth_manager.py index cdb0055003b67..92203e026648b 100644 --- a/providers/src/airflow/providers/fab/auth_manager/fab_auth_manager.py +++ b/providers/src/airflow/providers/fab/auth_manager/fab_auth_manager.py @@ -25,10 +25,12 @@ import packaging.version from connexion import FlaskApi +from fastapi import FastAPI from flask import Blueprint, g, url_for from packaging.version import Version from sqlalchemy import select from sqlalchemy.orm import Session, joinedload +from starlette.middleware.wsgi import WSGIMiddleware from airflow import __version__ as airflow_version from airflow.auth.managers.base_auth_manager import BaseAuthManager, ResourceMethod @@ -56,6 +58,7 @@ USERS_COMMANDS, ) from airflow.providers.fab.auth_manager.models import Permission, Role, User +from airflow.providers.fab.www.app import create_app from airflow.security import permissions from airflow.security.permissions import ( RESOURCE_AUDIT_LOG, @@ -95,6 +98,7 @@ ) from airflow.providers.common.compat.assets import AssetDetails from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride + from airflow.providers.fab.www.extensions.init_appbuilder import AirflowAppBuilder from airflow.security.permissions import RESOURCE_ASSET else: from airflow.providers.common.compat.security.permissions import RESOURCE_ASSET @@ -138,6 +142,8 @@ class FabAuthManager(BaseAuthManager[User]): This auth manager is responsible for providing a backward compatible user management experience to users. """ + appbuilder: AirflowAppBuilder | None = None + def init(self) -> None: """Run operations when Airflow is initializing.""" if self.appbuilder: @@ -166,6 +172,28 @@ def get_cli_commands() -> list[CLICommand]: commands.append(GroupCommand(name="fab-db", help="Manage FAB", subcommands=DB_COMMANDS)) return commands + def get_fastapi_app(self) -> FastAPI | None: + flask_blueprint = self.get_api_endpoints() + + if not flask_blueprint: + return None + + flask_app = create_app() + flask_app.register_blueprint(flask_blueprint) + + app = FastAPI( + title="FAB auth manager API", + description=( + "This is FAB auth manager API. This API is only available if the auth manager used in " + "the Airflow environment is FAB auth manager. " + "This API provides endpoints to manager users and permissions managed by the FAB auth " + "manager." + ), + ) + app.mount("/", WSGIMiddleware(flask_app)) + + return app + def get_api_endpoints(self) -> None | Blueprint: folder = Path(__file__).parents[0].resolve() # this is airflow/auth/managers/fab/ with folder.joinpath("openapi", "v1.yaml").open() as f: @@ -173,6 +201,7 @@ def get_api_endpoints(self) -> None | Blueprint: return FlaskApi( specification=specification, resolver=_LazyResolver(), + # TODO: change to "/fab/v1" when legacy UI is gone base_path="/auth/fab/v1", options={"swagger_ui": SWAGGER_ENABLED, "swagger_path": SWAGGER_BUNDLE.__fspath__()}, strict_validation=True, diff --git a/providers/src/airflow/providers/fab/www/app.py b/providers/src/airflow/providers/fab/www/app.py index 2cb7fbc273482..a3d9bc007b2c5 100644 --- a/providers/src/airflow/providers/fab/www/app.py +++ b/providers/src/airflow/providers/fab/www/app.py @@ -33,6 +33,7 @@ from airflow.providers.fab.www.extensions.init_manifest_files import configure_manifest_files from airflow.providers.fab.www.extensions.init_security import init_xframe_protection from airflow.providers.fab.www.extensions.init_views import init_error_handlers, init_plugins +from airflow.www.extensions.init_security import init_api_auth app: Flask | None = None @@ -41,7 +42,7 @@ csrf = CSRFProtect() -def create_app(config=None, testing=False): +def create_app(): """Create a new instance of Airflow WWW app.""" flask_app = Flask(__name__) flask_app.secret_key = conf.get("webserver", "SECRET_KEY") @@ -63,6 +64,7 @@ def create_app(config=None, testing=False): configure_logging() configure_manifest_files(flask_app) + init_api_auth(flask_app) with flask_app.app_context(): init_appbuilder(flask_app) @@ -73,11 +75,11 @@ def create_app(config=None, testing=False): return flask_app -def cached_app(config=None, testing=False): +def cached_app(): """Return cached instance of Airflow WWW app.""" global app if not app: - app = create_app(config=config, testing=testing) + app = create_app() return app diff --git a/providers/src/airflow/providers/fab/www/extensions/init_appbuilder.py b/providers/src/airflow/providers/fab/www/extensions/init_appbuilder.py index b233248991c61..465f35545766e 100644 --- a/providers/src/airflow/providers/fab/www/extensions/init_appbuilder.py +++ b/providers/src/airflow/providers/fab/www/extensions/init_appbuilder.py @@ -39,8 +39,8 @@ from flask_appbuilder.views import IndexView from airflow import settings +from airflow.api_fastapi.app import get_auth_manager from airflow.configuration import conf -from airflow.www.extensions.init_auth_manager import init_auth_manager if TYPE_CHECKING: from flask import Flask @@ -181,7 +181,8 @@ def init_app(self, app, session): self._addon_managers = app.config["ADDON_MANAGERS"] self.session = session - auth_manager = init_auth_manager(self) + auth_manager = get_auth_manager() + auth_manager.appbuilder = self self.sm = auth_manager.security_manager self.bm = BabelManager(self) self._add_global_static() diff --git a/providers/tests/amazon/aws/auth_manager/test_aws_auth_manager.py b/providers/tests/amazon/aws/auth_manager/test_aws_auth_manager.py index 7001247a9faa6..10bb69082a772 100644 --- a/providers/tests/amazon/aws/auth_manager/test_aws_auth_manager.py +++ b/providers/tests/amazon/aws/auth_manager/test_aws_auth_manager.py @@ -39,6 +39,7 @@ AwsSecurityManagerOverride, ) from airflow.providers.amazon.aws.auth_manager.user import AwsAuthManagerUser +from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS from airflow.security.permissions import ( RESOURCE_AUDIT_LOG, RESOURCE_CLUSTER_ACTIVITY, @@ -91,23 +92,15 @@ def auth_manager(): } ): with patch.object(AwsAuthManager, "_check_avp_schema_version"): - return AwsAuthManager(None) + return AwsAuthManager() @pytest.fixture -def auth_manager_with_appbuilder(): +def auth_manager_with_appbuilder(auth_manager): flask_app = Flask(__name__) appbuilder = init_appbuilder(flask_app) - with conf_vars( - { - ( - "core", - "auth_manager", - ): "airflow.providers.amazon.aws.auth_manager.aws_auth_manager.AwsAuthManager", - } - ): - with patch.object(AwsAuthManager, "_check_avp_schema_version"): - return AwsAuthManager(appbuilder) + auth_manager.appbuilder = appbuilder + return auth_manager @pytest.fixture @@ -154,6 +147,9 @@ def client_admin(): yield application.create_app(testing=True) +@pytest.mark.skipif( + not AIRFLOW_V_3_0_PLUS, reason="AWS auth manager is only compatible with Airflow >= 3.0.0" +) class TestAwsAuthManager: def test_avp_facade(self, auth_manager): assert hasattr(auth_manager, "avp_facade") diff --git a/providers/tests/amazon/aws/auth_manager/views/test_auth.py b/providers/tests/amazon/aws/auth_manager/views/test_auth.py index f6e545aa57432..2521dd9e43e6c 100644 --- a/providers/tests/amazon/aws/auth_manager/views/test_auth.py +++ b/providers/tests/amazon/aws/auth_manager/views/test_auth.py @@ -22,6 +22,7 @@ from flask import session, url_for from airflow.exceptions import AirflowException +from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS from airflow.www import app as application from tests_common.test_utils.config import conf_vars @@ -69,6 +70,9 @@ def aws_app(): return application.create_app(testing=True, config={"WTF_CSRF_ENABLED": False}) +@pytest.mark.skipif( + not AIRFLOW_V_3_0_PLUS, reason="AWS auth manager is only compatible with Airflow >= 3.0.0" +) @pytest.mark.db_test class TestAwsAuthManagerAuthenticationViews: def test_login_redirect_to_identity_center(self, aws_app): diff --git a/providers/tests/fab/auth_manager/test_fab_auth_manager.py b/providers/tests/fab/auth_manager/test_fab_auth_manager.py index 1994e910f9ebd..b13adc70c1770 100644 --- a/providers/tests/fab/auth_manager/test_fab_auth_manager.py +++ b/providers/tests/fab/auth_manager/test_fab_auth_manager.py @@ -95,7 +95,9 @@ def flask_app(): @pytest.fixture def auth_manager_with_appbuilder(flask_app): appbuilder = init_appbuilder(flask_app) - return FabAuthManager(appbuilder) + auth_manager = FabAuthManager() + auth_manager.appbuilder = appbuilder + return auth_manager @pytest.mark.db_test diff --git a/providers/tests/fab/auth_manager/test_security.py b/providers/tests/fab/auth_manager/test_security.py index 827ed4e7f3bf6..67dd4179b09a5 100644 --- a/providers/tests/fab/auth_manager/test_security.py +++ b/providers/tests/fab/auth_manager/test_security.py @@ -44,11 +44,11 @@ from airflow.providers.fab.auth_manager.models import assoc_permission_role from airflow.providers.fab.auth_manager.models.anonymous_user import AnonymousUser +from airflow.api_fastapi.app import get_auth_manager from airflow.security import permissions from airflow.security.permissions import ACTION_CAN_READ from airflow.www import app as application from airflow.www.auth import get_access_denied_message -from airflow.www.extensions.init_auth_manager import get_auth_manager from airflow.www.utils import CustomSQLAInterface from providers.tests.fab.auth_manager.api_endpoints.api_connexion_utils import ( diff --git a/tests/auth/managers/simple/test_simple_auth_manager.py b/tests/auth/managers/simple/test_simple_auth_manager.py index bf12e6b15d6ee..4a146e6bd3c02 100644 --- a/tests/auth/managers/simple/test_simple_auth_manager.py +++ b/tests/auth/managers/simple/test_simple_auth_manager.py @@ -39,8 +39,9 @@ def auth_manager(): @pytest.fixture def auth_manager_with_appbuilder(): flask_app = Flask(__name__) - appbuilder = init_appbuilder(flask_app) - return SimpleAuthManager(appbuilder) + auth_manager = SimpleAuthManager() + auth_manager.appbuilder = init_appbuilder(flask_app) + return auth_manager @pytest.fixture diff --git a/tests/auth/managers/test_base_auth_manager.py b/tests/auth/managers/test_base_auth_manager.py index c62076a4654d0..0e2924cbcee9b 100644 --- a/tests/auth/managers/test_base_auth_manager.py +++ b/tests/auth/managers/test_base_auth_manager.py @@ -39,9 +39,12 @@ ConfigurationDetails, DagAccessEntity, ) + from airflow.www.extensions.init_appbuilder import AirflowAppBuilder class EmptyAuthManager(BaseAuthManager[BaseUser]): + appbuilder: AirflowAppBuilder | None = None + def get_user(self) -> BaseUser: raise NotImplementedError() @@ -114,7 +117,7 @@ def get_url_logout(self) -> str: @pytest.fixture def auth_manager(): - return EmptyAuthManager(None) + return EmptyAuthManager() class TestBaseAuthManager: @@ -124,6 +127,9 @@ def test_get_cli_commands_return_empty_list(self, auth_manager): def test_get_api_endpoints_return_none(self, auth_manager): assert auth_manager.get_api_endpoints() is None + def test_get_fastapi_app_return_none(self, auth_manager): + assert auth_manager.get_fastapi_app() is None + def test_get_user_name(self, auth_manager): user = Mock() user.get_name.return_value = "test_username" diff --git a/tests_common/test_utils/db.py b/tests_common/test_utils/db.py index bcf1f0052bfed..8c5d59751f55c 100644 --- a/tests_common/test_utils/db.py +++ b/tests_common/test_utils/db.py @@ -71,7 +71,6 @@ def initial_db_init(): from airflow.configuration import conf from airflow.utils import db from airflow.www.extensions.init_appbuilder import init_appbuilder - from airflow.www.extensions.init_auth_manager import get_auth_manager from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS @@ -84,6 +83,12 @@ def initial_db_init(): flask_app = Flask(__name__) flask_app.config["SQLALCHEMY_DATABASE_URI"] = conf.get("database", "SQL_ALCHEMY_CONN") init_appbuilder(flask_app) + + if AIRFLOW_V_3_0_PLUS: + from airflow.api_fastapi.app import get_auth_manager + else: + from airflow.www.extensions.init_auth_manager import get_auth_manager + get_auth_manager().init()