Skip to content

Commit 9b48498

Browse files
Deprecate BackfillDetails and use DagAcccessEntity.Run for backfill p… (#61400)
1 parent 5fef698 commit 9b48498

File tree

12 files changed

+285
-86
lines changed

12 files changed

+285
-86
lines changed

airflow-core/docs/core-concepts/auth-manager/index.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ These authorization methods are:
136136
Also, ``is_authorized_dag`` is called for any entity related to Dags (e.g. task instances, Dag runs, ...). This information is passed in ``access_entity``.
137137
Example: ``auth_manager.is_authorized_dag(method="GET", access_entity=DagAccessEntity.Run, details=DagDetails(id="dag-1"))`` asks
138138
whether the user has permission to read the Dag runs of the Dag "dag-1".
139-
* ``is_authorized_backfill``: Return whether the user is authorized to access Airflow backfills. Some details about the backfill can be provided (e.g. the backfill ID).
140139
* ``is_authorized_asset``: Return whether the user is authorized to access Airflow assets. Some details about the asset can be provided (e.g. the asset ID).
141140
* ``is_authorized_asset_alias``: Return whether the user is authorized to access Airflow asset aliases. Some details about the asset alias can be provided (e.g. the asset alias ID).
142141
* ``is_authorized_pool``: Return whether the user is authorized to access Airflow pools. Some details about the pool can be provided (e.g. the pool name).
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
AuthManager Backfill permissions are now handled by the ``requires_access_dag`` on the ``DagAccessEntity.Run``
2+
3+
``is_authorized_backfill`` of the ``BaseAuthManager`` interface has been removed. Core will no longer call this method and their
4+
provider counterpart implementation will be marked as deprecated.
5+
Permissions for backfill operations are now checked against the ``DagAccessEntity.Run`` permission using the existing
6+
``requires_access_dag`` decorator. In other words, if a user has permission to run a DAG, they can perform backfill operations on it.
7+
8+
Please update your security policies to ensure that users who need to perform backfill operations have the appropriate ``DagAccessEntity.Run`` permissions. (Users
9+
having the Backfill permissions without having the DagRun ones will no longer be able to perform backfill operations without any update)
10+
11+
* Types of change
12+
13+
* [ ] Dag changes
14+
* [ ] Config changes
15+
* [x] API changes
16+
* [ ] CLI changes
17+
* [x] Behaviour changes
18+
* [ ] Plugin changes
19+
* [ ] Dependency changes
20+
* [ ] Code interface changes

airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
3131
from airflow.api_fastapi.auth.managers.models.resource_details import (
32-
BackfillDetails,
3332
ConnectionDetails,
3433
DagDetails,
3534
PoolDetails,
@@ -232,22 +231,6 @@ def is_authorized_dag(
232231
:param details: optional details about the DAG
233232
"""
234233

235-
@abstractmethod
236-
def is_authorized_backfill(
237-
self,
238-
*,
239-
method: ResourceMethod,
240-
user: T,
241-
details: BackfillDetails | None = None,
242-
) -> bool:
243-
"""
244-
Return whether the user is authorized to perform a given action on a backfill.
245-
246-
:param method: the method to perform
247-
:param user: the user to performing the action
248-
:param details: optional details about the backfill
249-
"""
250-
251234
@abstractmethod
252235
def is_authorized_asset(
253236
self,

airflow-core/src/airflow/api_fastapi/auth/managers/models/resource_details.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ class DagDetails:
4848

4949
@dataclass
5050
class BackfillDetails:
51-
"""Represents the details of a backfill."""
51+
"""
52+
Represents the details of a backfill.
53+
54+
.. deprecated:: 3.1.8
55+
Use DagAccessEntity.Run instead for a dag level access control.
56+
"""
5257

5358
id: NonNegativeInt | None = None
5459

airflow-core/src/airflow/api_fastapi/auth/managers/simple/simple_auth_manager.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
3838
from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
39-
from airflow.api_fastapi.auth.managers.models.resource_details import BackfillDetails, TeamDetails
39+
from airflow.api_fastapi.auth.managers.models.resource_details import TeamDetails
4040
from airflow.api_fastapi.auth.managers.simple.user import SimpleAuthManagerUser
4141
from airflow.api_fastapi.common.types import MenuItem
4242
from airflow.configuration import AIRFLOW_HOME, conf
@@ -194,20 +194,6 @@ def is_authorized_dag(
194194
user=user,
195195
)
196196

197-
def is_authorized_backfill(
198-
self,
199-
*,
200-
method: ResourceMethod,
201-
user: SimpleAuthManagerUser,
202-
details: BackfillDetails | None = None,
203-
) -> bool:
204-
return self._is_authorized(
205-
method=method,
206-
allow_get_role=SimpleAuthManagerRole.VIEWER,
207-
allow_role=SimpleAuthManagerRole.OP,
208-
user=user,
209-
)
210-
211197
def is_authorized_asset(
212198
self,
213199
*,

airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from sqlalchemy.orm import joinedload
2626

2727
from airflow._shared.timezones import timezone
28-
from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity
2928
from airflow.api_fastapi.common.db.common import (
3029
SessionDep,
3130
paginated_select,
@@ -42,7 +41,7 @@
4241
from airflow.api_fastapi.core_api.openapi.exceptions import (
4342
create_openapi_http_exception_doc,
4443
)
45-
from airflow.api_fastapi.core_api.security import GetUserDep, requires_access_backfill, requires_access_dag
44+
from airflow.api_fastapi.core_api.security import GetUserDep, requires_access_backfill
4645
from airflow.api_fastapi.logging.decorators import action_logging
4746
from airflow.exceptions import DagNotFound
4847
from airflow.models import DagRun
@@ -121,7 +120,6 @@ def get_backfill(
121120
dependencies=[
122121
Depends(action_logging()),
123122
Depends(requires_access_backfill(method="PUT")),
124-
Depends(requires_access_dag(method="PUT", access_entity=DagAccessEntity.RUN)),
125123
],
126124
)
127125
def pause_backfill(backfill_id: NonNegativeInt, session: SessionDep) -> BackfillResponse:
@@ -149,7 +147,6 @@ def pause_backfill(backfill_id: NonNegativeInt, session: SessionDep) -> Backfill
149147
dependencies=[
150148
Depends(action_logging()),
151149
Depends(requires_access_backfill(method="PUT")),
152-
Depends(requires_access_dag(method="PUT", access_entity=DagAccessEntity.RUN)),
153150
],
154151
)
155152
def unpause_backfill(backfill_id: NonNegativeInt, session: SessionDep) -> BackfillResponse:
@@ -175,7 +172,6 @@ def unpause_backfill(backfill_id: NonNegativeInt, session: SessionDep) -> Backfi
175172
),
176173
dependencies=[
177174
Depends(action_logging()),
178-
Depends(requires_access_dag(method="PUT", access_entity=DagAccessEntity.RUN)),
179175
Depends(requires_access_backfill(method="PUT")),
180176
],
181177
)
@@ -222,7 +218,6 @@ def cancel_backfill(backfill_id: NonNegativeInt, session: SessionDep) -> Backfil
222218
responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND, status.HTTP_409_CONFLICT]),
223219
dependencies=[
224220
Depends(action_logging()),
225-
Depends(requires_access_dag(method="POST", access_entity=DagAccessEntity.RUN)),
226221
Depends(requires_access_backfill(method="POST")),
227222
],
228223
)
@@ -270,7 +265,6 @@ def create_backfill(
270265
path="/dry_run",
271266
responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND, status.HTTP_409_CONFLICT]),
272267
dependencies=[
273-
Depends(requires_access_dag(method="POST", access_entity=DagAccessEntity.RUN)),
274268
Depends(requires_access_backfill(method="POST")),
275269
],
276270
)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from airflow.api_fastapi.core_api.openapi.exceptions import (
3737
create_openapi_http_exception_doc,
3838
)
39-
from airflow.api_fastapi.core_api.security import requires_access_backfill, requires_access_dag
39+
from airflow.api_fastapi.core_api.security import requires_access_backfill
4040
from airflow.models.backfill import Backfill
4141

4242
backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills")
@@ -47,7 +47,6 @@
4747
responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
4848
dependencies=[
4949
Depends(requires_access_backfill(method="GET")),
50-
Depends(requires_access_dag(method="GET")),
5150
],
5251
)
5352
def list_backfills_ui(

airflow-core/src/airflow/api_fastapi/core_api/security.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616
# under the License.
1717
from __future__ import annotations
1818

19-
from collections.abc import Callable
19+
from collections.abc import Callable, Coroutine
20+
from json import JSONDecodeError
2021
from pathlib import Path
21-
from typing import TYPE_CHECKING, Annotated, cast
22+
from typing import TYPE_CHECKING, Annotated, Any, cast
2223
from urllib.parse import ParseResult, unquote, urljoin, urlparse
2324

2425
from fastapi import Depends, HTTPException, Request, status
2526
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer, OAuth2PasswordBearer
2627
from jwt import ExpiredSignatureError, InvalidTokenError
27-
from pydantic import NonNegativeInt
28-
from sqlalchemy import or_
28+
from sqlalchemy import or_, select
29+
from sqlalchemy.orm import Session
2930

3031
from airflow.api_fastapi.app import get_auth_manager
3132
from airflow.api_fastapi.auth.managers.base_auth_manager import (
@@ -42,14 +43,14 @@
4243
AccessView,
4344
AssetAliasDetails,
4445
AssetDetails,
45-
BackfillDetails,
4646
ConfigurationDetails,
4747
ConnectionDetails,
4848
DagAccessEntity,
4949
DagDetails,
5050
PoolDetails,
5151
VariableDetails,
5252
)
53+
from airflow.api_fastapi.common.db.common import SessionDep
5354
from airflow.api_fastapi.core_api.base import OrmClause
5455
from airflow.api_fastapi.core_api.datamodels.common import (
5556
BulkAction,
@@ -64,6 +65,7 @@
6465
from airflow.api_fastapi.core_api.datamodels.variables import VariableBody
6566
from airflow.configuration import conf
6667
from airflow.models import Connection, Pool, Variable
68+
from airflow.models.backfill import Backfill
6769
from airflow.models.dag import DagModel, DagRun, DagTag
6870
from airflow.models.dagwarning import DagWarning
6971
from airflow.models.log import Log
@@ -146,14 +148,21 @@ async def get_user(
146148

147149

148150
def requires_access_dag(
149-
method: ResourceMethod, access_entity: DagAccessEntity | None = None
151+
method: ResourceMethod,
152+
access_entity: DagAccessEntity | None = None,
153+
param_dag_id: str | None = None,
150154
) -> Callable[[Request, BaseUser], None]:
151155
def inner(
152156
request: Request,
153157
user: GetUserDep,
154158
) -> None:
155-
dag_id = request.path_params.get("dag_id") or request.query_params.get("dag_id")
156-
dag_id = dag_id if dag_id != "~" else None
159+
# Required for the closure to capture the dag_id but still be able to mutate it.
160+
# Prevent from using a nonlocal statement causing test failures.
161+
dag_id = param_dag_id
162+
if dag_id is None:
163+
dag_id = request.path_params.get("dag_id") or request.query_params.get("dag_id")
164+
dag_id = dag_id if dag_id != "~" else None
165+
157166
team_name = DagModel.get_team_name(dag_id) if dag_id else None
158167

159168
_requires_access(
@@ -263,17 +272,35 @@ def depends_permitted_dags_filter(
263272
]
264273

265274

266-
def requires_access_backfill(method: ResourceMethod) -> Callable[[Request, BaseUser], None]:
267-
def inner(
275+
def requires_access_backfill(
276+
method: ResourceMethod,
277+
) -> Callable[[Request, BaseUser, Session], Coroutine[Any, Any, None]]:
278+
"""Wrap ``requires_access_dag`` and extract the dag_id from the backfill_id."""
279+
280+
async def inner(
268281
request: Request,
269282
user: GetUserDep,
283+
session: SessionDep,
270284
) -> None:
271-
backfill_id: NonNegativeInt | None = request.path_params.get("backfill_id")
272-
273-
_requires_access(
274-
is_authorized_callback=lambda: get_auth_manager().is_authorized_backfill(
275-
method=method, details=BackfillDetails(id=backfill_id), user=user
276-
),
285+
dag_id = None
286+
287+
# Try to retrieve the dag_id from the backfill_id path param
288+
backfill_id = request.path_params.get("backfill_id")
289+
if backfill_id is not None and isinstance(backfill_id, int):
290+
backfill = session.scalars(select(Backfill).where(Backfill.id == backfill_id)).one_or_none()
291+
dag_id = backfill.dag_id if backfill else None
292+
293+
# Try to retrieve the dag_id from the request body (POST backfill)
294+
if dag_id is None:
295+
try:
296+
dag_id = (await request.json()).get("dag_id")
297+
except JSONDecodeError:
298+
# Not a json body, ignore
299+
pass
300+
301+
requires_access_dag(method, DagAccessEntity.RUN, dag_id)(
302+
request,
303+
user,
277304
)
278305

279306
return inner

airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_simple_auth_manager.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ def test_serialize_user(self, auth_manager):
135135
"is_authorized_dag",
136136
"is_authorized_asset",
137137
"is_authorized_asset_alias",
138-
"is_authorized_backfill",
139138
"is_authorized_pool",
140139
"is_authorized_variable",
141140
],
@@ -191,7 +190,6 @@ def test_is_authorized_view_methods(self, auth_manager, api, kwargs, role, resul
191190
"is_authorized_connection",
192191
"is_authorized_asset",
193192
"is_authorized_asset_alias",
194-
"is_authorized_backfill",
195193
"is_authorized_pool",
196194
"is_authorized_variable",
197195
],
@@ -237,7 +235,6 @@ def test_is_authorized_methods_user_role_required(self, auth_manager, api, role,
237235
"is_authorized_dag",
238236
"is_authorized_asset",
239237
"is_authorized_asset_alias",
240-
"is_authorized_backfill",
241238
"is_authorized_pool",
242239
],
243240
)

airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager, T
2626
from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
2727
from airflow.api_fastapi.auth.managers.models.resource_details import (
28-
BackfillDetails,
2928
ConnectionDetails,
3029
DagDetails,
3130
PoolDetails,
@@ -92,15 +91,6 @@ def is_authorized_dag(
9291
) -> bool:
9392
raise NotImplementedError()
9493

95-
def is_authorized_backfill(
96-
self,
97-
*,
98-
method: ResourceMethod,
99-
details: BackfillDetails | None = None,
100-
user: BaseAuthManagerUserTest | None = None,
101-
) -> bool:
102-
raise NotImplementedError()
103-
10494
def is_authorized_asset(
10595
self,
10696
*,

0 commit comments

Comments
 (0)