Scope /assets/events to the Dags the caller may read - #71741
Merged
Conversation
GET /api/v2/assets/events returned AssetEvent rows for every Dag. It is gated on requires_access_asset(method="GET"), which under the FAB auth manager checks the global "Assets" resource and does not consider which Dag produced the event, and the query applied no per-Dag row filter. A caller with read on a single Dag plus the global "Assets" resource could therefore read the source Dag, task and run identifiers, the created dag runs, and the task-authored "extra" payload of events belonging to every other Dag, and could target a specific one with ?source_dag_id=. The six sibling queued-events routes in the same file already apply ReadableDagsFilterDep; only this one did not. Add PermittedAssetEventFilter and apply it to the query. Events produced by a Dag's task are scoped to that Dag's readability. Events with no source Dag — created through the API, or emitted by a watcher — carry no per-Dag key to authorize on and stay visible to any caller who may read assets. The filter is applied inside paginated_select rather than after the fact, so total_entries and pagination are scoped too and the existence of hidden events does not leak either. Test fixtures that create events with a source_dag_id now register the corresponding Dag, since the scoping resolves against DagModel and the fixtures previously referenced Dags that did not exist. The query-count assertion moves from 4 to 5: resolving the caller's readable Dags costs one query, the same cost the queued-events routes already pay. Generated-by: Claude Opus 5 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
potiuk
requested review from
bugraoz93,
choo121600,
ephraimbuddy,
henry3260,
jason810496,
pierrejeambrun,
rawwar and
shubhamraj-git
as code owners
August 17, 2026 22:27
vincbeck
approved these changes
Aug 18, 2026
Contributor
|
Hi maintainer, this PR was merged without a milestone set.
|
Contributor
Backport successfully created: v3-3-testNote: As of Merging PRs targeted for Airflow 3.X In matter of doubt please ask in #release-management Slack channel.
|
potiuk
added a commit
that referenced
this pull request
Aug 18, 2026
) (#71785) GET /api/v2/assets/events returned AssetEvent rows for every Dag. It is gated on requires_access_asset(method="GET"), which under the FAB auth manager checks the global "Assets" resource and does not consider which Dag produced the event, and the query applied no per-Dag row filter. A caller with read on a single Dag plus the global "Assets" resource could therefore read the source Dag, task and run identifiers, the created dag runs, and the task-authored "extra" payload of events belonging to every other Dag, and could target a specific one with ?source_dag_id=. The six sibling queued-events routes in the same file already apply ReadableDagsFilterDep; only this one did not. Add PermittedAssetEventFilter and apply it to the query. Events produced by a Dag's task are scoped to that Dag's readability. Events with no source Dag — created through the API, or emitted by a watcher — carry no per-Dag key to authorize on and stay visible to any caller who may read assets. The filter is applied inside paginated_select rather than after the fact, so total_entries and pagination are scoped too and the existence of hidden events does not leak either. Test fixtures that create events with a source_dag_id now register the corresponding Dag, since the scoping resolves against DagModel and the fixtures previously referenced Dags that did not exist. The query-count assertion moves from 4 to 5: resolving the caller's readable Dags costs one query, the same cost the queued-events routes already pay. (cherry picked from commit f01520c) Generated-by: Claude Opus 5 (1M context) following the guidelines at https: //github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions Co-authored-by: Jarek Potiuk <jarek@potiuk.com>
Contributor
|
This one broke main. Fix: #71799 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GET /api/v2/assets/eventsreturnsAssetEventrows for every Dag. It is gated onrequires_access_asset(method="GET"), which under the FAB auth manager checks the globalAssetsresource and does not consider which Dag produced the event, and the query applies no per-Dag row filter.A caller with read on a single Dag plus the global
Assetsresource can therefore read the source Dag/task/run identifiers, the created dag runs, and the task-authoredextrapayload of events belonging to every other Dag — and can target a specific one with?source_dag_id=.The six sibling queued-events routes in the same file already apply
ReadableDagsFilterDep. Only this one does not.Change
Adds
PermittedAssetEventFilter, following the existingPermitted*Filterfamily, and applies it to the query.The filter goes into
paginated_selectrather than being applied after the fact, sototal_entriesand pagination are scoped too — otherwise the existence of hidden events still leaks through the count. That mirrors the reasoning already written intoPermittedEventLogFilter.The design question I'd most like reviewed
What should happen to events with no source Dag?
AssetEvent.source_dag_idis nullable — events created through the API, or emitted by a watcher, have no producing Dag. I've made those visible to any caller who may read assets, on the grounds that they carry no per-Dag key to authorize on:PermittedEventLogFilterfaces the same question for audit rows not tied to a Dag, and answers it differently: it gates them behind a separate permission (AccessView.AUDIT_LOGS_ALL) rather than showing them unconditionally. If you want symmetry with that, this should grow an equivalent gate.There is a second-order case I have not handled:
source_dag_idis denormalized with no FK, so events survive deletion of the Dag that produced them. Those rows have a non-nullsource_dag_idthat matches no readable Dag, so under this patch they become invisible to everyone, permanently. Alternatives are to treat "source Dag no longer exists" like the null case (preserves audit history, but leaks the existence of events from a Dag you could never read, once it is deleted) or to leave it as-is. I went with as-is because it is the conservative default, but it is a real behaviour change for anyone who deletes Dags and later reads asset events.Test fixture change worth a look
The existing fixtures created events with
source_dag_id="source_dag_id"(and"d","d1","d2") without ever creating the correspondingDagModelrows, so those Dags did not exist. Once events are scoped against readable Dags, that made 41 tests fail — not because the scoping was wrong, but because the fixtures did not represent a real deployment. They now register the Dags they attribute events to.I want to flag that explicitly rather than bury it: a large fixture change landing alongside a behaviour change is exactly the shape that hides a bug, so please sanity-check that the fixtures now describe something realistic rather than merely something green.
The query-count assertion moves 4 → 5. The extra query is
get_authorized_dag_ids, i.e. the authorization lookup itself — the same cost the queued-events routes already pay.Testing
TestGetAssetEventsPerDagScopingcovers the clause shape, the filter in isolation, and — the part that actually matters — an end-to-end test that mocksget_authorized_dag_idsand asserts the endpoint returns only permitted events, includingtotal_entries.I checked that these are not vacuous: unwiring the filter from the route makes the end-to-end cases fail while the isolated-filter cases still pass. An earlier version of these tests exercised only the filter class and passed even with the route unwired, which is why the end-to-end case exists.
186 tests in
test_assets.pypass, as do the 34 intest_dependencies.py(the other consumer of this filter family).Generated-by: Claude Opus 5 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions