Enforce access checks on mutating routes + add coverage guard#2334
Enforce access checks on mutating routes + add coverage guard#2334ruizhang0519 wants to merge 26 commits into
Conversation
✅ Deploy Preview for thriving-cassata-78ae72 canceled.
|
d2d4900 to
ffc3e61
Compare
ffc3e61 to
3c8787d
Compare
|
|
||
| @router.post("/namespaces/{namespace}/", status_code=HTTPStatus.CREATED) | ||
| async def create_node_namespace( | ||
| async def create_or_reactivate_namespace( |
There was a problem hiding this comment.
Can this be moved into an internal/namespaces.py?
| @@ -24,8 +24,10 @@ | |||
| from datajunction_server.internal.access.authentication.http import SecureAPIRouter | |||
There was a problem hiding this comment.
OK, so after some stress testing found an issue. upsert_materialization never checks WRITE on node_name.
create_new_materialization sends transforms and dimensions through build_non_cube_materialization_config, which is not passed an access_checker, and the DRUID_CUBE job through build_cube_materialization, which also does not check. The only branch that reaches .check() is the non-DRUID cube job, and that check is READ on the cube's metrics and dimensions, not WRITE on the node being materialized. So there is no WRITE gate on this endpoint for any node type.
Quick Example:
Let's say Restrictive RBAC governs finance; Alice has no grant on it.
POST /nodes/finance.orders/materializations/{name}/backfill returns 403
POST /nodes/finance.orders/materialization with a valid transform body returns 201: DJ writes the materialization, records history, and schedules the workflow. Because upsert updates in place, Alice can also overwrite the owner's existing config and reschedule it.
Suggested fix:
Add access_checker.add_request_by_node_name(node_name, ResourceAction.WRITE) and await access_checker.check(on_denied=AccessDenialMode.RAISE) at the top of the handler, matching deactivate and backfill.
Tracking: #2234 (step 0 of the RBAC enablement sequence).
RBAC enforcement is opt-in per endpoint: a mutation is only authorized if its handler reaches
AccessChecker.check(). Because the default access policy is permissive, an endpoint that never callscheck()fails open silently — nothing turns red today, and the gap only becomes exploitable once a namespace is governed. There was no test guaranteeing coverage, and several mutating endpoints were uncovered (includingremove_complex_dimension_link, which built an access request but never calledcheck()).This PR makes the mutating-route surface completely and durably covered:
tests/test_route_coverage.py): enumerates every mutating (POST/PUT/PATCH/DELETE) route from the app and asserts each either reachescheck()(directly or via a helper) or is in an explicit allowlist with a reason. Two buckets:PENDING_COVERAGE(the step-0 backlog, which must reach empty) andINTENTIONAL_EXCLUSIONS(routes deliberately not node-governed). A companion test turns green once the backlog is empty, so "step-0 route coverage is complete" is a checkable signal.WRITEon the affected resource:remove_complex_dimension_link) — built requests but never calledcheck().WRITEon the parent boundary. Extractedcreate_or_reactivate_namespaceso the HTTP endpoint (which enforces) and the internalregister_table/register_viewcallers (which enforce their own) share the create logic without an endpoint-level check being skipped.WRITEon the node.WRITEon the cube node.WRITEon the node the pre-agg is based on (the metrics' parent nodes forplan/register).POST /preaggs/{id}/availability/is a query-service completion callback, not a user action; governing it with userWRITEwould break callbacks, so it is excluded pending a service-identity model.403whenWRITEis denied. The guard provescheck()is reached; these prove it enforces.Why this won't take effect now: DJ's default access policy is still permissive, so the new checks allow requests when no explicit restrictive grant/policy denies them. This is a no-op today; it becomes relevant once a deployment enables restrictive RBAC and governs a namespace.
Out of scope (tracked as step-0 follow-ups):
availabilityservice-identity model, and theregisterdenial test.Verification:
Automated (same as CI):
The denial tests install an authorization service that denies
WRITE(the automated equivalent of a restrictive deployment) and assert403 Access deniedfor: dimension-link removal, namespace create, materialization deactivate/backfill, cube materialize/deactivate/backfill, preaggregation bulk-deactivate, and preaggregation materialize/config/workflow/backfill/plan.