Skip to content

feat(AF-939): table and schema deny-lists alongside allow-lists - #1097

Merged
babltiga merged 1 commit into
mainfrom
feature/AF-939-table-deny-lists
Sep 24, 2026
Merged

babltiga merged 1 commit into
mainfrom
feature/AF-939-table-deny-lists

Conversation

@babltiga

Copy link
Copy Markdown
Contributor

Closes #939

What

Datasource grants gain denied_schemas / denied_tables alongside the allow-lists, so an admin can say "all of crm except crm.salary" instead of enumerating every other table. That enumeration would go stale, and fail open, whenever someone adds a table.

  • A denial always beats the allow-list. allowed_schemas=[crm] + denied_tables=[crm.salary] permits crm.customer and any table created later in crm, and rejects crm.salary. Denials also work with no allow-list at all.
  • Denials merge by union across direct, group and JIT grants. This is the inverse of every other merged field, and it is stated in the DatasourceUserPermissionLookupService Javadoc and merge(). A permissive group grant cannot lift a direct grant's denial (DefaultDatasourceUserPermissionLookupServiceTest, DeniedTablesEnforcementIntegrationTest). When an approved JIT request replaces an expiring direct row, that row's denials (tables, schemas and columns) carry over onto the JIT grant.
  • One fail-closed matcher, core.api.DeniedTables:
    • Names compare segment by segment from the right. salary denies it in every schema, and crm.salary also denies an unqualified salary.
    • Any denied_schemas entry refuses unqualified references, so users must schema-qualify (confirmed design decision).
    • SQL Server db..table, Oracle @dblink suffixes and Elasticsearch-style patterns (sal*) cannot slip past.
    • schema.* in denied_tables means the whole schema.
  • Enforced everywhere the allow-list is:
    • Submission and the recurring recheck: 403 error.permission.table_denied.
    • Break-glass and dry-run.
    • Access simulator: new denied_tables detail and DENY step.
    • Effective-access reverse index and query suggestions.
    • Table preview (404) and filtered schema introspection: denied tables and schemas are hidden, which also covers MCP and AI text-to-SQL.
    • Request-group members: new. Request groups previously had no table check at all.
  • Validation:
    • Limits are @Size(50) / @Size(200) with @NotBlank items.
    • An @Pattern refuses entries that could never match anything: a dotted or wildcard schema, empty segments, or a stray *.
    • The same checks are mirrored in the service layer and in the Form.Item rules.
  • UI:
    • The Grant access modal gets "Denied schemas" and "Denied tables" tag fields; table options are schema-qualified.
    • The user and group permission tables get a "Denied tables" column. Its tooltip lists denied schemas as schema.*.
    • The existing "Denied" header is renamed "Denied columns".
  • Migration: V190__add_denied_tables_to_permissions.sql adds nullable TEXT[] columns.

Docs & website

  • docs/03-data-model.md, docs/04-api-spec.md, docs/05-backend.md, docs/06-frontend.md, docs/07-security.md: the authorization flow diagram gets a deny step, plus a section on known engine limits.
  • README.md
  • website/docs/configuration/datasources/, website/docs/configuration/users-roles/, website/docs/guides/datasource/, website/docs/guides/team/, and website/sitemap.xml.
  • help-corpus/ is regenerated.
  • The core: table deny-lists alongside allow-lists #939 roadmap entry is unchanged because it isn't listed there.

Tests

  • Backend mvn verify -Pcoverage: 10,387 tests, 0 failures. That includes ApplicationModulesTest, ApiPackageDependencyTest and MessagesParityTest.
    • New test classes: DeniedTablesTest and DeniedTablesEnforcementIntegrationTest.
    • Deny-list cases are added to about 15 existing test classes.
  • Frontend: lint (0 errors), typecheck, test:coverage (2,653+ tests, 96% lines / 87.7% branches) and build all pass.
  • E2E: new datasource-denied-tables.spec.ts. It passed locally together with datasource-denied-columns, datasource-settings and the access-simulation specs.

