Skip to content

feat(AF-942): per-user data-volume budgets - #1105

Merged
babltiga merged 4 commits into
mainfrom
feature/AF-942-per-user-data-volume-budgets
Sep 25, 2026
Merged

babltiga merged 4 commits into
mainfrom
feature/AF-942-per-user-data-volume-budgets

Conversation

@babltiga

Copy link
Copy Markdown
Contributor

Closes #942

What

Per-user data-volume budgets: a datasource-scoped limit on the rows and/or result bytes each targeted user may read over a rolling window (1 hour–31 days, default 24 h), with an action once it is used up. This is the slow-exfiltration control that per-query caps can't be: until now, a user allowed 1,000 rows per query could read a million in an afternoon.

These were the open questions in the issue; each was settled before implementation:

Question Decision
Window Rolling trailing window (matches the max_queries_per_day precedent — no reset job, no timezone)
Accounting Append-only usage ledger (data_budget_usage) rather than sums over query_requests, so request groups and table previews count too
Metrics Rows and result bytes (the proxy's estimated result size)
Partial consumption Truncate at the remaining allowance (truncated_reason=DATA_BUDGET); once exhausted, per-budget REJECT or REQUIRE_REVIEW
Masked / row-security-filtered rows Count what the user actually received

Semantics

  • Scope. applies_to roles / groups / users, the same model as row-limit policies (proxy: per-table row limits #934); an empty scope means everyone. Each user gets their own allowance: a group target is not a shared pool. When several budgets apply, the most constrained one wins, and REJECT beats REQUIRE_REVIEW.
  • Decision (SELECT only). An exhausted REJECT budget moves the query PENDING_AI → REJECTED, decided right after the proxy: bytes-scanned cost caps #941 bytes cap. An exhausted REQUIRE_REVIEW budget suppresses every auto-approve path, like a SQL review BLOCK, and never softens AUTO_REJECT. There is a new DATA_BUDGET decision-trace step and an audit action, QUERY_DATA_BUDGET_ENFORCED.
  • Execution. While allowance remains, the result is capped to it. Once the budget is exhausted, REJECT means APPROVED → FAILED. Under REQUIRE_REVIEW the query runs only if the exhausted budget itself forced its review (query_requests.data_budget_review_forced, V195), so an approval obtained while allowance remained never lifts the budget. Request-group members fail closed. Table previews answer 409 DATA_BUDGET_EXHAUSTED. Break-glass is counted but never capped or refused.
  • Routing operand data_budget_used_percent. Escalates heavy readers before they hit the limit; fails closed.
  • Visibility. The query editor shows the remaining allowance, and admins get a Data usage drawer on the users page. Notifications fire at the warn threshold (DATA_BUDGET_THRESHOLD_REACHED) and on exhaustion (DATA_BUDGET_EXHAUSTED, which also goes to DATA_BUDGET_MANAGE holders).
  • No budget configured leaves behaviour unchanged. No ledger rows, no truncation, no decision input.

Interface surface

  • Migrations. V193 adds data_budgets, data_budget_usage and two PG enums. V194 seeds the DATA_BUDGET_MANAGE permission. V195 adds query_requests.data_budget_review_forced.
  • REST. /api/v1/datasources/{id}/data-budgets (CRUD), …/data-budgets/me, and /api/v1/admin/users/{id}/data-budget-usage.
  • Configuration. ACCESSFLOW_CORE_DATA_BUDGET_USAGE_RETENTION (default P32D) and ACCESSFLOW_CORE_DATA_BUDGET_PRUNE_INTERVAL (default PT1H) drive DataBudgetUsagePruneJob, which holds a @SchedulerLock.
  • Engine plugins. QueryExecutionRequest.maxResultBytesOverride and SelectExecutionResult.resultBytes are new; the previous canonical constructors are kept, so published engine plugins stay binary-compatible.

Docs & website

  • docs/: 03-data-model, 04-api-spec, 05-backend (including the job registry), 06-frontend, 07-security, 08-notifications, 09-deployment and 13-mcp.
  • Repo root: CLAUDE.md (the state machine) and README.md.
  • Website: website/docs/configuration/{datasources,review-workflows,users-roles}/index.html and website/README.md.
  • help-corpus/: regenerated.

Verification

  • Backend: the full mvn verify -Pcoverage ran 10,747 tests. The 2 failures were stale step-index assertions in AdminAccessSimulationControllerIntegrationTest (the trace gained a step); they are fixed and re-run green. After the review fixes, the affected suites were re-run green, including the new DataBudgetEnforcementIntegrationTest and DataBudgetControllerIntegrationTest (Testcontainers) and ApplicationModulesTest / ApiPackageDependencyTest / MessagesParityTest.
  • Frontend: lint, typecheck and build pass. test:coverage ran 2,738/2,738 at 94.7% statements / 87.8% branches.
  • E2E: the new e2e/tests/data-budgets.spec.ts covers truncation, escalation to review, rejection, the editor indicator, the settings tab and the admin drawer. It passed 3×, repeated, together with discovery.spec.ts.
    • The full local suite passed 372/394. One of its failures, discovery.spec.ts, was caused by this change: the new tab pushes Discovery out of the visible tab strip. It is fixed by switching to the existing clickTab helper.

Review notes

Five independent reviewers ran (af-verifier, af-reviewer, af-java-reviewer, af-frontend-reviewer, af-content-reviewer). What they found and what was done:

Fixed:

  • af-java-reviewer: an approval given while allowance remained lifted the cap after the budget ran out. Fixed with the data_budget_review_forced stamp.
  • af-java-reviewer: a query approved through an AF-453 ticket failed at execution (such approvals write no decision row). The same stamp fixes it.
  • af-java-reviewer: concurrent charges could miss or double threshold notifications. Fixed with a per-(user, datasource) pg_advisory_xact_lock.
  • af-java-reviewer: a refused table preview wrote no audit row. It now does.
  • af-frontend-reviewer: the e2e scoped to an empty tabpanel.
  • af-frontend-reviewer: a null warn threshold still warned at 80%.
  • af-frontend-reviewer: a 90-minute window was rewritten on edit (a minutes unit was added).
  • af-frontend-reviewer: the remaining-allowance indicator went stale after an execute.
  • af-frontend-reviewer: the admin drawer had no e2e coverage.
  • af-content-reviewer: the break-glass wording was wrong, and the "Data usage" visibility claim did not match the page's permission gate.
  • af-content-reviewer: request-group audit rows lacked window_minutes; a table-preview row cut was not attributed to the budget; the website used the wrong breach-action label.

Surviving concerns, not addressed:

  • af-java-reviewer: each decision reads the standing twice (once for the routing context, once for the guard). It is cheap and was left as is.
  • af-java-reviewer: measureBytes re-estimates every SELECT row even when no budget applies.
  • af-frontend-reviewer: budget notifications open /editor without the datasource pre-selected.
  • af-reviewer: there is no UI e2e for the new routing operand. It is covered by the backend integration tests and a form unit test.
  • Local e2e environment: admin-users-crud (row edit), reviews-reject and ws-realtime failed on this machine. The users table is wider than the 1280px viewport, and a sticky onboarding panel overlays rows. These specs exercise flows this change does not touch, but I did not prove the failures also happen on main. admin-review-plans failed once in the parallel run and passed when re-run alone.

Screenshots

Data budgets tab on the datasource settings page
New data budget form
Admin Data usage drawer on the users page
Remaining allowance in the query editor

Datasource-scoped budgets bound the rows and result bytes each targeted user
may read over a rolling window. Reads are capped at the allowance left; an
exhausted budget rejects the SELECT or forces human review, and only a review
the exhausted budget itself forced may run past it. Usage is an append-only
ledger charged by query execution, request-group members and sample data,
pruned by a locked job. Adds the data_budget_used_percent routing operand,
the DATA_BUDGET decision step, DATA_BUDGET_MANAGE, audit actions and two
notification events.

Refs #942
Datasource settings gain a Data budgets tab; the query editor shows the
caller's remaining allowance; the users page opens a per-user Data usage
drawer. Adds the routing operand, trace step, truncation label, bell cases
and an e2e spec.

Refs #942
The Data budgets tab pushes Discovery/Classification out of the visible
tab strip, where a physical click is swallowed; use the clickTab helper.

Refs #942
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Test Results

    1 files  ± 0    319 suites  +5   13m 28s ⏱️ -12s
2 737 tests +38  2 737 ✅ +38  0 💤 ±0  0 ❌ ±0 
2 738 runs  +38  2 738 ✅ +38  0 💤 ±0  0 ❌ ±0 

Results for commit b4c722e. ± Comparison against base commit cbee5ad.

This pull request removes 2 and adds 40 tests. Note that renamed tests count towards both.
src/utils/__tests__/decisionTraceEnums.test.ts ‑ decision-trace enum labels > keeps the query step enum at the thirteen backend values
src/utils/permissions.test.ts ‑ permissions > exposes the full 47-entry catalog mirror
src/api/dataBudgets.test.ts ‑ api/dataBudgets > builds query keys
src/api/dataBudgets.test.ts ‑ api/dataBudgets > creates, updates and deletes a budget
src/api/dataBudgets.test.ts ‑ api/dataBudgets > lists budgets for a datasource
src/api/dataBudgets.test.ts ‑ api/dataBudgets > reads the caller standing and a user standing
src/components/admin/UserDataBudgetDrawer.test.tsx ‑ UserDataBudgetDrawer > does not load anything while closed
src/components/admin/UserDataBudgetDrawer.test.tsx ‑ UserDataBudgetDrawer > reports a load failure
src/components/admin/UserDataBudgetDrawer.test.tsx ‑ UserDataBudgetDrawer > says when no budget applies
src/components/admin/UserDataBudgetDrawer.test.tsx ‑ UserDataBudgetDrawer > shows usage per datasource and budget
src/components/common/__tests__/NotificationBell.test.tsx ‑ NotificationBell > renders DATA_BUDGET_EXHAUSTED and opens the query editor (#942)
src/components/common/__tests__/NotificationBell.test.tsx ‑ NotificationBell > renders DATA_BUDGET_THRESHOLD_REACHED and opens the query editor (#942)
…

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Frontend Coverage (frontend)

Status Category Percentage Covered / Total
🟢 Lines 96.13% (🎯 90%) 3757 / 3908
🟢 Statements 94.75% (🎯 90%) 4171 / 4402
🟢 Functions 94.14% (🎯 90%) 1142 / 1213
🟢 Branches 87.79% (🎯 80%) 2532 / 2884
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
frontend/src/components/policies/decisionTraceDetails.ts 98.68% 95.86% 95% 100% 223
frontend/src/pages/admin/routingPolicyForm.ts 98.08% 79.45% 95.23% 99.31% 137, 143, 437
frontend/src/utils/dataBudget.ts 100% 100% 100% 100%
frontend/src/utils/enumLabels.ts 98.52% 100% 95.83% 98.52% 285, 293, 734, 792
frontend/src/utils/permissions.ts 100% 100% 100% 100%
Generated in workflow #1425 for commit b4c722e by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Backend Test Results

10 753 tests  +163   10 753 ✅ +163   12m 23s ⏱️ +25s
 1 168 suites + 14        0 💤 ±  0 
 1 168 files   + 14        0 ❌ ±  0 

Results for commit b4c722e. ± Comparison against base commit cbee5ad.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Code Coverage

Overall Project 94.92% -0.06% 🍏
Files changed 96.73% 🍏

File Coverage
DataBudgetUsagePruneJob.java 100% 🍏
QueryDecisionKind.java 100% 🍏
QueryDecision.java 100% 🍏
DataBudgetThresholdCrossedEvent.java 100% 🍏
DefaultDataBudgetUsageService.java 100% 🍏
AppliesToMatcher.java 100% 🍏
DefaultRowLimitPolicyResolutionService.java 100% 🍏
DefaultDataBudgetAdminService.java 100% 🍏
NotificationEventType.java 100% 🍏
PagerDutyTrigger.java 100% 🍏
DataBudgetNotificationListener.java 100% 🍏
DataBudgetNotice.java 100% 🍏
NotificationContext.java 100% 🍏
DataBudgetEntity.java 100% 🍏
DataBudgetResponse.java 100% 🍏
DataBudgetRequest.java 100% 🍏
DataBudgetListResponse.java 100% 🍏
DataBudgetStatusResponse.java 100% 🍏
DataBudgetStatusListResponse.java 100% 🍏
RoutingConditionEvaluator.java 100% 🍏
ConditionContextFactory.java 100% 🍏
DataBudgetView.java 100% 🍏
DataBudgetUsageSource.java 100% 🍏
DataBudgetExhaustedException.java 100% 🍏
IllegalDataBudgetException.java 100% 🍏
DataBudgetCommand.java 100% 🍏
DataBudgetStatus.java 100% 🍏
DataBudgetConsumption.java 100% 🍏
DataBudgetException.java 100% 🍏
DataBudgetUsageRecord.java 100% 🍏
SelectExecutionResult.java 100% 🍏
DataBudgetBreachAction.java 100% 🍏
Permission.java 100% 🍏
DataBudgetNotFoundException.java 100% 🍏
AuditAction.java 100% 🍏
AuditResourceType.java 100% 🍏
QueryDecisionStepKind.java 100% 🍏
TicketDescriptionBuilder.java 100% 🍏
CorePropertiesConfiguration.java 100% 🍏
DataBudgetProperties.java 100% 🍏
QueryDecisionEvaluator.java 99.93% 🍏
DefaultSampleDataService.java 98.65% -1.02% 🍏
DefaultAccessSimulationService.java 98.54% 🍏
DefaultDataBudgetStatusService.java 97.44% -2.56% 🍏
DataBudgetText.java 97.41% -2.59% 🍏
DataBudgetCheck.java 97.1% -2.9% 🍏
ConditionContext.java 96.95% 🍏
QueryReviewStateMachine.java 96.81% -0.99% 🍏
ConditionNode.java 96.42% 🍏
DefaultQueryLifecycleService.java 96.26% 🍏
DefaultQueryExecutor.java 95.38% 🍏
QueryExecutionRequest.java 94.96% 🍏
DefaultQueryRequestStateService.java 93.6% 🍏
GroupExecutionService.java 93.23% 🍏
DataBudgetController.java 92.65% -7.35% 🍏
NotificationContextBuilder.java 90.8% -0.28% 🍏
DataBudgetStatusController.java 89.87% -10.13% 🍏
MsTeamsPayloadFactory.java 88.73% 🍏
SlackBlockKitFactory.java 88.49% 🍏
WebhookPayloadFactory.java 88.08% 🍏
PagerDutyPayloadFactory.java 87.98% 🍏
TelegramMessageFactory.java 87.78% 🍏
DiscordPayloadFactory.java 87.58% 🍏
GlobalExceptionHandler.java 84.97% -1.95% 🍏
NotificationDispatcher.java 83.87% 🍏
EmailNotificationStrategy.java 77.66% -6.22% 🍏

@babltiga
babltiga merged commit 34e5c4b into main Sep 25, 2026
55 of 57 checks passed
@babltiga
babltiga deleted the feature/AF-942-per-user-data-volume-budgets branch September 25, 2026 14:21
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.

proxy: per-user data-volume budgets

1 participant