Skip to content

feat(AF-938): record the calling application on requests and audit rows - #1096

Merged
babltiga merged 4 commits into
mainfrom
feature/AF-938-application-name
Sep 24, 2026
Merged

babltiga merged 4 commits into
mainfrom
feature/AF-938-application-name

Conversation

@babltiga

Copy link
Copy Markdown
Contributor

Closes #938

What

Records which application submitted a request, so an audit row can say "this came from the reporting service" rather than only "a Java HTTP client". This is for identification and audit only: it never grants or blocks access.

  • Two sources with different trust.
    • API key (trusted): a key can carry an optional application_name, set on POST /me/api-keys and the service-account issue/rotate endpoints. A rotation inherits the old key's name unless a new one is given. The name cannot be forged without the key, and it always takes precedence over the header.
    • Header (untrusted): an X-AccessFlow-Application request header is used when the key has no name or the caller signs in with a JWT. The caller controls it, so it is recorded with source HEADER and labelled Untrusted in the UI. The value is trimmed and dropped if it contains control characters. It is cut to 100 code points, so a two-part character like an emoji is never split.
  • Where the name is stored:
    • On query_requests as application_name + application_name_source (V189). It is set by all four submission paths: REST submit, break-glass, replay and MCP submit_query. Recurring runs copy it from their series.
    • In the metadata of every audit row the request writes, via a new security.internal.ApplicationAuditMetadataContributor. audit_log gets no new column, because adding one would change the hash-chain input for every existing row (same approach as V176). This is also why other request types, such as API calls and deployments, can be filtered on the audit log without a column of their own.
  • Filters: application_name on GET /queries (and its CSV export), applicationName on GET /admin/audit-log (and its CSV export). The export's own audit row records the filter as filter_application_name, so it does not claim the exporter's application.
  • Audit sinks: application_name / application_name_source are now top-level fields in the canonical event (Splunk / HTTPS batch / S3) and appear as cs5 / cs6 in CEF. The docs note these two fields are copied from metadata and are not part of the hash-chain input.
  • UI:
    • A new ClientApplicationTag shows the name, with an Untrusted tag for header values. It appears on query detail and on audit rows and the audit detail drawer.
    • An Application filter on the query list and the audit log.
    • An Application name field and column on personal and service-account keys.
  • Not in scope: a routing operand for this field. docs/07-security.md records that any future operand must fail closed on a header source, like cicd_origin does.

