This is an automated email from the ASF dual-hosted git repository.
vincbeck 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 a1db3ee999a Add option in auth manager interface to define FastAPI api
(#45009)
a1db3ee999a is described below
commit a1db3ee999a875def035ce8c6d028cc237ba2b5f
Author: Vincent <[email protected]>
AuthorDate: Fri Dec 20 18:19:44 2024 -0500
Add option in auth manager interface to define FastAPI api (#45009)
---
airflow/api_connexion/endpoints/asset_endpoint.py | 2 +-
airflow/api_connexion/endpoints/dag_endpoint.py | 2 +-
airflow/api_connexion/endpoints/dag_parsing.py | 2 +-
.../api_connexion/endpoints/dag_run_endpoint.py | 2 +-
.../api_connexion/endpoints/dag_source_endpoint.py | 2 +-
.../api_connexion/endpoints/dag_stats_endpoint.py | 2 +-
.../endpoints/import_error_endpoint.py | 2 +-
.../endpoints/task_instance_endpoint.py | 2 +-
airflow/api_connexion/endpoints/xcom_endpoint.py | 2 +-
airflow/api_connexion/security.py | 2 +-
airflow/api_fastapi/app.py | 34 +++++-----
airflow/auth/managers/base_auth_manager.py | 19 +++---
.../auth/managers/simple/simple_auth_manager.py | 4 ++
airflow/auth/managers/simple/views/auth.py | 2 +-
airflow/cli/cli_parser.py | 2 +-
airflow/www/auth.py | 2 +-
airflow/www/decorators.py | 2 +-
airflow/www/extensions/init_appbuilder.py | 6 +-
airflow/www/extensions/init_auth_manager.py | 73 ----------------------
airflow/www/extensions/init_jinja_globals.py | 2 +-
airflow/www/extensions/init_views.py | 2 +-
airflow/www/security_manager.py | 2 +-
airflow/www/utils.py | 2 +-
airflow/www/views.py | 5 +-
newsfragments/aip-79.significant.rst | 11 ++++
.../src/airflow/providers/amazon/CHANGELOG.rst | 8 +++
.../amazon/aws/auth_manager/aws_auth_manager.py | 38 +++++------
.../providers/fab/auth_manager/fab_auth_manager.py | 29 +++++++++
providers/src/airflow/providers/fab/www/app.py | 8 ++-
.../fab/www/extensions/init_appbuilder.py | 5 +-
.../aws/auth_manager/test_aws_auth_manager.py | 20 +++---
.../amazon/aws/auth_manager/views/test_auth.py | 4 ++
.../fab/auth_manager/test_fab_auth_manager.py | 4 +-
providers/tests/fab/auth_manager/test_security.py | 2 +-
.../managers/simple/test_simple_auth_manager.py | 5 +-
tests/auth/managers/test_base_auth_manager.py | 8 ++-
tests_common/test_utils/db.py | 7 ++-
37 files changed, 159 insertions(+), 167 deletions(-)
diff --git a/airflow/api_connexion/endpoints/asset_endpoint.py
b/airflow/api_connexion/endpoints/asset_endpoint.py
index 64930b12494..30bb2d513ae 100644
--- a/airflow/api_connexion/endpoints/asset_endpoint.py
+++ b/airflow/api_connexion/endpoints/asset_endpoint.py
@@ -43,6 +43,7 @@ from airflow.api_connexion.schemas.asset_schema import (
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.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.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 bb5d4196915..8c82b864678 100644
--- a/airflow/api_connexion/endpoints/dag_endpoint.py
+++ b/airflow/api_connexion/endpoints/dag_endpoint.py
@@ -37,6 +37,7 @@ from airflow.api_connexion.schemas.dag_schema import (
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.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.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 1580529bcc4..c6fde8d851b 100644
--- a/airflow/api_connexion/endpoints/dag_parsing.py
+++ b/airflow/api_connexion/endpoints/dag_parsing.py
@@ -26,12 +26,12 @@ from sqlalchemy import exc, select
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 985efc7fc89..4574a7b77c7 100644
--- a/airflow/api_connexion/endpoints/dag_run_endpoint.py
+++ b/airflow/api_connexion/endpoints/dag_run_endpoint.py
@@ -59,6 +59,7 @@ from airflow.api_connexion.schemas.task_instance_schema
import (
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.session import NEW_SESSION, provide_session
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 1c6e34fc2b1..c1c4929aed9 100644
--- a/airflow/api_connexion/endpoints/dag_source_endpoint.py
+++ b/airflow/api_connexion/endpoints/dag_source_endpoint.py
@@ -26,12 +26,12 @@ from sqlalchemy import select
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 c4d8701f8d3..05489dd27a7 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 import security
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 76fad6cb92d..d2780fe941b 100644
--- a/airflow/api_connexion/endpoints/import_error_endpoint.py
+++ b/airflow/api_connexion/endpoints/import_error_endpoint.py
@@ -29,12 +29,12 @@ from airflow.api_connexion.schemas.error_schema import (
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 b599f24ab31..8751187a70a 100644
--- a/airflow/api_connexion/endpoints/task_instance_endpoint.py
+++ b/airflow/api_connexion/endpoints/task_instance_endpoint.py
@@ -47,6 +47,7 @@ from airflow.api_connexion.schemas.task_instance_schema
import (
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.db import get_query_count
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 cb3faf7379e..3951e8be467 100644
--- a/airflow/api_connexion/endpoints/xcom_endpoint.py
+++ b/airflow/api_connexion/endpoints/xcom_endpoint.py
@@ -31,13 +31,13 @@ from airflow.api_connexion.schemas.xcom_schema import (
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 1098de3a1f4..3601116fdb4 100644
--- a/airflow/api_connexion/security.py
+++ b/airflow/api_connexion/security.py
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Callable, TypeVar, cast
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 @@ from airflow.auth.managers.models.resource_details import (
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 b90993571b4..3c9aa39ae70 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 ea0c921c98c..4ebf13a612b 100644
--- a/airflow/auth/managers/base_auth_manager.py
+++ b/airflow/auth/managers/base_auth_manager.py
@@ -36,6 +36,7 @@ from airflow.utils.log.logging_mixin import LoggingMixin
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 @@ if TYPE_CHECKING:
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 @@ class BaseAuthManager(Generic[T], LoggingMixin):
"""
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 @@ class BaseAuthManager(Generic[T], LoggingMixin):
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 67eb373ced7..3d53d1d097b 100644
--- a/airflow/auth/managers/simple/simple_auth_manager.py
+++ b/airflow/auth/managers/simple/simple_auth_manager.py
@@ -43,6 +43,7 @@ if TYPE_CHECKING:
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 6bf92cf0bc7..58d5f008cc4 100644
--- a/airflow/auth/managers/simple/views/auth.py
+++ b/airflow/auth/managers/simple/views/auth.py
@@ -21,12 +21,12 @@ from urllib.parse import parse_qsl, urlencode, urlsplit,
urlunsplit
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 a48e6b438d3..d2f9a73653b 100644
--- a/airflow/cli/cli_parser.py
+++ b/airflow/cli/cli_parser.py
@@ -36,6 +36,7 @@ from typing import TYPE_CHECKING
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.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
if TYPE_CHECKING:
from airflow.cli.cli_config import (
diff --git a/airflow/www/auth.py b/airflow/www/auth.py
index 2b864273e5f..101b1463596 100644
--- a/airflow/www/auth.py
+++ b/airflow/www/auth.py
@@ -30,6 +30,7 @@ from flask_appbuilder.const import (
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.auth.managers.models.resource_details import (
)
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 fc0ad893369..0651820ce78 100644
--- a/airflow/www/decorators.py
+++ b/airflow/www/decorators.py
@@ -29,10 +29,10 @@ import pendulum
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 9f8ef4b6028..e985f318a45 100644
--- a/airflow/www/extensions/init_appbuilder.py
+++ b/airflow/www/extensions/init_appbuilder.py
@@ -39,8 +39,8 @@ from flask_appbuilder.menu import Menu
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 @@ class AirflowAppBuilder:
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 6e6f1f8af1b..00000000000
--- 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 a64212ef7b3..60761a9b3b8 100644
--- a/airflow/www/extensions/init_jinja_globals.py
+++ b/airflow/www/extensions/init_jinja_globals.py
@@ -21,11 +21,11 @@ import logging
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 faed1761c19..a540bdd3a67 100644
--- a/airflow/www/extensions/init_views.py
+++ b/airflow/www/extensions/init_views.py
@@ -27,11 +27,11 @@ from connexion.exceptions import BadRequestProblem
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 6d782410c3d..d02d7efcf77 100644
--- a/airflow/www/security_manager.py
+++ b/airflow/www/security_manager.py
@@ -24,6 +24,7 @@ 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,
@@ -63,7 +64,6 @@ from airflow.security.permissions import (
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 8a74a74d5a8..727139a9a6b 100644
--- a/airflow/www/utils.py
+++ b/airflow/www/utils.py
@@ -40,6 +40,7 @@ from pygments.formatters import HtmlFormatter
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.helpers import alchemy_to_dict
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 d25db666167..a654c9ad720 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -88,6 +88,7 @@ from airflow.api.common.mark_tasks import (
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.utils.types import NOTSET,
DagRunTriggeredByType
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 00000000000..18a3884054c
--- /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 47572b55e4c..3a1d9589aaa 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
<https://github.com/apache/airflow/blob/main/PROVIDERS.rst#minimum-supported-version-of-airflow-for-community-managed-providers>`_.
+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 378b7d49d88..37da993deab 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 typing import TYPE_CHECKING, cast
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 @@ from
airflow.providers.amazon.aws.auth_manager.security_manager.aws_security_man
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 cdb0055003b..92203e02664 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 @@ from typing import TYPE_CHECKING, Any
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 @@ from
airflow.providers.fab.auth_manager.cli_commands.definition import (
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 @@ if TYPE_CHECKING:
)
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 @@ class FabAuthManager(BaseAuthManager[User]):
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 @@ class FabAuthManager(BaseAuthManager[User]):
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 2cb7fbc2734..a3d9bc007b2 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_jinja_globals
import init_jinja_g
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 @@ app: Flask | None = None
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 b233248991c..465f3554576 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.menu import Menu
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 @@ class AirflowAppBuilder:
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 7001247a9fa..10bb69082a7 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 @@ from
airflow.providers.amazon.aws.auth_manager.security_manager.aws_security_man
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)
[email protected](
+ 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 f6e545aa574..2521dd9e43e 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 @@ import pytest
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})
[email protected](
+ 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 1994e910f9e..b13adc70c17 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 827ed4e7f3b..67dd4179b09 100644
--- a/providers/tests/fab/auth_manager/test_security.py
+++ b/providers/tests/fab/auth_manager/test_security.py
@@ -44,11 +44,11 @@ with ignore_provider_compatibility_error("2.9.0+",
__file__):
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 bf12e6b15d6..4a146e6bd3c 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 c62076a4654..0e2924cbcee 100644
--- a/tests/auth/managers/test_base_auth_manager.py
+++ b/tests/auth/managers/test_base_auth_manager.py
@@ -39,9 +39,12 @@ if TYPE_CHECKING:
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 @@ class EmptyAuthManager(BaseAuthManager[BaseUser]):
@pytest.fixture
def auth_manager():
- return EmptyAuthManager(None)
+ return EmptyAuthManager()
class TestBaseAuthManager:
@@ -124,6 +127,9 @@ class TestBaseAuthManager:
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 bcf1f0052bf..8c5d59751f5 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()