Gate audit log rows not tied to a Dag on a dedicated AccessView - #70759
Merged
potiuk merged 3 commits intoAug 1, 2026
Merged
Conversation
`PermittedEventLogFilter` returned every `dag_id IS NULL` row unconditionally, and the detail endpoint's guard resolved such a row to `dag_id=None` and then authorized it as Dag-level `DagAccessEntity.AUDIT_LOG` access. Those rows record Connection, Variable and Pool operations, so they carry no per-Dag key to authorize on -- and Dag-level audit access is held by any viewer under the default auth manager, which is not the audience for them. Add `AccessView.AUDIT_LOGS_ALL` and gate both read paths on it, mirroring the `IMPORT_ERRORS_ALL` view that covers the same shape of problem for import errors of files with no registered Dag. The list endpoint filters the rows out in the query, so they stay out of `total_entries` and pagination too; the detail endpoint answers 403. `SimpleAuthManager` grants the view to admins only, and the FAB auth manager maps it to a new `All Audit Logs` resource that lands in `ADMIN_PERMISSIONS`. The detail guard now selects `Log.id` alongside `Log.dag_id`: a missing row and a NULL `dag_id` both read back as `None` from a bare scalar, and they authorize differently. A missing row keeps the Dag-level check so the route can still answer 404 rather than turning an unknown id into a permission error. `authorize_view` also stops propagating a `KeyError` from an auth manager that cannot map an `AccessView`. `AccessView` members are added by core, but auth managers ship as separately released providers, so a core newer than the installed manager can name a view the manager has never heard of -- the FAB manager translates the enum through a lookup table and would have raised, which surfaces as a 500 on the endpoint. It now denies and warns instead, which keeps the endpoint working and fails closed. 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,
shubhamraj-git and
vincbeck
as code owners
July 30, 2026 11:58
The guard now selects Log.id alongside Log.dag_id so a missing row and a NULL dag_id can be told apart. These tests mocked session.scalar, so with a Mock session the new call returned a truthy Mock whose dag_id was itself a Mock. Mock the execute(...).one_or_none() shape instead, and add the two cases the unit tests did not cover: a row that exists with a NULL dag_id authorizes on AUDIT_LOGS_ALL rather than the Dag check, and is Forbidden without it.
vincbeck
approved these changes
Jul 30, 2026
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.
Audit log rows whose
dag_idisNULLrecord operations that are not tied to aDag — Connection, Variable and Pool changes. Both read paths treated them as
Dag-level audit data:
PermittedEventLogFilter.to_ormappendedor_(…, Log.dag_id.is_(None))unconditionally, so every caller that could read event logs at all received them;
requires_access_event_logresolved such a row todag_id=Noneand passed it torequires_access_dag(…, DagAccessEntity.AUDIT_LOG, None), an unscoped Dag check.There is no per-Dag key to authorize these rows on, and Dag-level audit access is
held by any viewer under the default auth manager — not the audience for a
Connection or Variable change record.
Approach
This is the same shape of problem
AccessView.IMPORT_ERRORS_ALLalready solves forimport errors of files with no registered Dag, so it gets the same treatment: a
dedicated, admin-by-default view.
AccessView.AUDIT_LOGS_ALLis added and both read paths consult ittotal_entriesand pagination too and their existence does not leak; the detailendpoint answers 403
SimpleAuthManagergrants it to admins only, alongsideIMPORT_ERRORS_ALLAll Audit Logsresource, added toADMIN_PERMISSIONS, reached through thecommon.compatshim so the providerstill imports on a core that predates the view
Rows bound to a Dag are unaffected and keep their existing per-Dag check.
The detail guard now selects
Log.idalongsideLog.dag_id. A missing row anda NULL
dag_idboth read back asNonefrom a bare scalar, and they authorizedifferently. Telling them apart lets a missing row keep the Dag-level check, so the
route still answers 404 rather than turning an unknown id into a permission error.
authorize_viewno longer propagates aKeyErrorfrom an auth manager thatcannot map an
AccessView.AccessViewmembers are added by core, but authmanagers ship as separately released providers, so a core newer than the installed
manager can name a view the manager has never heard of. The FAB manager translates
the enum through a lookup table (
_MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[access_view])and would raise, surfacing as a 500 on whichever endpoint consults the view — which
is how any future
AccessViewaddition would land on an older FAB. It now deniesand warns instead: the endpoint keeps working, and it fails closed. This also covers
IMPORT_ERRORS_ALL, which has the same exposure today.Behaviour changes
GET /eventLogsno longer returns rows with aNULLdag_idto callers withoutAUDIT_LOGS_ALL, and those rows are excluded fromtotal_entries. UnderSimpleAuthManagerthat means admins only; under FAB, holders ofAll Audit Logs.can_read, which ships in the Admin role.GET /eventLogs/{event_log_id}answers 403 for such a row to the same callers.An id matching no row still answers 404.
AccessViewnow denies the view witha
UserWarninginstead of raisingKeyError(previously a 500).All Audit Logs; existing custom roles do not gain itautomatically and need it granted explicitly to keep reading these rows.
Two incidental fixes in the touched code:
ReadableEventLogsFilterDepwas annotatedPermittedTIFilterrather thanPermittedEventLogFilter, and the two/eventLogsrows in the FAB access-control table listed a minimum role of Viewer although
Audit Logs.can_readhas only ever been inADMIN_PERMISSIONS.Test plan
test_event_logs.py— the list endpoint returns only Dag-bound rows without theview and all four rows with it (asserting
total_entries, not just the payload);the detail endpoint 403s/200s on a non-Dag row; an unknown id still 404s and does
not consult the view
security.py; the unknown-id case passeseither way and is a regression guard for the 404-vs-403 distinction
test_simple_auth_manager.py—AUDIT_LOGS_ALLandIMPORT_ERRORS_ALLauthorizefor ADMIN and deny OP/USER/VIEWER, while every other view stays viewer-readable
test_base_auth_manager.py— a lookup-table manager denies an unmapped view with awarning and still answers a mapped one
test_access_view.py— the compat shim resolves both members, and both toNoneona core without them
test_event_logs.py37 passed / 3 skipped (backend-gated, pre-existing),test_base_auth_manager.py+test_simple_auth_manager.py252 passed,test_import_error.py+ compat 49 passed,test_fab_auth_manager.py127 passedruff checkandruff formatclean on every changed fileWas generative AI tooling used to co-author this PR?
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