Skip to content

feat(AF-940): query-shape routing condition and denied_shapes grants - #1101

Merged
babltiga merged 3 commits into
mainfrom
feature/AF-940-query-shape-policy
Sep 25, 2026
Merged

babltiga merged 3 commits into
mainfrom
feature/AF-940-query-shape-policy

Conversation

@babltiga

Copy link
Copy Markdown
Contributor

Closes #940

Summary

Lets an admin constrain the shape of a query, not just its type and tables. Both deliverables from the issue, in the order it proposed:

  1. query_shape routing condition: {"type":"query_shape","any_of":["JOIN","SUBQUERY"]} matches a query that has any listed shape. It works with ESCALATE, REQUIRE_APPROVALS or AUTO_REJECT.
  2. denied_shapes grant capability: a hard deny-list on user and group datasource grants (V191). A query with a denied shape is rejected with 403 before it is persisted.

Shapes: JOIN, UNION (any set operation), SUBQUERY, CTE, GROUP_BY, HAVING, AGGREGATE, WINDOW_FUNCTION. They are detected anywhere in the statement by a new JSqlParser AST walker (proxy.internal.QueryShapeDetector) and unioned across a BEGIN…COMMIT batch.

Resolving the issue's open questions

  • Non-relational engines: SqlParseResult gains a third state, shapesAnalyzed, which mirrors the existing columnsAnalyzed. Plugin engines keep the pre-workflow: query-shape routing condition and grammar restriction #940 constructors and report "not analyzed". The deny-list fails closed on an unanalysed parse, and a non-empty denied_shapes is refused at grant time for an engine-managed datasource (422 DENIED_SHAPES_NOT_SUPPORTED). No engine re-pins are needed, because the old constructors are kept.
  • Aggregates: AGGREGATE covers the engines' built-in aggregate set, matched by name, plus any WITHIN GROUP or FILTERed call. User-defined aggregates are not detected; this is documented.

Where the issue's premise was wrong

Step 4 ("persist shape flags on the request") is not needed. Routing does not read persisted clause flags. ConditionContextFactory re-parses sql_text both live and in the AF-630 replay, exactly as has_where already does. So there is no query_requests migration.

Enforcement (denied_shapes)

  • Where it is checked: submission and the recurring recheck (DatasourcePermissionVerifier), break-glass, dry-run, request-group members (including OTHER members such as MERGE), and the access simulator (denied_shapes detail).
  • Merging and carry-over: denials union across grants, carry over when a JIT approval replaces a grant, and are included in attestation snapshots.

