Skip to content

Scope asset API responses to the assets a user may read - #72682

Merged
vincbeck merged 6 commits into
apache:mainfrom
henry3260:add-asset-authorization-filtering
Sep 9, 2026
Merged

vincbeck merged 6 commits into
apache:mainfrom
henry3260:add-asset-authorization-filtering

Conversation

@henry3260

@henry3260 henry3260 commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Why

GET /api/v2/assets, GET /ui/assets and GET /api/v2/assets/events run one coarse is_authorized_asset check and then return every row. In a multi-team deployment every caller can therefore see every other team's asset names and URIs, which commonly encode bucket, table or dataset names. Dags, connections, pools and variables already scope their list endpoints to what the caller may read; assets had no equivalent.

Auth managers also had no way to express such a rule: AssetDetails only carried the numeric id, so a manager that wants to authorize by URI prefix or name had nothing to decide on.

What

Core:

  • AssetDetails gains name and uri.
  • BaseAuthManager gains get_authorized_assets and filter_authorized_assets, following the existing connection pattern. The default calls is_authorized_asset per asset, so a manager that ignores asset details keeps today's behaviour.
  • security.py adds PermittedAssetFilter, PermittedAssetEventByAssetFilter and permitted_asset_filter_factory. The two asset list endpoints and the asset events endpoint are scoped with them, so both the rows and total_entries reflect only readable assets. Events whose asset row no longer exists stay visible, since there is no name or uri left to authorize on.
  • requires_access_asset resolves the asset's name and uri (via a new AssetModel.get_name_and_uri) before calling is_authorized_asset, so single-asset routes can be authorized by URI or name too.
  • The auth manager docs list filter_authorized_assets among the methods recommended to override.

Providers, so the per-asset default does not turn one listing into one remote call per asset:

  • AwsAuthManager.filter_authorized_assets sends a single batched AVP request.
  • KeycloakAuthManager.filter_authorized_assets parallelises the checks over its request pool behind single_flight, keyed on user, method and asset ids.
  • FabAuthManager.get_authorized_assets returns every asset id from one query, since FAB grants asset access at the resource level.

Tests cover the new auth manager methods, the filters and factory, the enriched requires_access_asset, the three endpoints, and the provider overrides. Provider tests that build AssetDetails with name/uri are gated on Airflow 3.4+. Query-count assertions on the touched endpoints go up by one.

closes: #72333


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Fable 5.1)

@boring-cyborg boring-cyborg Bot added area:API Airflow's REST/HTTP API kind:documentation labels Sep 8, 2026
@henry3260
henry3260 force-pushed the add-asset-authorization-filtering branch 5 times, most recently from a40b290 to 3d4e0b6 Compare September 8, 2026 12:23
Multi-team deployments isolate Dags, connections, variables and pools per
team, but any caller could still list every asset. Asset names and URIs
commonly encode bucket, table or dataset names, so a team could read the
whole data catalog of every other team. Auth managers also had no way to
express such a rule: asset authorization only ever received the numeric
id, never the name or uri the rule needs to decide on.
Unspecced mocks accept any attribute and any call signature, so a rename
or signature change in the auth manager leaves these tests passing while
the code under test is broken. The surrounding tests already spec their
mocks; these new ones were the outliers.
The 403 path belongs to _requires_access, which predates this change and
is already covered elsewhere. Keeping a copy of it here spends CI time on
logic this branch does not touch, and would keep passing if every line
this branch adds were reverted.
Scoping the asset endpoints makes the auth manager decide on every asset
rather than once per request. Both of these managers answer each decision
with a remote call, so inheriting the per-asset default would turn a single
asset listing into one round trip per asset. They already batch the same way
for connections, pools and variables.
FAB grants asset access at the resource level, so its is_authorized_asset
ignores the asset it is handed and returns the same answer every time.
Inheriting the per-asset default would spend a permission-list scan per
asset to recompute one boolean. It already short-circuits the same way for
connections, pools and variables.
The provider tests build AssetDetails with name and uri, which only exist
from Airflow 3.4, so they broke the older versions amazon and keycloak
still support. Two others proved less than they claimed: the AWS one stubbed
out the response matching it was meant to exercise, and the Keycloak cache
key carried a resource prefix whose stated reason -- collision with another
resource's entry -- cannot happen, since every sibling key is a longer tuple.
@henry3260

Copy link
Copy Markdown
Contributor Author

This scopes the asset collection endpoints to the assets the caller may read,
and gives auth managers the name and uri they need to make that decision:

  • GET /assets and GET /ui/assets — filtered rows and total_entries
  • GET /assets/events — filtered by the event's asset
  • every /assets/{asset_id}/... route — requires_access_asset now resolves
    the asset's name and uri, so an auth manager can deny per asset rather than seeing an opaque id

