Skip to content

Commit cd5d7d5

Browse files
committed
refactor: cleanups
1 parent 7b64d3a commit cd5d7d5

3 files changed

Lines changed: 38 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ All notable changes to this project will be documented in this file.
2424
- vector: Look for SBOM in correct location ([#1471]).
2525
- vector: Use correct license ([#1476]).
2626
- trino: Build a patched Airlift from source and depend on it to backport [airlift/airlift#1943](https://github.com/airlift/airlift/pull/1943), applying the configured max response header size to Jetty's `maxResponseHeaderSize` ([#1510]).
27-
- airflow: Route DAG listings and menu items through OPA in the Airflow 3 OPA auth manager, and wire the OPA cache on the FastAPI api-server init path ([#XXXX]).
27+
- airflow: Route DAG listings and menu items through OPA in the Airflow 3 OPA auth manager, and wire the OPA cache on the FastAPI api-server init path ([#1512]).
2828

2929
### Removed
3030

@@ -43,6 +43,7 @@ All notable changes to this project will be documented in this file.
4343
[#1493]: https://github.com/stackabletech/docker-images/pull/1493
4444
[#1509]: https://github.com/stackabletech/docker-images/pull/1509
4545
[#1510]: https://github.com/stackabletech/docker-images/pull/1510
46+
[#1512]: https://github.com/stackabletech/docker-images/pull/1512
4647

4748
## [26.3.0] - 2026-03-16
4849

airflow/opa-auth-manager/airflow-3/opa_auth_manager/opa_fab_auth_manager.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,16 @@ def _init_opa_resources(self) -> None:
103103
"""
104104
Set up the OPA cache and HTTP session.
105105
106-
Idempotent so it can be called from both ``init`` (FastAPI api-server)
107-
and ``init_flask_resources`` (Flask AppBuilder) — only one of the two
108-
runs depending on the Airflow component.
106+
Called from both ``init`` (FastAPI api-server) and
107+
``init_flask_resources`` (Flask AppBuilder). In Airflow 3 both run
108+
during a single api-server startup but on *different*
109+
OpaFabAuthManager instances — ``init_appbuilder`` calls
110+
``create_auth_manager()`` again, which constructs a fresh instance.
111+
The api-server instance is reachable via
112+
``request.app.state.auth_manager``; the FAB instance is returned by
113+
``get_auth_manager()``. Both need their own cache and session.
109114
"""
110115

111-
if getattr(self, "opa_cache", None) is not None:
112-
return
113-
114116
Stats.incr(METRIC_NAME_OPA_CACHE_LIMIT_REACHED, count=0)
115117

116118
self.opa_cache = Cache(
@@ -590,11 +592,18 @@ def get_authorized_dag_ids(
590592
# FabAuthManager's implementation consults the user's FAB DB role
591593
# permissions and bypasses is_authorized_dag entirely, which makes any
592594
# user without a FAB role (e.g. the default Public role) see an empty
593-
# DAG list even when OPA would allow them. Re-implement the
594-
# BaseAuthManager default: list all DAG ids and filter them via
595-
# is_authorized_dag → OPA.
595+
# DAG list even when OPA would allow them. List all DAG ids and filter
596+
# them via is_authorized_dag → OPA directly, without going through
597+
# filter_authorized_dag_ids — the Fab base class may add a DB-backed
598+
# override of that method in the future.
596599
dag_ids = {dag.dag_id for dag in session.execute(select(DagModel.dag_id))}
597-
return self.filter_authorized_dag_ids(dag_ids=dag_ids, method=method, user=user)
600+
return {
601+
dag_id
602+
for dag_id in dag_ids
603+
if self.is_authorized_dag(
604+
method=method, details=DagDetails(id=dag_id), user=user
605+
)
606+
}
598607

599608
@override
600609
def filter_authorized_menu_items(

airflow/opa-auth-manager/airflow-3/tests/test_opa_fab_auth_manager.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,23 @@ def test_filter_authorized_menu_items_routes_through_opa(
389389
)
390390
assert denied == []
391391

392+
def test_filter_authorized_menu_items_denies_unknown(
393+
self, auth_manager_with_appbuilder, mock_opa
394+
):
395+
# A MenuItem value not handled by _is_menu_item_authorized (e.g. one
396+
# introduced in a future Airflow version) must fail closed: denied
397+
# without consulting OPA, so a new UI surface isn't silently exposed
398+
# before the dispatch table is updated.
399+
unknown = Mock(spec=MenuItem)
400+
unknown.name = "FUTURE_THING"
401+
402+
result = auth_manager_with_appbuilder.filter_authorized_menu_items(
403+
[unknown], user=_make_user()
404+
)
405+
406+
assert result == []
407+
assert mock_opa.calls == []
408+
392409
def test_filter_authorized_menu_items_preserves_order_and_filters(
393410
self, auth_manager_with_appbuilder, mock_opa
394411
):

0 commit comments

Comments
 (0)