This is an automated email from the ASF dual-hosted git repository.
weilee 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 5f004b112f4 feat(AIP-84): add auth to /ui/backfills (#47657)
5f004b112f4 is described below
commit 5f004b112f4a4ea2000026762d6641084aa85b3e
Author: Wei Lee <[email protected]>
AuthorDate: Fri Mar 14 08:28:19 2025 +0800
feat(AIP-84): add auth to /ui/backfills (#47657)
* add auth to backfills endpoints
* feat(security): add is_authorized_backfill
* feat(api_fastapi): add required permission for backfills
* feat(AIP-84): add auth to /ui/backfills
---------
Co-authored-by: vatsrahul1001 <[email protected]>
---
.../api_fastapi/core_api/openapi/v1-generated.yaml | 2 +
.../core_api/routes/public/backfills.py | 10 ++++-
.../api_fastapi/core_api/routes/ui/backfills.py | 5 +++
.../providers/fab/auth_manager/fab_auth_manager.py | 45 +++++++++++++++++-----
.../fab/auth_manager/security_manager/override.py | 3 +-
.../core_api/routes/ui/test_backfills.py | 10 ++++-
6 files changed, 61 insertions(+), 14 deletions(-)
diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index 56e4f119e40..a437510b453 100644
--- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -373,6 +373,8 @@ paths:
- Backfill
summary: List Backfills
operationId: list_backfills
+ security:
+ - OAuth2PasswordBearer: []
parameters:
- name: limit
in: query
diff --git a/airflow/api_fastapi/core_api/routes/public/backfills.py
b/airflow/api_fastapi/core_api/routes/public/backfills.py
index 31638e55503..ea882f9dc8b 100644
--- a/airflow/api_fastapi/core_api/routes/public/backfills.py
+++ b/airflow/api_fastapi/core_api/routes/public/backfills.py
@@ -116,7 +116,10 @@ def get_backfill(
status.HTTP_409_CONFLICT,
]
),
- dependencies=[Depends(requires_access_backfill(method="PUT"))],
+ dependencies=[
+ Depends(requires_access_backfill(method="PUT")),
+ Depends(requires_access_dag(method="PUT",
access_entity=DagAccessEntity.RUN)),
+ ],
)
def pause_backfill(backfill_id, session: SessionDep) -> BackfillResponse:
b = session.get(Backfill, backfill_id)
@@ -138,7 +141,10 @@ def pause_backfill(backfill_id, session: SessionDep) ->
BackfillResponse:
status.HTTP_409_CONFLICT,
]
),
- dependencies=[Depends(requires_access_backfill(method="PUT"))],
+ dependencies=[
+ Depends(requires_access_backfill(method="PUT")),
+ Depends(requires_access_dag(method="PUT",
access_entity=DagAccessEntity.RUN)),
+ ],
)
def unpause_backfill(backfill_id, session: SessionDep) -> BackfillResponse:
b = session.get(Backfill, backfill_id)
diff --git a/airflow/api_fastapi/core_api/routes/ui/backfills.py
b/airflow/api_fastapi/core_api/routes/ui/backfills.py
index a749cdd6cfc..add5c536e76 100644
--- a/airflow/api_fastapi/core_api/routes/ui/backfills.py
+++ b/airflow/api_fastapi/core_api/routes/ui/backfills.py
@@ -35,6 +35,7 @@ from airflow.api_fastapi.core_api.datamodels.backfills import
BackfillCollection
from airflow.api_fastapi.core_api.openapi.exceptions import (
create_openapi_http_exception_doc,
)
+from airflow.api_fastapi.core_api.security import requires_access_backfill,
requires_access_dag
from airflow.models.backfill import Backfill
backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills")
@@ -43,6 +44,10 @@ backfills_router = AirflowRouter(tags=["Backfill"],
prefix="/backfills")
@backfills_router.get(
path="",
responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
+ dependencies=[
+ Depends(requires_access_backfill(method="GET")),
+ Depends(requires_access_dag(method="GET")),
+ ],
)
def list_backfills(
limit: QueryLimit,
diff --git
a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
index 9238aecf5af..331c92f8a5f 100644
--- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
+++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
@@ -62,7 +62,10 @@ from airflow.providers.fab.auth_manager.models import
Permission, Role, User
from airflow.providers.fab.auth_manager.models.anonymous_user import
AnonymousUser
from airflow.providers.fab.www.app import create_app
from airflow.providers.fab.www.constants import SWAGGER_BUNDLE, SWAGGER_ENABLED
-from airflow.providers.fab.www.extensions.init_views import
_CustomErrorRequestBodyValidator, _LazyResolver
+from airflow.providers.fab.www.extensions.init_views import (
+ _CustomErrorRequestBodyValidator,
+ _LazyResolver,
+)
from airflow.providers.fab.www.security import permissions
from airflow.providers.fab.www.security.permissions import (
RESOURCE_AUDIT_LOG,
@@ -89,7 +92,10 @@ from airflow.providers.fab.www.security.permissions import (
RESOURCE_WEBSITE,
RESOURCE_XCOM,
)
-from airflow.providers.fab.www.utils import get_fab_action_from_method_map,
get_method_from_fab_action_map
+from airflow.providers.fab.www.utils import (
+ get_fab_action_from_method_map,
+ get_method_from_fab_action_map,
+)
from airflow.security.permissions import RESOURCE_BACKFILL
from airflow.utils.session import NEW_SESSION, create_session, provide_session
from airflow.utils.yaml import safe_load
@@ -100,7 +106,9 @@ if TYPE_CHECKING:
CLICommand,
)
from airflow.providers.common.compat.assets import AssetAliasDetails,
AssetDetails
- from airflow.providers.fab.auth_manager.security_manager.override import
FabAirflowSecurityManagerOverride
+ from airflow.providers.fab.auth_manager.security_manager.override import (
+ FabAirflowSecurityManagerOverride,
+ )
from airflow.providers.fab.www.extensions.init_appbuilder import
AirflowAppBuilder
from airflow.providers.fab.www.security.permissions import (
RESOURCE_ASSET,
@@ -200,7 +208,9 @@ class FabAuthManager(BaseAuthManager[User]):
def get_fastapi_app(self) -> FastAPI | None:
"""Get the FastAPI app."""
- from airflow.providers.fab.auth_manager.api_fastapi.routes.login
import login_router
+ from airflow.providers.fab.auth_manager.api_fastapi.routes.login
import (
+ login_router,
+ )
flask_app = create_app(enable_plugins=False)
@@ -229,7 +239,10 @@ class FabAuthManager(BaseAuthManager[User]):
specification=specification,
resolver=_LazyResolver(),
base_path="/fab/v1",
- options={"swagger_ui": SWAGGER_ENABLED, "swagger_path":
SWAGGER_BUNDLE.__fspath__()},
+ options={
+ "swagger_ui": SWAGGER_ENABLED,
+ "swagger_path": SWAGGER_BUNDLE.__fspath__(),
+ },
strict_validation=True,
validate_responses=True,
validator_map={"body": _CustomErrorRequestBodyValidator},
@@ -336,7 +349,11 @@ class FabAuthManager(BaseAuthManager[User]):
)
def is_authorized_backfill(
- self, *, method: ResourceMethod, user: User, details: BackfillDetails
| None = None
+ self,
+ *,
+ method: ResourceMethod,
+ user: User,
+ details: BackfillDetails | None = None,
) -> bool:
return self._is_authorized(method=method,
resource_type=RESOURCE_BACKFILL, user=user)
@@ -346,7 +363,11 @@ class FabAuthManager(BaseAuthManager[User]):
return self._is_authorized(method=method,
resource_type=RESOURCE_ASSET, user=user)
def is_authorized_asset_alias(
- self, *, method: ResourceMethod, user: User, details:
AssetAliasDetails | None = None
+ self,
+ *,
+ method: ResourceMethod,
+ user: User,
+ details: AssetAliasDetails | None = None,
) -> bool:
return self._is_authorized(method=method,
resource_type=RESOURCE_ASSET_ALIAS, user=user)
@@ -356,7 +377,11 @@ class FabAuthManager(BaseAuthManager[User]):
return self._is_authorized(method=method, resource_type=RESOURCE_POOL,
user=user)
def is_authorized_variable(
- self, *, method: ResourceMethod, user: User, details: VariableDetails
| None = None
+ self,
+ *,
+ method: ResourceMethod,
+ user: User,
+ details: VariableDetails | None = None,
) -> bool:
return self._is_authorized(method=method,
resource_type=RESOURCE_VARIABLE, user=user)
@@ -364,7 +389,9 @@ class FabAuthManager(BaseAuthManager[User]):
# "Docs" are only links in the menu, there is no page associated
method: ResourceMethod = "MENU" if access_view == AccessView.DOCS else
"GET"
return self._is_authorized(
- method=method,
resource_type=_MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[access_view], user=user
+ method=method,
+ resource_type=_MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[access_view],
+ user=user,
)
def is_authorized_custom_view(
diff --git
a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
index bf8ca649900..37929d3cb24 100644
---
a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
+++
b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
@@ -106,18 +106,17 @@ from airflow.providers.fab.www.session import (
AirflowDatabaseSessionInterface,
AirflowDatabaseSessionInterface as FabAirflowDatabaseSessionInterface,
)
+from airflow.security.permissions import RESOURCE_BACKFILL
if TYPE_CHECKING:
from airflow.providers.fab.www.security.permissions import (
RESOURCE_ASSET,
RESOURCE_ASSET_ALIAS,
- RESOURCE_BACKFILL,
)
else:
from airflow.providers.common.compat.security.permissions import (
RESOURCE_ASSET,
RESOURCE_ASSET_ALIAS,
- RESOURCE_BACKFILL,
)
log = logging.getLogger(__name__)
diff --git a/tests/api_fastapi/core_api/routes/ui/test_backfills.py
b/tests/api_fastapi/core_api/routes/ui/test_backfills.py
index bf405b087a2..0c5e2a7cda7 100644
--- a/tests/api_fastapi/core_api/routes/ui/test_backfills.py
+++ b/tests/api_fastapi/core_api/routes/ui/test_backfills.py
@@ -87,7 +87,7 @@ class TestListBackfills(TestBackfillEndpoint):
({"dag_id": "TEST_DAG_1"}, ["backfill1"], 1),
],
)
- def test_list_backfill(self, test_params, response_params, total_entries,
test_client, session):
+ def test_should_response_200(self, test_params, response_params,
total_entries, test_client, session):
dags = self._create_dag_models()
from_date = timezone.utcnow()
to_date = timezone.utcnow()
@@ -150,3 +150,11 @@ class TestListBackfills(TestBackfillEndpoint):
"backfills": expected_response,
"total_entries": total_entries,
}
+
+ def test_should_response_401(self, unauthenticated_test_client):
+ response = unauthenticated_test_client.get("/ui/backfills", params={})
+ assert response.status_code == 401
+
+ def test_should_response_403(self, unauthorized_test_client):
+ response = unauthorized_test_client.get("/ui/backfills", params={})
+ assert response.status_code == 403