It does not extend to endpoints that return an asset inside a response which is not an asset collection. Those still authorize on the coarse requires_access_asset check alone:

Endpoint What it would need
POST /assets/events the asset id read from the request body — requires_access_asset reads path parameters, so it authorizes on id=None
GET /dags/{dag_id}/dagRuns/{dag_run_id}/upstreamAssetEvents the event filter, applied where the run's events are serialized
GET /ui/next_run_assets/{dag_id} an asset filter alongside the existing Dag check
GET /ui/partitioned_dag_runs, GET /ui/pending_partitioned_dag_run/{dag_id} same
GET /ui/dependencies?dependency_type=data asset-level gating; today it hides the graph unless the caller can read a connected Dag
GET /assets/aliases a decision on whether alias names fall under asset authorization

Each needs its own call on where the filter belongs, and I would rather settle the shared filter and the auth manager interface first. Happy to fold any of them in if reviewers would prefer this land as one change.

@vincbeck vincbeck left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assets are global to the environment. They do not belong to any team.

@henry3260

Copy link
Copy Markdown
Contributor Author

Assets are global to the environment. They do not belong to any team.

Thanks for taking a look, and that's a fair point you're right that assets are global and don't belong to any team. I should clarify that this PR doesn't try to change that; it doesn't introduce any team-to-asset ownership.

What it's really after is authorization granularity. Right now the asset list,events endpoints run a single coarse is_authorized_asset check and then return every row, and the auth manager only ever receives an opaque numeric id. This PR simply passes the asset's name and uri along, so an auth manager can decide readability per asset (say, by URI prefix) if it chooses to. Whether a deployment actually restricts anything stays entirely in the auth manager's hands the default still calls is_authorized_asset per asset, so nothing changes for managers that don't care about asset identity.

My thinking was to mirror what connections, pools and variables already do: those are global too, yet their list endpoints are still scoped to what the caller may read, and assets were the one exception with no way to express that.

That said, I may well be missing context here would you prefer that we not scope global assets at all, or is it more about how the filtering is done? Happy to adjust either way.

@vincbeck

vincbeck commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Assets are global to the environment. They do not belong to any team.

Thanks for taking a look, and that's a fair point you're right that assets are global and don't belong to any team. I should clarify that this PR doesn't try to change that; it doesn't introduce any team-to-asset ownership.

What it's really after is authorization granularity. Right now the asset list,events endpoints run a single coarse is_authorized_asset check and then return every row, and the auth manager only ever receives an opaque numeric id. This PR simply passes the asset's name and uri along, so an auth manager can decide readability per asset (say, by URI prefix) if it chooses to. Whether a deployment actually restricts anything stays entirely in the auth manager's hands the default still calls is_authorized_asset per asset, so nothing changes for managers that don't care about asset identity.

My thinking was to mirror what connections, pools and variables already do: those are global too, yet their list endpoints are still scoped to what the caller may read, and assets were the one exception with no way to express that.

That said, I may well be missing context here would you prefer that we not scope global assets at all, or is it more about how the filtering is done? Happy to adjust either way.

Oh I see now! Alright, let me take a look :)

@vincbeck vincbeck left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid work!

@henry3260

Copy link
Copy Markdown
Contributor Author

Should this go into 3.4 or a 3.3.x patch release?

@vincbeck

vincbeck commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Definitely 3.4

@henry3260 henry3260 added this to the Airflow 3.4.0 milestone Sep 9, 2026
@vincbeck
vincbeck merged commit 0f5a23a into apache:main Sep 9, 2026
170 checks passed
imrichardwu pushed a commit to imrichardwu/airflow that referenced this pull request Sep 11, 2026
Multi-team deployments isolate Dags, connections, variables and pools per
team, but any caller could still list every asset. Asset names and URIs
commonly encode bucket, table or dataset names, so a team could read the
whole data catalog of every other team. Auth managers also had no way to
express such a rule: asset authorization only ever received the numeric
id, never the name or uri the rule needs to decide on.
xvega pushed a commit to xvega/airflow that referenced this pull request Sep 13, 2026
Multi-team deployments isolate Dags, connections, variables and pools per
team, but any caller could still list every asset. Asset names and URIs
commonly encode bucket, table or dataset names, so a team could read the
whole data catalog of every other team. Auth managers also had no way to
express such a rule: asset authorization only ever received the numeric
id, never the name or uri the rule needs to decide on.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:API Airflow's REST/HTTP API kind:documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Add query-level asset authorization filtering to BaseAuthManager and FastAPI /assets endpoint

2 participants