diff --git a/Dockerfile.ci b/Dockerfile.ci index 34b53214229a9..603925475c2c2 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1017,7 +1017,6 @@ function start_api_server_with_examples(){ return fi export AIRFLOW__CORE__LOAD_EXAMPLES=True - export AIRFLOW__API__AUTH_BACKENDS=airflow.providers.fab.auth_manager.api.auth.backend.session,airflow.providers.fab.auth_manager.api.auth.backend.basic_auth export AIRFLOW__WEBSERVER__EXPOSE_CONFIG=True echo echo "${COLOR_BLUE}Initializing database${COLOR_RESET}" diff --git a/airflow/api/__init__.py b/airflow/api/__init__.py deleted file mode 100644 index 10c1ce6cea3c3..0000000000000 --- a/airflow/api/__init__.py +++ /dev/null @@ -1,44 +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. -"""Authentication backend.""" - -from __future__ import annotations - -import logging -from importlib import import_module - -from airflow.configuration import conf -from airflow.exceptions import AirflowException - -log = logging.getLogger(__name__) - - -def load_auth(): - """Load authentication backends.""" - auth_backends = conf.get("api", "auth_backends") - - backends = [] - try: - for backend in auth_backends.split(","): - auth = import_module(backend.strip()) - log.info("Loaded API auth backend: %s", backend) - backends.append(auth) - except ImportError as err: - log.critical("Cannot import %s for API authentication due to: %s", backend, err) - raise AirflowException(err) - return backends diff --git a/airflow/api/auth/__init__.py b/airflow/api/auth/__init__.py deleted file mode 100644 index 217e5db960782..0000000000000 --- a/airflow/api/auth/__init__.py +++ /dev/null @@ -1,17 +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. diff --git a/airflow/api/auth/backend/__init__.py b/airflow/api/auth/backend/__init__.py deleted file mode 100644 index 217e5db960782..0000000000000 --- a/airflow/api/auth/backend/__init__.py +++ /dev/null @@ -1,17 +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. diff --git a/airflow/api/auth/backend/deny_all.py b/airflow/api/auth/backend/deny_all.py deleted file mode 100644 index 8f7132cbdf304..0000000000000 --- a/airflow/api/auth/backend/deny_all.py +++ /dev/null @@ -1,44 +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. -"""Authentication backend that denies all requests.""" - -from __future__ import annotations - -from functools import wraps -from typing import Any, Callable, TypeVar, cast - -from flask import Response - -CLIENT_AUTH: tuple[str, str] | Any | None = None - - -def init_app(_): - """Initialize authentication.""" - - -T = TypeVar("T", bound=Callable) - - -def requires_authentication(function: T): - """Decorate functions that require authentication.""" - - @wraps(function) - def decorated(*args, **kwargs): - return Response("Forbidden", 403) - - return cast(T, decorated) diff --git a/airflow/api/client/__init__.py b/airflow/api/client/__init__.py index 3e1d83169369d..f0d236b9019ab 100644 --- a/airflow/api/client/__init__.py +++ b/airflow/api/client/__init__.py @@ -19,20 +19,8 @@ from __future__ import annotations -from airflow import api from airflow.api.client.local_client import Client def get_current_api_client() -> Client: - """Return current API Client based on current Airflow configuration.""" - auth_backends = api.load_auth() - session = None - for backend in auth_backends: - session_factory = getattr(backend, "create_client_session", None) - if session_factory: - session = session_factory() - api_client = Client( - auth=getattr(backend, "CLIENT_AUTH", None), - session=session, - ) - return api_client + return Client() diff --git a/airflow/cli/cli_config.py b/airflow/cli/cli_config.py index 8104e991a165d..754a8e8394dd3 100644 --- a/airflow/cli/cli_config.py +++ b/airflow/cli/cli_config.py @@ -1570,12 +1570,6 @@ class GroupCommand(NamedTuple): func=lazy_load_command("airflow.cli.commands.remote_commands.provider_command.secrets_backends_list"), args=(ARG_OUTPUT, ARG_VERBOSE), ), - ActionCommand( - name="auth", - help="Get information about API auth backends provided", - func=lazy_load_command("airflow.cli.commands.remote_commands.provider_command.auth_backend_list"), - args=(ARG_OUTPUT, ARG_VERBOSE), - ), ActionCommand( name="executors", help="Get information about executors provided", diff --git a/airflow/cli/commands/remote_commands/config_command.py b/airflow/cli/commands/remote_commands/config_command.py index 1c3c48f24e394..fac8be85543b1 100644 --- a/airflow/cli/commands/remote_commands/config_command.py +++ b/airflow/cli/commands/remote_commands/config_command.py @@ -219,7 +219,11 @@ def message(self) -> str: ), ConfigChange( config=ConfigParameter("api", "auth_backend"), - renamed_to=ConfigParameter("api", "auth_backends"), + renamed_to=ConfigParameter("fab", "auth_backends"), + ), + ConfigChange( + config=ConfigParameter("api", "auth_backends"), + renamed_to=ConfigParameter("fab", "auth_backends"), ), # logging ConfigChange( diff --git a/airflow/cli/commands/remote_commands/provider_command.py b/airflow/cli/commands/remote_commands/provider_command.py index 20f64ed79c406..bd03d07ee45b2 100644 --- a/airflow/cli/commands/remote_commands/provider_command.py +++ b/airflow/cli/commands/remote_commands/provider_command.py @@ -194,19 +194,6 @@ def secrets_backends_list(args): ) -@suppress_logs_and_warning -@providers_configuration_loaded -def auth_backend_list(args): - """List all API auth backend modules at the command line.""" - AirflowConsole().print_as( - data=list(ProvidersManager().auth_backend_module_names), - output=args.output, - mapper=lambda x: { - "api_auth_backend_module": x, - }, - ) - - @suppress_logs_and_warning @providers_configuration_loaded def auth_managers_list(args): diff --git a/airflow/config_templates/config.yml b/airflow/config_templates/config.yml index 699fe1e549df5..f2f36114ac015 100644 --- a/airflow/config_templates/config.yml +++ b/airflow/config_templates/config.yml @@ -1364,15 +1364,6 @@ api: type: string example: ~ default: "" - auth_backends: - description: | - Comma separated list of auth backends to authenticate users of the API. See - `Security: API - `__ for possible values - version_added: 2.3.0 - type: string - example: ~ - default: "airflow.providers.fab.auth_manager.api.auth.backend.session" maximum_page_limit: description: | Used to set the maximum page limit for API requests. If limit passed as param diff --git a/airflow/config_templates/unit_tests.cfg b/airflow/config_templates/unit_tests.cfg index 813c2c025941f..8bb224fb9e684 100644 --- a/airflow/config_templates/unit_tests.cfg +++ b/airflow/config_templates/unit_tests.cfg @@ -71,6 +71,9 @@ celery_logging_level = INFO smtp_mail_from = airflow@example.com [api] + + +[fab] auth_backends = airflow.providers.fab.auth_manager.api.auth.backend.session [hive] diff --git a/airflow/configuration.py b/airflow/configuration.py index 2d01e83c108cf..7347ab7839c86 100644 --- a/airflow/configuration.py +++ b/airflow/configuration.py @@ -374,13 +374,6 @@ def inversed_deprecated_sections(self): "3.0", ), }, - "api": { - "auth_backends": ( - re.compile(r"^airflow\.api\.auth\.backend\.deny_all$|^$"), - "airflow.providers.fab.auth_manager.api.auth.backend.session", - "3.0", - ), - }, "elasticsearch": { "log_id_template": ( re.compile("^" + re.escape("{dag_id}-{task_id}-{logical_date}-{try_number}") + "$"), @@ -674,35 +667,9 @@ def validate(self): version=version, ) - self._upgrade_auth_backends() self._upgrade_postgres_metastore_conn() self.is_validated = True - def _upgrade_auth_backends(self): - """ - Ensure a custom auth_backends setting contains session. - - This is required by the UI for ajax queries. - """ - old_value = self.get("api", "auth_backends", fallback="") - if "airflow.providers.fab.auth_manager.api.auth.backend.session" not in old_value: - new_value = old_value + ",airflow.providers.fab.auth_manager.api.auth.backend.session" - self._update_env_var(section="api", name="auth_backends", new_value=new_value) - self.upgraded_values[("api", "auth_backends")] = old_value - - # if the old value is set via env var, we need to wipe it - # otherwise, it'll "win" over our adjusted value - old_env_var = self._env_var_name("api", "auth_backend") - os.environ.pop(old_env_var, None) - - warnings.warn( - "The auth_backends setting in [api] missed airflow.providers.fab.auth_manager.api.auth.backend.session " - "in the running config, which is needed by the UI. Please update your config before " - "Apache Airflow 3.0.", - FutureWarning, - stacklevel=1, - ) - def _upgrade_postgres_metastore_conn(self): """ Upgrade SQL schemas. diff --git a/airflow/providers_manager.py b/airflow/providers_manager.py index 3c19a5d4797cc..f42cec3b007b9 100644 --- a/airflow/providers_manager.py +++ b/airflow/providers_manager.py @@ -422,7 +422,6 @@ def __init__(self): self._secrets_backend_class_name_set: set[str] = set() self._executor_class_name_set: set[str] = set() self._provider_configs: dict[str, dict[str, Any]] = {} - self._api_auth_backend_module_names: set[str] = set() self._trigger_info_set: set[TriggerInfo] = set() self._notification_info_set: set[NotificationInfo] = set() self._provider_schema_validator = _create_provider_info_schema_validator() @@ -574,12 +573,6 @@ def _initialize_providers_configuration(self): conf.load_providers_configuration() - @provider_info_cache("auth_backends") - def initialize_providers_auth_backends(self): - """Lazy initialization of providers API auth_backends information.""" - self.initialize_providers_list() - self._discover_auth_backends() - @provider_info_cache("plugins") def initialize_providers_plugins(self): self.initialize_providers_list() @@ -1096,14 +1089,6 @@ def _discover_secrets_backends(self) -> None: if _correctness_check(provider_package, secrets_backends_class_name, provider): self._secrets_backend_class_name_set.add(secrets_backends_class_name) - def _discover_auth_backends(self) -> None: - """Retrieve all API auth backends defined in the providers.""" - for provider_package, provider in self._provider_dict.items(): - if provider.data.get("auth-backends"): - for auth_backend_module_name in provider.data["auth-backends"]: - if _correctness_check(provider_package, auth_backend_module_name + ".init_app", provider): - self._api_auth_backend_module_names.add(auth_backend_module_name) - def _discover_executors(self) -> None: """Retrieve all executors defined in the providers.""" for provider_package, provider in self._provider_dict.items(): @@ -1237,12 +1222,6 @@ def secrets_backend_class_names(self) -> list[str]: self.initialize_providers_secrets_backends() return sorted(self._secrets_backend_class_name_set) - @property - def auth_backend_module_names(self) -> list[str]: - """Returns set of API auth backend class names.""" - self.initialize_providers_auth_backends() - return sorted(self._api_auth_backend_module_names) - @property def executor_class_names(self) -> list[str]: self.initialize_providers_executors() @@ -1296,7 +1275,6 @@ def _cleanup(self): self._secrets_backend_class_name_set.clear() self._executor_class_name_set.clear() self._provider_configs.clear() - self._api_auth_backend_module_names.clear() self._trigger_info_set.clear() self._notification_info_set.clear() self._plugins_set.clear() diff --git a/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py b/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py index 073d675230069..03c191ee15417 100644 --- a/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py @@ -1025,8 +1025,6 @@ def _deploy_helm_chart( "-v", "1", "--set", - "config.api.auth_backends=airflow.providers.fab.auth_manager.api.auth.backend.basic_auth", - "--set", "config.logging.logging_level=DEBUG", "--set", f"executor={executor}", diff --git a/docs/apache-airflow-providers/core-extensions/auth-backends.rst b/docs/apache-airflow-providers/core-extensions/auth-backends.rst deleted file mode 100644 index 325b0c2819a26..0000000000000 --- a/docs/apache-airflow-providers/core-extensions/auth-backends.rst +++ /dev/null @@ -1,34 +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. - -Auth backends -------------- - -This is a summary of all Apache Airflow Community provided implementations of authentication backends -exposed via community-managed providers. - -Airflow's authentication for web server and API is based on Flask Application Builder's authentication -capabilities. You can read more about those in -`FAB security docs `_. - -You can also -take a look at Auth backends available in the core Airflow in :doc:`apache-airflow:security/webserver` -or see those provided by the community-managed providers: - -.. airflow-auth-backends:: - :tags: None - :header-separator: " diff --git a/docs/apache-airflow-providers/howto/create-custom-providers.rst b/docs/apache-airflow-providers/howto/create-custom-providers.rst index ba6d8068c4281..7d82d784560b3 100644 --- a/docs/apache-airflow-providers/howto/create-custom-providers.rst +++ b/docs/apache-airflow-providers/howto/create-custom-providers.rst @@ -80,9 +80,6 @@ Exposing customized functionality to the Airflow's core: provider provides. See :doc:`apache-airflow:administration-and-deployment/logging-monitoring/logging-tasks` for description of the logging handlers. -* ``auth-backends`` - this field should contain the authentication backend module names for API/UI. - See :doc:`apache-airflow:security/api` for description of the auth backends. - * ``notifications`` - this field should contain the notification classes. See :doc:`apache-airflow:howto/notifications` for description of the notifications. diff --git a/docs/apache-airflow-providers/index.rst b/docs/apache-airflow-providers/index.rst index c4cf49d1bb696..d23bc916567a3 100644 --- a/docs/apache-airflow-providers/index.rst +++ b/docs/apache-airflow-providers/index.rst @@ -64,15 +64,6 @@ Providers can have their own configuration options which allow you to configure You can see all community-managed providers with their own configuration in :doc:`/core-extensions/configurations` -Auth backends -''''''''''''' - -The providers can add custom authentication backends, that allow you to configure the way how your -web server authenticates your users, integrating it with public or private authentication services. - -You can see all the authentication backends available via community-managed providers in -:doc:`/core-extensions/auth-backends` - Custom connections '''''''''''''''''' diff --git a/docs/apache-airflow/howto/docker-compose/docker-compose.yaml b/docs/apache-airflow/howto/docker-compose/docker-compose.yaml index d1ee96b72b79f..2fa0e82fd6fb2 100644 --- a/docs/apache-airflow/howto/docker-compose/docker-compose.yaml +++ b/docs/apache-airflow/howto/docker-compose/docker-compose.yaml @@ -60,8 +60,6 @@ x-airflow-common: AIRFLOW__CORE__FERNET_KEY: '' AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true' AIRFLOW__CORE__LOAD_EXAMPLES: 'true' - AIRFLOW__API__AUTH_BACKENDS: >- - airflow.providers.fab.auth_manager.api.auth.backend.basic_auth,airflow.providers.fab.auth_manager.api.auth.backend.session AIRFLOW__WORKERS__EXECUTION_API_SERVER_URL: 'http://airflow-apiserver:8080/execution/' # yamllint disable rule:line-length # Use simple http server on scheduler for health checks diff --git a/docs/apache-airflow/public-airflow-interface.rst b/docs/apache-airflow/public-airflow-interface.rst index 53d6dfc5d7d96..83608f334ef27 100644 --- a/docs/apache-airflow/public-airflow-interface.rst +++ b/docs/apache-airflow/public-airflow-interface.rst @@ -335,14 +335,6 @@ public, but the different implementations of auth managers are not (i.e. FabAuth You can read more about auth managers and how to write your own in :doc:`core-concepts/auth-manager/index`. -Authentication Backends ------------------------ - -Authentication backends can extend the way how Airflow authentication mechanism works. You can find out more -about authentication in :doc:`apache-airflow-providers:core-extensions/auth-backends` that also shows available -Authentication backends implemented in the community providers. In case of authentication backend implemented in a -provider, it is then part of the provider's public interface and not Airflow's. - Connections ----------- diff --git a/docs/conf.py b/docs/conf.py index 9f0a9686facfd..c3e2121f3feb5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -213,6 +213,8 @@ # Included in the cluster-policies doc "_api/airflow/policies/index.rst", "README.rst", + "_api/client", + "_api/common", ] elif PACKAGE_NAME.startswith("apache-airflow-providers-"): extensions.extend( diff --git a/docs/exts/operators_and_hooks_ref.py b/docs/exts/operators_and_hooks_ref.py index 39ff017558e9a..6c3506a066c49 100644 --- a/docs/exts/operators_and_hooks_ref.py +++ b/docs/exts/operators_and_hooks_ref.py @@ -406,19 +406,6 @@ def render_content( ) -class AuthBackendDirective(BaseJinjaReferenceDirective): - """Generate list of auth backend handlers""" - - def render_content( - self, *, tags: set[str] | None, header_separator: str = DEFAULT_HEADER_SEPARATOR - ) -> str: - return _common_render_list_content( - header_separator=header_separator, - resource_type="auth-backends", - template="auth_backend.rst.jinja2", - ) - - class AuthConfigurations(BaseJinjaReferenceDirective): """Generate list of configurations""" @@ -530,7 +517,6 @@ def setup(app): app.add_directive("operators-hooks-ref", OperatorsHooksReferenceDirective) app.add_directive("transfers-ref", TransfersReferenceDirective) app.add_directive("airflow-logging", LoggingDirective) - app.add_directive("airflow-auth-backends", AuthBackendDirective) app.add_directive("airflow-configurations", AuthConfigurations) app.add_directive("airflow-secrets-backends", SecretsBackendDirective) app.add_directive("airflow-connections", ConnectionsDirective) @@ -587,19 +573,6 @@ def logging(header_separator: str): ) -@cli.command() -@option_header_separator -def auth_backends(header_separator: str): - """Renders Logger content""" - print( - _common_render_list_content( - header_separator=header_separator, - resource_type="auth-backends", - template="auth_backend.rst.jinja2", - ) - ) - - @cli.command() @option_header_separator def secret_backends(header_separator: str): diff --git a/docs/exts/templates/auth_backend.rst.jinja2 b/docs/exts/templates/auth_backend.rst.jinja2 deleted file mode 100644 index 781e4b1a936ef..0000000000000 --- a/docs/exts/templates/auth_backend.rst.jinja2 +++ /dev/null @@ -1,27 +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. -#} -{%for provider, provider_dict in items.items() %} -{{ provider_dict['name'] }} -{{ header_separator * (provider_dict['name']|length) }} - -{% for backend in provider_dict['auth-backends'] -%} -- :class:`~{{ backend }}` -{% endfor -%} - -{% endfor %} diff --git a/kubernetes_tests/test_base.py b/kubernetes_tests/test_base.py index 31248b02ac420..740fd6d606aa6 100644 --- a/kubernetes_tests/test_base.py +++ b/kubernetes_tests/test_base.py @@ -330,6 +330,8 @@ def set_api_server_base_url_config(self) -> bool: # set [api/base_url] with `f"http://{KUBERNETES_HOST_PORT}"` in airflow.cfg # The airflow.cfg is toml format, so we need to convert it to json airflow_cfg_dict = self._parse_airflow_cfg_as_dict(original_airflow_cfg) + if "api" not in airflow_cfg_dict: + airflow_cfg_dict["api"] = {} airflow_cfg_dict["api"]["base_url"] = f"http://{KUBERNETES_HOST_PORT}" # update the configmap with the new airflow.cfg patch_configmap_result = check_output( diff --git a/newsfragments/47399.significant.rst b/newsfragments/47399.significant.rst new file mode 100644 index 0000000000000..bd84287acec61 --- /dev/null +++ b/newsfragments/47399.significant.rst @@ -0,0 +1,20 @@ +Removed auth backends. Auth backends are no longer used in Airflow 3. Please refer to documentation on how to use Airflow 3 public API. + +Moved the configuration ``[api] auth_backends`` to ``[fab] auth_backends``. + +* Types of change + + * [ ] Dag changes + * [x] Config changes + * [ ] API changes + * [ ] CLI changes + * [ ] Behaviour changes + * [ ] Plugin changes + * [ ] Dependency changes + * [x] Code interface changes + +* Migration rules needed + + * ``airflow config lint`` + + * [x] ``api.auth_backends`` → ``fab.auth_backends`` diff --git a/providers/fab/provider.yaml b/providers/fab/provider.yaml index 478c870bac961..66c4e12ed65a7 100644 --- a/providers/fab/provider.yaml +++ b/providers/fab/provider.yaml @@ -76,6 +76,13 @@ config: type: string example: ~ default: "True" + auth_backends: + description: | + Comma separated list of auth backends to authenticate users of the API. + version_added: 2.3.0 + type: string + example: ~ + default: "airflow.providers.fab.auth_manager.api.auth.backend.session" auth-managers: - airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager diff --git a/providers/fab/src/airflow/providers/fab/get_provider_info.py b/providers/fab/src/airflow/providers/fab/get_provider_info.py index fa183c9aa293b..f4409eab8a9b7 100644 --- a/providers/fab/src/airflow/providers/fab/get_provider_info.py +++ b/providers/fab/src/airflow/providers/fab/get_provider_info.py @@ -72,6 +72,13 @@ def get_provider_info(): "example": None, "default": "True", }, + "auth_backends": { + "description": "Comma separated list of auth backends to authenticate users of the API.\n", + "version_added": "2.3.0", + "type": "string", + "example": None, + "default": "airflow.providers.fab.auth_manager.api.auth.backend.session", + }, }, } }, diff --git a/providers/fab/src/airflow/providers/fab/www/app.py b/providers/fab/src/airflow/providers/fab/www/app.py index 82f0dd3c1f22f..22e164e1c8c2e 100644 --- a/providers/fab/src/airflow/providers/fab/www/app.py +++ b/providers/fab/src/airflow/providers/fab/www/app.py @@ -87,9 +87,9 @@ def create_app(enable_plugins: bool): # FAB auth manager. One example is ``run_update_fastapi_api_spec``, it calls # ``FabAuthManager().get_fastapi_app()`` to generate the openapi documentation regardless of the # configured auth manager. - if enable_plugins or not isinstance(get_auth_manager(), FabAuthManager): + if enable_plugins: init_plugins(flask_app) - else: + elif isinstance(get_auth_manager(), FabAuthManager): init_api_auth_provider(flask_app) init_api_error_handlers(flask_app) init_jinja_globals(flask_app, enable_plugins=enable_plugins) diff --git a/providers/fab/src/airflow/providers/fab/www/extensions/init_security.py b/providers/fab/src/airflow/providers/fab/www/extensions/init_security.py index ab594d3c9109e..6f62880808014 100644 --- a/providers/fab/src/airflow/providers/fab/www/extensions/init_security.py +++ b/providers/fab/src/airflow/providers/fab/www/extensions/init_security.py @@ -46,7 +46,9 @@ def apply_caching(response): def init_api_auth(app): """Load authentication backends.""" - auth_backends = conf.get("api", "auth_backends") + auth_backends = conf.get( + "fab", "auth_backends", fallback="airflow.providers.fab.auth_manager.api.auth.backend.session" + ) app.api_auth = [] try: diff --git a/providers/fab/tests/unit/fab/auth_manager/api_endpoints/test_auth.py b/providers/fab/tests/unit/fab/auth_manager/api_endpoints/test_auth.py index 1f71295a5b548..d268ab93ab0ce 100644 --- a/providers/fab/tests/unit/fab/auth_manager/api_endpoints/test_auth.py +++ b/providers/fab/tests/unit/fab/auth_manager/api_endpoints/test_auth.py @@ -59,7 +59,7 @@ def with_basic_auth_backend(self, minimal_app_for_auth_api): try: with conf_vars( - {("api", "auth_backends"): "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth"} + {("fab", "auth_backends"): "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth"} ): init_api_auth(minimal_app_for_auth_api) yield diff --git a/providers/fab/tests/unit/fab/auth_manager/conftest.py b/providers/fab/tests/unit/fab/auth_manager/conftest.py index f56dac8ab80f8..ebb105372941b 100644 --- a/providers/fab/tests/unit/fab/auth_manager/conftest.py +++ b/providers/fab/tests/unit/fab/auth_manager/conftest.py @@ -47,7 +47,7 @@ def factory(): with conf_vars( { ( - "api", + "fab", "auth_backends", ): "unit.fab.auth_manager.api_endpoints.remote_user_api_auth_backend,airflow.providers.fab.auth_manager.api.auth.backend.session", ( diff --git a/providers/google/tests/unit/google/common/auth_backend/test_google_openid.py b/providers/google/tests/unit/google/common/auth_backend/test_google_openid.py index 5ec863080f20a..3a7090d7f0e9f 100644 --- a/providers/google/tests/unit/google/common/auth_backend/test_google_openid.py +++ b/providers/google/tests/unit/google/common/auth_backend/test_google_openid.py @@ -25,12 +25,11 @@ if not AIRFLOW_V_3_0_PLUS: pytest.skip( - "``providers/google/tests/unit/google/common/auth_backend/test_google_openid.py`` is only compatible with Airflow 2.X.", + "``providers/google/tests/unit/google/common/auth_backend/test_google_openid.py`` is only compatible with Airflow 3.X.", allow_module_level=True, ) from tests_common.test_utils.config import conf_vars -from tests_common.test_utils.db import clear_db_pools @pytest.fixture(scope="module") @@ -38,7 +37,7 @@ def google_openid_app(): def factory(): with conf_vars( { - ("api", "auth_backends"): "airflow.providers.google.common.auth_backend.google_openid", + ("fab", "auth_backends"): "airflow.providers.google.common.auth_backend.google_openid", ( "core", "auth_manager", @@ -54,20 +53,27 @@ def factory(): return factory() +def delete_user(app, username): + appbuilder = app.appbuilder + for user in appbuilder.sm.get_all_users(): + if user.username == username: + appbuilder.sm.del_register_user(user) + break + + @pytest.fixture(scope="module") def admin_user(google_openid_app): appbuilder = google_openid_app.appbuilder role_admin = appbuilder.sm.find_role("Admin") - tester = appbuilder.sm.find_user(username="test") - if not tester: - appbuilder.sm.add_user( - username="test", - first_name="test", - last_name="test", - email="test@fab.org", - role=role_admin, - password="test", - ) + delete_user(google_openid_app, "test") + appbuilder.sm.add_user( + username="test", + first_name="test", + last_name="test", + email="test@fab.org", + role=role_admin, + password="test", + ) return role_admin @@ -80,7 +86,6 @@ def _set_attrs(self, google_openid_app, admin_user) -> None: @mock.patch("google.oauth2.id_token.verify_token") def test_success(self, mock_verify_token): - clear_db_pools() mock_verify_token.return_value = { "iss": "accounts.google.com", "email_verified": True, @@ -132,14 +137,14 @@ def test_user_not_exists(self, mock_verify_token): assert response.status_code == 401 - @conf_vars({("api", "auth_backends"): "airflow.providers.google.common.auth_backend.google_openid"}) + @conf_vars({("fab", "auth_backends"): "airflow.providers.google.common.auth_backend.google_openid"}) def test_missing_id_token(self): with self.app.test_client() as test_client: response = test_client.get("/fab/v1/users") assert response.status_code == 401 - @conf_vars({("api", "auth_backends"): "airflow.providers.google.common.auth_backend.google_openid"}) + @conf_vars({("fab", "auth_backends"): "airflow.providers.google.common.auth_backend.google_openid"}) @mock.patch("google.oauth2.id_token.verify_token") def test_invalid_id_token(self, mock_verify_token): mock_verify_token.side_effect = GoogleAuthError("Invalid token") diff --git a/scripts/docker/entrypoint_ci.sh b/scripts/docker/entrypoint_ci.sh index 4caa698e24256..3527e442336bc 100755 --- a/scripts/docker/entrypoint_ci.sh +++ b/scripts/docker/entrypoint_ci.sh @@ -347,7 +347,6 @@ function start_api_server_with_examples(){ return fi export AIRFLOW__CORE__LOAD_EXAMPLES=True - export AIRFLOW__API__AUTH_BACKENDS=airflow.providers.fab.auth_manager.api.auth.backend.session,airflow.providers.fab.auth_manager.api.auth.backend.basic_auth export AIRFLOW__WEBSERVER__EXPOSE_CONFIG=True echo echo "${COLOR_BLUE}Initializing database${COLOR_RESET}" diff --git a/scripts/in_container/verify_providers.py b/scripts/in_container/verify_providers.py index 7bd2f9d22a204..33fcdb505b78b 100755 --- a/scripts/in_container/verify_providers.py +++ b/scripts/in_container/verify_providers.py @@ -743,8 +743,6 @@ def run_provider_discovery(): subprocess.run(["airflow", "providers", "logging"], check=True) console.print("[bright_blue]List all secrets[/]\n") subprocess.run(["airflow", "providers", "secrets"], check=True) - console.print("[bright_blue]List all auth backends[/]\n") - subprocess.run(["airflow", "providers", "auth"], check=True) console.print("[bright_blue]List all triggers[/]\n") subprocess.run(["airflow", "providers", "triggers"], check=True) console.print("[bright_blue]List all executors[/]\n") diff --git a/tests/always/test_providers_manager.py b/tests/always/test_providers_manager.py index 670ee1037f460..39637c6ea513b 100644 --- a/tests/always/test_providers_manager.py +++ b/tests/always/test_providers_manager.py @@ -416,11 +416,6 @@ def test_secrets_backends(self): secrets_backends_class_names = list(provider_manager.secrets_backend_class_names) assert len(secrets_backends_class_names) > 4 - def test_auth_backends(self): - provider_manager = ProvidersManager() - auth_backend_module_names = list(provider_manager.auth_backend_module_names) - assert len(auth_backend_module_names) > 0 - def test_trigger(self): provider_manager = ProvidersManager() trigger_class_names = list(provider_manager.trigger) diff --git a/tests/core/test_configuration.py b/tests/core/test_configuration.py index 3b86d8e1015be..2b8f7d08ab3c9 100644 --- a/tests/core/test_configuration.py +++ b/tests/core/test_configuration.py @@ -614,31 +614,6 @@ def test_config_value_types(self, key, type): section_dict = conf.getsection("example_section") assert isinstance(section_dict[key], type) - def test_auth_backends_adds_session(self): - with patch("os.environ", {"AIRFLOW__API__AUTH_BACKEND": None}): - test_conf = AirflowConfigParser(default_config="") - # Guarantee we have deprecated settings, so we test the deprecation - # lookup even if we remove this explicit fallback - test_conf.deprecated_values = { - "api": { - "auth_backends": ( - re.compile(r"^airflow\.api\.auth\.backend\.deny_all$|^$"), - "airflow.providers.fab.auth_manager.api.auth.backend.session", - "3.0", - ), - }, - } - test_conf.read_dict( - {"api": {"auth_backends": "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth"}} - ) - - with pytest.warns(FutureWarning): - test_conf.validate() - assert ( - test_conf.get("api", "auth_backends") - == "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth,airflow.providers.fab.auth_manager.api.auth.backend.session" - ) - def test_command_from_env(self): test_cmdenv_config = textwrap.dedent("""\ [testcmdenv]