Also in this PR

  • DatasourceController now answers an unreadable request body with 400 VALIDATION_ERROR, where it used to return a 500 via the catch-all. This follows the existing per-controller precedent. It also changes an unknown db_type on create/update from 500 to 400.
  • Corrected a stale sentence in the team guide: denied columns now add up across grants (core: merge denied_columns by union across a user's grants #1099).

Docs / website

  • docs: docs/03-data-model.md, docs/04-api-spec.md, docs/05-backend.md, docs/06-frontend.md, docs/07-security.md
  • website: website/docs/configuration/{datasources,review-workflows,users-roles}, website/docs/guides/team, website/sitemap.xml (all three dates bumped to 2026-09-25)
  • README.md, and a regenerated help-corpus/

Tests

  • Backend: QueryShapeDetectorTest (56 cases), DeniedShapesTest, parser, routing-leaf, verifier, checker, merge, admin-service, handler, break-glass, request-group, dry-run and simulator tests, and DeniedShapesEnforcementIntegrationTest.
  • Frontend: form and trace mapping, deniedShapes util, and settings-page tests.
  • e2e: new e2e/tests/datasource-denied-shapes.spec.ts (UI grant plus 403/202), and a query_shape case in admin-routing-policies.spec.ts.

Verification (local)

  • mvn verify -Pcoverage green: 10,489 tests, including ApplicationModulesTest, ApiPackageDependencyTest and MessagesParityTest. After the review fixes, the affected classes plus the architecture gates were re-run and are green.
  • Frontend lint, typecheck, test:coverage (2,671 tests) and build are green.
  • mongodb and snowflake engines compile against the installed backend jar, and check-engine-pins.mjs verifies all 10 pins.
  • The e2e specs (denied-shapes, routing policies, denied-tables) pass against a local stack.
  • af-verifier was not dispatched: I ran every gate it would run myself (listed above).

Review notes

The reviewers ran before the final fix commit. I fixed their blockers in fix(AF-940): close shape-detector gaps found in review:

  • af-java-reviewer / af-reviewer (fixed): the detector missed FROM (a JOIN b …), MERGE, function-argument subqueries (ARRAY(SELECT …)), JSON_ARRAYAGG/JSON_OBJECTAGG and several vendor built-in aggregates. OTHER request-group members also skipped the check. All of these now have regression tests.
  • af-java-reviewer (fixed): a stored shape name that the enum no longer has now denies every shape instead of throwing.
  • af-content-reviewer (fixed): corrected the engine-scope wording of the routing leaf and the simulator's shapes_analyzed. Also documented who is exempt (query-admins at submission, but not at break-glass), and stated that shapes do not make a grant read-only.
  • af-frontend-reviewer (fixed): the e2e query_shape policy is now scoped to its datasource with a run-unique priority, so it cannot auto-reject other specs' joins. Also added a group-row test.
  • Accepted gap (af-frontend-reviewer): the routing builder's query_shape editor is not rendered in a component or e2e test. Its form mapping is unit-tested, and it mirrors the day_of_week/risk_level editors exactly.
  • Pre-existing, out of scope (af-java-reviewer): DeniedColumns.rejected has the same OTHER short-circuit, so request-group OTHER members skip denied-column checks. I'll file it as a follow-up.
  • Known limit (af-reviewer): Russian plural forms only have _one/_other, matching the existing deny-list keys.

Screenshots

Routing policy list with query-shape conditions
Routing policy editor with the Query shape operand
Permissions table showing the Denied shapes column and tooltip
Grant access modal with the Denied query shapes multi-select

Detect joins, set operations, subqueries, CTEs, GROUP BY, HAVING, aggregates
and window functions anywhere in a statement's AST, unioned across a
transactional batch. SqlParseResult gains shapes + shapesAnalyzed; plugin
engines keep the old constructors and report not-analyzed, so the new
query_shape routing leaf fails closed on them.
Add denied_shapes to user and group datasource grants (V191), enforced at
submission, the recurring recheck, break-glass, dry-run, request-group
members and the access simulator with a 403. Denials union across grants,
carry over a JIT replacement, and fail closed when the shape was not
analyzed; engine-managed datasources refuse the field at grant time (422).
Docs, website, help corpus and e2e specs cover both halves of #940.
Flag parenthesised joins, MERGE, function-argument subqueries such as
ARRAY(SELECT ...), JSON_ARRAYAGG/OBJECTAGG and more vendor aggregates;
check OTHER request-group members; an unknown stored shape denies all.
Scope the e2e routing policy to its datasource; correct docs scope.
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Test Results

    1 files  ± 0    312 suites  +1   12m 58s ⏱️ -37s
2 671 tests +13  2 671 ✅ +13  0 💤 ±0  0 ❌ ±0 
2 672 runs  +13  2 672 ✅ +13  0 💤 ±0  0 ❌ ±0 

Results for commit 12f0de9. ± Comparison against base commit cf06398.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Frontend Coverage (frontend)

Status Category Percentage Covered / Total
🟢 Lines 96.06% (🎯 90%) 3688 / 3839
🟢 Statements 94.68% (🎯 90%) 4097 / 4327
🟢 Functions 94.07% (🎯 90%) 1128 / 1199
🟢 Branches 87.68% (🎯 80%) 2457 / 2802
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
frontend/src/components/policies/decisionTraceDetails.ts 98.48% 94.94% 95% 100% 182
frontend/src/pages/admin/routingPolicyForm.ts 97.95% 79.27% 95.23% 99.26% 126, 132, 393
frontend/src/utils/apiErrors.ts 79.33% 71.91% 96.29% 84.8% 108-109, 111, 126-127, 129, 148, 151-152, 154, 170-171, 236, 239, 255, 302-303, 305, 340-342, 344, 360-361, 363, 401-403, 406, 421-422, 445-447, 449, 468, 493-494, 496, 530, 537-556, 600-607, 610
frontend/src/utils/deniedShapes.ts 100% 100% 100% 100%
frontend/src/utils/enumLabels.ts 98.46% 100% 95.65% 98.45% 281, 289, 688, 746
Generated in workflow #1417 for commit 12f0de9 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Backend Test Results

10 504 tests  +110   10 504 ✅ +110   11m 48s ⏱️ +15s
 1 150 suites +  3        0 💤 ±  0 
 1 150 files   +  3        0 ❌ ±  0 

Results for commit 12f0de9. ± Comparison against base commit cf06398.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Code Coverage

Overall Project 94.8% 🍏
Files changed 99.25% 🍏

File Coverage
DatasourcePermissionVerifier.java 100% 🍏
DatasourcePermissionChecker.java 100% 🍏
AccessGrantMaterializer.java 100% 🍏
CreatePermissionRequest.java 100% 🍏
PermissionResponse.java 100% 🍏
GroupPermissionResponse.java 100% 🍏
CreateGroupPermissionRequest.java 100% 🍏
RoutingConditionEvaluator.java 100% 🍏
ConditionContextFactory.java 100% 🍏
QueryShape.java 100% 🍏
DatasourceGroupPermissionView.java 100% 🍏
CreateDatasourceGroupPermissionCommand.java 100% 🍏
DeniedShapesNotSupportedException.java 100% 🍏
DatasourceUserPermissionView.java 100% 🍏
DeniedShapes.java 100% 🍏
DatasourcePermissionView.java 100% 🍏
DatasourceAdminException.java 100% 🍏
CreatePermissionCommand.java 100% 🍏
QueryShapeDetector.java 99.22% -0.78% 🍏
DefaultAccessSimulationService.java 98.51% 🍏
DefaultDatasourceUserPermissionLookupService.java 97.04% 🍏
AccessSimulationResponse.java 95.94% 🍏
ConditionNode.java 95.82% 🍏
ConditionContext.java 95.77% -1.41% 🍏
DefaultQueryDryRunService.java 95.15% 🍏
DefaultAttestationLifecycleService.java 93.45% 🍏
SqlParserServiceImpl.java 93.05% -0.29% 🍏
SqlParseResult.java 91.82% 🍏
DatasourceAdminServiceImpl.java 90.94% 🍏
DatasourcePermissionContribution.java 89.29% -1.79% 🍏
GlobalExceptionHandler.java 86.23% 🍏
DefaultBreakGlassService.java 85.65% 🍏
RoutingConditionValidator.java 83.96% 🍏
DefaultRequestGroupService.java 82.82% 🍏
DatasourceController.java 79.89% 🍏

@babltiga
babltiga merged commit 4d28670 into main Sep 25, 2026
35 checks passed
@babltiga
babltiga deleted the feature/AF-940-query-shape-policy branch September 25, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

workflow: query-shape routing condition and grammar restriction

1 participant