apache / apache/airflow

Multi-Team: list endpoints return resources from all teams regardless of user's team

Open
#71,277 2 comments 0 reactions 0 assignees View on GitHub
area:API kind:bug needs-triage security
Dominant language
Python
Stars
46.9k
Forks
17.8k
Avg merge
2d 9h
Merged PRs (30d)
472

Description

### Under which category would you file this issue?

Providers

### Apache Airflow version

3.3.0

### What happened and how to reproduce it?

With `core.multi_team=True` and the Keycloak auth manager, `GET /api/v2/dags`, `GET /api/v2/variables` and `GET /api/v2/connections` return resources belonging to every team, regardless of the requesting user's team membership.

The docs state that Multi-Team is for organisations that "need resource isolation (Variables, Connections, Secrets, etc) between teams **at the UI and API level**" and that a use case is "You want separate views per team in the Airflow UI"
([Multi-Team → When to Use Multi-Team Mode](https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/multi-team.html#when-to-use-multi-team-mode)).

The Work in Progress section lists "Some UI elements may not be fully team-aware" as a known 3.4+ gap. This report concerns the **API** layer — the leak reproduces against the REST endpoints directly, not only through the UI — so it appears to fall outside that caveat.

Write operations and Task Execution API access **are** correctly scoped; only list/read appears to be unfiltered.

**Setup**

Two teams, `team-a` and `team-b`, created via `airflow teams create` and `airflow keycloak-auth-manager create-team`. Per-team Dag bundles:

```
AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST:
[{"name": "team_a_dags", "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
"kwargs": {"path": "/opt/airflow/dags/team-a"}, "team_name": "team-a"},
{"name": "team_b_dags", "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
"kwargs": {"path": "/opt/airflow/dags/team-b"}, "team_name": "team-b"},
{"name": "shared_dags", "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
"kwargs": {"path": "/opt/airflow/dags/shared"}}]
```

`airflow dags list` confirms the association:
`dag_id | bundle_name
check_dag_running | team_a_dags
test_owner_dag | team_b_dags
test_dag | shared_dags`

`user-a` is a member of Keycloak group `/team-a` only, with realm role `Admin` (explicitly **not** `SuperAdmin`).

Decoded access token:

```json
"realm_access": { "roles": ["default-roles-airflow", "offline_access", "uma_authorization", "Admin"] }, "groups": ["team-a"]
```

**Steps to reproduce**

1. Configure two teams with per-team Dag bundles as above.
2. Log in as `user-a` (member of `team-a` only).
3. `GET /api/v2/dags`

**Observed:** all three Dags returned, including `test_owner_dag` from `team_b_dags`. Same in the UI Dags list.

**Expected:** only `check_dag_running` (team-a) and `test_dag` (global).

`GET /api/v2/variables` behaves the same way: `user-a` sees entries with `"team_name": "team-b"`, including their values where the key name does not happen to match the sensitive-keyword masker.

**What IS correctly scoped**

Write operations — `user-a` cannot edit or delete `team-b` Variables or Connections.

Task Execution API, Variables — a task in a `team-a` Dag:
`BLOCKED user-b-var: AirflowRuntimeError: VARIABLE_NOT_FOUND
READ OK shared_var = everyone`

Task Execution API, Connections — a `SQLExecuteQueryOperator` using a `team-b` connection succeeds in a `team-b` Dag, and the identical task in a `team-a` Dag fails:

> AirflowNotFoundException: The conn_id user-b-conn-postgres isn't defined
> File ".../airflow/sdk/execution_time/context.py", line 230 in _get_connection

So enforcement is working at the Execution API and for writes, but not for list/read.

### What you think should happen instead?

List endpoints should filter by the requesting user's team, consistent with the documented "resource isolation ... at the UI and API level".

As it stands, a user in `team-a` can enumerate `team-b`'s Dag IDs and Variable keys via the REST API, and read Variable values whose names don't match the sensitive-keyword list.

Execution-layer isolation is working correctly, so this looks like a bounded gap in the list/read endpoints rather than a design problem. Filing separately from the known "some UI elements may not be fully team-aware" caveat because it reproduces against the API directly.

### Operating System

_No response_

### Deployment

None

### Apache Airflow Provider(s)

_No response_

### Versions of Apache Airflow Providers

_No response_

### Official Helm Chart version

Not Applicable

### Kubernetes Version

_No response_

### Helm Chart configuration

_No response_

### Docker Image customizations

Based on apache/airflow:3.3.0 with apache-airflow-providers-keycloak==0.8.2. No changes to core Airflow.

### Anything else?

Keycloak 26 auth manager, CeleryExecutor, Postgres 16 metadata DB, Python 3.13. Reproduces consistently.

Note: the Keycloak client's resource-server decision strategy must be set to Affirmative — under the default Unanimous, role-specific permissions veto access and every endpoint returns 403.

### Are you willing to submit PR?

- [ ] Yes I am willing to submit a PR!

### Code of Conduct

- [x] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)

Contributor guide

Open the contributing guide

Research direction

Start with the GET /api/v2/dags, /api/v2/variables, and /api/v2/connections entry points, comparing their list/read authorization with the write operations and Task Execution API behavior described here. Reproduce with user-a in team-a and confirm that team-b resources are excluded while shared resources remain visible. Done means the API no longer exposes team-b Dag IDs, Variable keys or values, or Connections to a team-a user.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, authorization, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.