Review notes

The af-verifier, af-reviewer, af-java-reviewer, af-frontend-reviewer and af-content-reviewer agents reviewed the branch before this PR.

  • Fixed:
    • af-reviewer (Blocker): a JIT approval replaced an admin's expiring direct row, and the replacement carried no denials. Denials now carry over, with tests.
    • af-java-reviewer (Blockers): db..table and sal* index patterns could get past a denial. Also fixed: the Oracle @dblink concern, and entries that could never match (now refused by validation).
    • af-frontend-reviewer: validator branches and the empty cell are now tested.
    • af-content-reviewer: the JIT wording that read as an exemption is fixed, and engine scope is now documented.
  • Rebutted:
    • af-reviewer: "the effective-access row shows granted=false without a reason." The reverse index only returns granted rows (DefaultEffectiveAccessService:88), so a denied user is simply not listed.
    • af-frontend-reviewer: "the blank-entry branch is unreachable." It is kept for backend @NotBlank parity (frontend-form rule).
  • Surviving concerns, documented as known limits:
    • af-java-reviewer: some engine plugins under-report references, a gap the allow-list shares. The misses are MongoDB $lookup / $unionWith / $graphLookup, Neo4j MATCH (n) and Redis KEYS globs.
    • af-java-reviewer: on schema-less engines, any denied_schemas entry refuses every query.
    • af-java-reviewer: revoking a grant removes its denials, so access can widen.
  • Out of scope, filed as follow-ups:

Screenshots

Permissions table with the new Denied tables column and its tooltip

Grant access modal with Denied schemas and Denied tables

Invalid deny entry refused in the form

Add denied_schemas / denied_tables to user and group datasource grants
(V190). A denial always beats the allow-list, so an admin can allow a
whole schema and carve out a few tables; new tables in the schema are
allowed automatically unless denied.

Denials merge by union across a user's grants — the inverse of the
allow-list merge — so a permissive group grant can never lift one, and a
JIT approval carries the replaced row's denials over. One fail-closed
matcher (core.api.DeniedTables) backs submission, the recurring recheck,
break-glass, dry-run, the access simulator, effective access, query
suggestions, table preview, schema introspection and request-group
members. Unqualified references are refused under any schema denial;
db..table, @dblink suffixes and index patterns cannot slip past.
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Test Results

    1 files  ± 0    311 suites  +1   12m 14s ⏱️ -18s
2 658 tests +16  2 658 ✅ +16  0 💤 ±0  0 ❌ ±0 
2 659 runs  +16  2 659 ✅ +16  0 💤 ±0  0 ❌ ±0 

Results for commit 106e956. ± Comparison against base commit cfae20e.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Frontend Coverage (frontend)

Status Category Percentage Covered / Total
🟢 Lines 96.05% (🎯 90%) 3672 / 3823
🟢 Statements 94.68% (🎯 90%) 4080 / 4309
🟢 Functions 94.05% (🎯 90%) 1124 / 1195
🟢 Branches 87.7% (🎯 80%) 2439 / 2781
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
frontend/src/components/policies/decisionTraceDetails.ts 98.43% 94.68% 95% 100% 176
frontend/src/utils/deniedTables.ts 100% 100% 100% 100%
Generated in workflow #1411 for commit 106e956 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Backend Test Results

10 387 tests  +78   10 387 ✅ +78   10m 13s ⏱️ - 1m 22s
 1 147 suites + 2        0 💤 ± 0 
 1 147 files   + 2        0 ❌ ± 0 

Results for commit 106e956. ± Comparison against base commit cfae20e.

@babltiga
babltiga merged commit 9fc3865 into main Sep 24, 2026
55 of 57 checks passed
@babltiga
babltiga deleted the feature/AF-939-table-deny-lists branch September 24, 2026 15:44
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.

core: table deny-lists alongside allow-lists

1 participant