From 8ddfa418e059367c14fc3b94c6160cf02d1c986f Mon Sep 17 00:00:00 2001 From: Vincent Beck Date: Thu, 5 Oct 2023 15:45:25 -0400 Subject: [PATCH 1/6] Add back decorator `has_access` --- airflow/auth/managers/fab/decorators/auth.py | 89 ++++++++++++++++++++ airflow/www/auth.py | 23 ++++- 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 airflow/auth/managers/fab/decorators/auth.py diff --git a/airflow/auth/managers/fab/decorators/auth.py b/airflow/auth/managers/fab/decorators/auth.py new file mode 100644 index 0000000000000..b9e51396abb61 --- /dev/null +++ b/airflow/auth/managers/fab/decorators/auth.py @@ -0,0 +1,89 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import logging +from functools import wraps +from typing import Callable, Sequence, TypeVar, cast + +from flask import current_app, render_template, request + +from airflow.configuration import conf +from airflow.utils.net import get_hostname +from airflow.www.auth import _has_access +from airflow.www.extensions.init_auth_manager import get_auth_manager + +T = TypeVar("T", bound=Callable) + +log = logging.getLogger(__name__) + + +def has_access_fab(permissions: Sequence[tuple[str, str]] | None = None) -> Callable[[T], T]: + """ + Factory for decorator that checks current user's permissions against required permissions. + + This decorator is only kept for backward compatible reasons. The decorator + ``airflow.www.auth.has_access``, which redirects to this decorator, is widely used in user plugins. + Thus, we need to keep it. + See https://github.com/apache/airflow/pull/33213#discussion_r1346287224 + """ + + def requires_access_decorator(func: T): + @wraps(func) + def decorated(*args, **kwargs): + __tracebackhide__ = True # Hide from pytest traceback. + + appbuilder = current_app.appbuilder + + dag_id_kwargs = kwargs.get("dag_id") + dag_id_args = request.args.get("dag_id") + dag_id_form = request.form.get("dag_id") + dag_id_json = request.json.get("dag_id") if request.is_json else None + all_dag_ids = [dag_id_kwargs, dag_id_args, dag_id_form, dag_id_json] + unique_dag_ids = set(dag_id for dag_id in all_dag_ids if dag_id is not None) + + if len(unique_dag_ids) > 1: + log.warning( + f"There are different dag_ids passed in the request: {unique_dag_ids}. Returning 403." + ) + log.warning( + f"kwargs: {dag_id_kwargs}, args: {dag_id_args}, " + f"form: {dag_id_form}, json: {dag_id_json}" + ) + return ( + render_template( + "airflow/no_roles_permissions.html", + hostname=get_hostname() + if conf.getboolean("webserver", "EXPOSE_HOSTNAME") + else "redact", + logout_url=get_auth_manager().get_url_logout(), + ), + 403, + ) + dag_id = unique_dag_ids.pop() if unique_dag_ids else None + + return _has_access( + is_authorized=appbuilder.sm.check_authorization(permissions, dag_id), + func=func, + args=args, + kwargs=kwargs, + ) + + return cast(T, decorated) + + return requires_access_decorator diff --git a/airflow/www/auth.py b/airflow/www/auth.py index c943779ab09c5..abfe9d5e2243e 100644 --- a/airflow/www/auth.py +++ b/airflow/www/auth.py @@ -17,11 +17,13 @@ from __future__ import annotations import logging +import warnings from functools import wraps -from typing import TYPE_CHECKING, Callable, TypeVar, cast +from typing import TYPE_CHECKING, Callable, Sequence, TypeVar, cast from flask import flash, g, redirect, render_template, request +from airflow.auth.managers.fab.decorators.auth import has_access_fab from airflow.auth.managers.models.resource_details import ( ConnectionDetails, DagAccessEntity, @@ -44,6 +46,25 @@ def get_access_denied_message(): return conf.get("webserver", "access_denied_message") +def has_access(permissions: Sequence[tuple[str, str]] | None = None) -> Callable[[T], T]: + """ + Factory for decorator that checks current user's permissions against required permissions. + + Deprecated. Do not use this decorator, use one of the decorator `has_access_cluster_*` defined in + airflow/www/auth.py instead. + + This decorator is widely used in user plugins, do not remove it. See + https://github.com/apache/airflow/pull/33213#discussion_r1346287224 + """ + warnings.warn( + "The 'has_access' decorator is deprecated. Please use one of the decorator `has_access_cluster_*`" + "defined in airflow/www/auth.py instead.", + DeprecationWarning, + stacklevel=2, + ) + return has_access_fab(permissions) + + def _has_access_no_details(is_authorized_callback: Callable[[], bool]) -> Callable[[T], T]: """ Generic Decorator that checks current user's permissions against required permissions. From 7595a2e7f791d0f504d93257c35e7ae3118f2b49 Mon Sep 17 00:00:00 2001 From: Vincent Beck Date: Thu, 5 Oct 2023 16:51:26 -0400 Subject: [PATCH 2/6] Address feedbacks --- airflow/auth/managers/fab/decorators/auth.py | 4 +++- airflow/www/auth.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/airflow/auth/managers/fab/decorators/auth.py b/airflow/auth/managers/fab/decorators/auth.py index b9e51396abb61..5f0f16147075b 100644 --- a/airflow/auth/managers/fab/decorators/auth.py +++ b/airflow/auth/managers/fab/decorators/auth.py @@ -33,7 +33,7 @@ log = logging.getLogger(__name__) -def has_access_fab(permissions: Sequence[tuple[str, str]] | None = None) -> Callable[[T], T]: +def _has_access_fab(permissions: Sequence[tuple[str, str]] | None = None) -> Callable[[T], T]: """ Factory for decorator that checks current user's permissions against required permissions. @@ -41,6 +41,8 @@ def has_access_fab(permissions: Sequence[tuple[str, str]] | None = None) -> Call ``airflow.www.auth.has_access``, which redirects to this decorator, is widely used in user plugins. Thus, we need to keep it. See https://github.com/apache/airflow/pull/33213#discussion_r1346287224 + + :meta private: """ def requires_access_decorator(func: T): diff --git a/airflow/www/auth.py b/airflow/www/auth.py index abfe9d5e2243e..ef63d93a9fa36 100644 --- a/airflow/www/auth.py +++ b/airflow/www/auth.py @@ -23,13 +23,14 @@ from flask import flash, g, redirect, render_template, request -from airflow.auth.managers.fab.decorators.auth import has_access_fab +from airflow.auth.managers.fab.decorators.auth import _has_access_fab from airflow.auth.managers.models.resource_details import ( ConnectionDetails, DagAccessEntity, DagDetails, ) from airflow.configuration import conf +from airflow.exceptions import RemovedInAirflow3Warning from airflow.utils.net import get_hostname from airflow.www.extensions.init_auth_manager import get_auth_manager @@ -59,10 +60,10 @@ def has_access(permissions: Sequence[tuple[str, str]] | None = None) -> Callable warnings.warn( "The 'has_access' decorator is deprecated. Please use one of the decorator `has_access_cluster_*`" "defined in airflow/www/auth.py instead.", - DeprecationWarning, + RemovedInAirflow3Warning, stacklevel=2, ) - return has_access_fab(permissions) + return _has_access_fab(permissions) def _has_access_no_details(is_authorized_callback: Callable[[], bool]) -> Callable[[T], T]: From 5c1680164ca572b07e44cc8cd9c01b79d5c4868d Mon Sep 17 00:00:00 2001 From: Vincent Beck Date: Thu, 5 Oct 2023 17:20:47 -0400 Subject: [PATCH 3/6] Avoid circular dependency --- airflow/www/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airflow/www/auth.py b/airflow/www/auth.py index ef63d93a9fa36..99b555139b674 100644 --- a/airflow/www/auth.py +++ b/airflow/www/auth.py @@ -23,7 +23,6 @@ from flask import flash, g, redirect, render_template, request -from airflow.auth.managers.fab.decorators.auth import _has_access_fab from airflow.auth.managers.models.resource_details import ( ConnectionDetails, DagAccessEntity, @@ -63,6 +62,8 @@ def has_access(permissions: Sequence[tuple[str, str]] | None = None) -> Callable RemovedInAirflow3Warning, stacklevel=2, ) + from airflow.auth.managers.fab.decorators.auth import _has_access_fab + return _has_access_fab(permissions) From 942315e26bbf883348e119adf146b8367865349c Mon Sep 17 00:00:00 2001 From: Vincent Beck Date: Fri, 6 Oct 2023 09:37:13 -0400 Subject: [PATCH 4/6] Add __init__ file --- .../auth/managers/fab/decorators/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 airflow/auth/managers/fab/decorators/__init__.py diff --git a/airflow/auth/managers/fab/decorators/__init__.py b/airflow/auth/managers/fab/decorators/__init__.py new file mode 100644 index 0000000000000..217e5db960782 --- /dev/null +++ b/airflow/auth/managers/fab/decorators/__init__.py @@ -0,0 +1,17 @@ +# +# 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 30cd18e9d83118f376b844dc72385b6203e17d57 Mon Sep 17 00:00:00 2001 From: Vincent <97131062+vincbeck@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:50:32 -0400 Subject: [PATCH 5/6] Update airflow/www/auth.py Co-authored-by: Jens Scheffler <95105677+jens-scheffler-bosch@users.noreply.github.com> --- airflow/www/auth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/airflow/www/auth.py b/airflow/www/auth.py index 99b555139b674..e3fd7be2ad6d0 100644 --- a/airflow/www/auth.py +++ b/airflow/www/auth.py @@ -52,6 +52,7 @@ def has_access(permissions: Sequence[tuple[str, str]] | None = None) -> Callable Deprecated. Do not use this decorator, use one of the decorator `has_access_cluster_*` defined in airflow/www/auth.py instead. + This decorator will only work with FAB authentication and not with other auth providers. This decorator is widely used in user plugins, do not remove it. See https://github.com/apache/airflow/pull/33213#discussion_r1346287224 From a8832e80108aa6122cc29fdbb6ff53a807459628 Mon Sep 17 00:00:00 2001 From: Vincent Beck Date: Tue, 10 Oct 2023 12:53:37 -0400 Subject: [PATCH 6/6] Fix typo --- airflow/www/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airflow/www/auth.py b/airflow/www/auth.py index e3fd7be2ad6d0..93ee8196bd575 100644 --- a/airflow/www/auth.py +++ b/airflow/www/auth.py @@ -50,7 +50,7 @@ def has_access(permissions: Sequence[tuple[str, str]] | None = None) -> Callable """ Factory for decorator that checks current user's permissions against required permissions. - Deprecated. Do not use this decorator, use one of the decorator `has_access_cluster_*` defined in + Deprecated. Do not use this decorator, use one of the decorator `has_access_*` defined in airflow/www/auth.py instead. This decorator will only work with FAB authentication and not with other auth providers. @@ -58,7 +58,7 @@ def has_access(permissions: Sequence[tuple[str, str]] | None = None) -> Callable https://github.com/apache/airflow/pull/33213#discussion_r1346287224 """ warnings.warn( - "The 'has_access' decorator is deprecated. Please use one of the decorator `has_access_cluster_*`" + "The 'has_access' decorator is deprecated. Please use one of the decorator `has_access_*`" "defined in airflow/www/auth.py instead.", RemovedInAirflow3Warning, stacklevel=2,