Multi-Team: list endpoints return resources from all teams regardless of user's team
- 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
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