Docs / website updated

  • docs/03-data-model.md, docs/04-api-spec.md, docs/05-backend.md, docs/06-frontend.md, docs/07-security.md, docs/13-mcp.md
  • README.md (audit log feature line)
  • website/docs/configuration/audit-compliance/index.html: new Which application made a request? subsection (#cfg-audit-application), the filter list and the sinks sentence
  • website/docs/configuration/users-roles/index.html: service-account issue/rotate steps
  • website/README.md: content-source map row
  • help-corpus/ regenerated
  • docs/09-deployment.md: no change, since no configuration setting was added

Verification

  • Backend: full mvn clean verify -Pcoverage on the final tree: 10,309 tests, 0 failures. Includes ApplicationModulesTest, ApiPackageDependencyTest, MessagesParityTest, Spotless and Checkstyle.
  • Frontend: lint (0 errors), typecheck, build, and test:coverage (2,640 tests; 94.6% lines / 87.7% branches) all pass. The last commit, a one-line CSS fix to the tag, was re-checked with lint, typecheck, build and the tag's own test; the full coverage suite was not re-run after it.
  • E2E: profile-api-keys, admin-audit-log, query-list and service-accounts ran against the e2e stack rebuilt from the final backend: 21/21 passed.
    • New tests: key with an application name; audit filter by key-sourced and header-sourced application (a named key beats a spoofed header); query-list application filter plus the untrusted marker on detail.
    • One existing spec needed column indexes shifted for the new Application column.
  • help-corpus drift check and the website tests pass.

Review notes

Independent reviewers ran before this PR. Fixed:

  • af-reviewer: V188 clashed with a V188 merged to main in the meantime. Rebased and renumbered to V189.
  • af-frontend-reviewer: the existing Expires-column e2e assertion was index-pinned. Shifted it.
  • af-java-reviewer + af-reviewer: a key name containing a control character was stored but dropped at read time, letting the header take over. All three key request records now carry @Pattern("[^\\p{Cntrl}]*") (400 on violation, with a new i18n key in all locales), and the forms have a matching rule.
  • af-java-reviewer: added tests that the application passes through DefaultQuerySubmissionService, DefaultBreakGlassService, DefaultQueryReplayService and MCP submitQuery. Truncation now counts code points. A blank export filter is no longer recorded.
  • af-frontend-reviewer: added tests for the service-account key issue form and column and for the audit drawer. Removed an unused enum-label helper and its keys.
  • af-content-reviewer: the new website subsection had swallowed the existing "Tune it." paragraph (and its help-corpus chunk). Moved it, and tightened the wording.

Still open:

  • af-reviewer (nit), not changed: suggested indexing (organization_id, application_name). query_requests has no organization_id column (org is reached through the datasource), so the index is a partial one on application_name alone.
  • af-java-reviewer (nit), documented: on rotate, an explicit empty string clears the application name instead of inheriting it; the API spec now says so.
  • Rows written after the request (by design): audit rows written after the request has finished, by after-commit listeners or scheduled jobs, carry no application. Rows written during the request, including system rows such as QUERY_REVIEW_REQUESTED when AI analysis is off, do carry it. This matches how the V176 on-behalf-of attribution works.
  • Existing flaky test: ApiKeysSection › "commits an expiry chosen from the panel" is flaky on main too (failed 1 of 4 baseline runs); it is not caused by this change.

Screenshots

API keys with the new Application column
Query list filtered by application
Query detail showing a header-supplied application marked Untrusted
Audit log rows with the calling application (trusted key vs untrusted header)
Audit event drawer with the application row

@github-actions

Copy link
Copy Markdown
Contributor

Frontend Test Results

    1 files    310 suites   7m 32s ⏱️
2 642 tests 2 642 ✅ 0 💤 0 ❌
2 643 runs  2 643 ✅ 0 💤 0 ❌

Results for commit 53bfb66.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Frontend Coverage (frontend)

Status Category Percentage Covered / Total
🟢 Lines 96.03% (🎯 90%) 3662 / 3813
🟢 Statements 94.66% (🎯 90%) 4066 / 4295
🟢 Functions 94.02% (🎯 90%) 1118 / 1189
🟢 Branches 87.68% (🎯 80%) 2435 / 2777
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
frontend/src/api/admin.ts 95.27% 86.04% 89.02% 94.5% 70, 76, 78, 88-89, 96, 120, 303, 338-339, 349
frontend/src/api/queries.ts 84% 91.66% 71.42% 81.25% 60, 64-66, 100, 134-142, 151-157
frontend/src/pages/admin/service-accounts/serviceAccountForm.ts 98.79% 96.84% 100% 100% 137
Generated in workflow #1409 for commit 53bfb66 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Backend Test Results

10 309 tests  +34   10 309 ✅ +34   10m 33s ⏱️ -50s
 1 145 suites + 3        0 💤 ± 0 
 1 145 files   + 3        0 ❌ ± 0 

Results for commit 53bfb66. ± Comparison against base commit fd005b4.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Code Coverage

Overall Project 94.75% -0.01% 🍏
Files changed 95.98% 🍏

File Coverage
ApiKeyAuthenticationToken.java 100% 🍏
DefaultQueryReplayService.java 100% 🍏
DefaultQuerySubmissionService.java 100% 🍏
DefaultRequestApplicationService.java 100% 🍏
ApplicationAuditMetadataContributor.java 100% 🍏
QueryRequestSpecifications.java 100% 🍏
ServiceAccountKeyResponse.java 100% 🍏
RotateServiceAccountKeyRequest.java 100% 🍏
IssueServiceAccountKeyRequest.java 100% 🍏
ServiceAccountKeyView.java 100% 🍏
IssueServiceAccountKeyCommand.java 100% 🍏
RotateServiceAccountKeyCommand.java 100% 🍏
ApiKeyView.java 100% 🍏
ApiKeyAuthentication.java 100% 🍏
ResolvedApiKey.java 100% 🍏
ApiKeyService.java 100% 🍏
BreakGlassController.java 100% 🍏
QueryListItem.java 100% 🍏
ApiKeyCreateRequest.java 100% 🍏
ApiKeyResponse.java 100% 🍏
ApiKeysController.java 100% 🍏
ApplicationNameSource.java 100% 🍏
ClientApplication.java 100% 🍏
QueryDetailView.java 100% 🍏
QueryListItemView.java 100% 🍏
QueryListFilter.java 100% 🍏
AuditLogQuery.java 100% 🍏
QuerySubmissionService.java 100% 🍏
AuditExportEvent.java 100% 🍏
DefaultApiKeyService.java 100% 🍏
CefFormatter.java 99.11% 🍏
AuditExportEventWriter.java 99.07% 🍏
DefaultServiceAccountAdminService.java 98.32% 🍏
ApiKeyAuthenticationFilter.java 96.21% 🍏
McpToolService.java 95.99% 🍏
DefaultQueryRequestPersistenceService.java 94.89% 🍏
AuditLogSpecifications.java 92.11% 🍏
QueryReadController.java 91.81% 🍏
QueryReplayController.java 90.53% 🍏
DefaultQueryRequestLookupService.java 89.23% 🍏
BreakGlassService.java 88.5% -11.5% 🍏
AdminAuditLogController.java 86.71% 🍏
QuerySubmissionController.java 85.39% 🍏
QueryDetailResponse.java 85.06% -1% 🍏
DefaultBreakGlassService.java 84.99% 🍏
SubmitQueryCommand.java 83.18% 🍏
QueryReplayService.java 68% -16% 🍏

@babltiga
babltiga merged commit cfae20e into main Sep 24, 2026
35 checks passed
@babltiga
babltiga deleted the feature/AF-938-application-name branch September 24, 2026 13:38
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.

audit: record the calling application on requests and audit rows

1 participant