Skip to content

feat(AF-613): dynamic variables for API connectors#619

Merged
babltiga merged 1 commit into
mainfrom
feature/AF-613-api-connector-dynamic-variables
Jul 20, 2026
Merged

feat(AF-613): dynamic variables for API connectors#619
babltiga merged 1 commit into
mainfrom
feature/AF-613-api-connector-dynamic-variables

Conversation

@babltiga

Copy link
Copy Markdown
Contributor

Closes #613

Why

An API connector could only send static values, which ruled out every API whose contract requires a value computed per request — HMAC request signing, nonces, timestamps, correlation ids, idempotency keys. A submitter cannot hand-compute those for a governed request: the signature covers a body the reviewer saw minutes or hours earlier, and any timestamp would be stale by execution time.

What

A connector now declares named variables (api_connector_variables) that are evaluated at execution time and substituted into header values, the path, query values and the body via {{name}} placeholders.

Where it runs. Inside ApiExecutionService.executeCall, deliberately after buildHeaders — so the evaluation context already carries the connector defaults, per-request headers, trace headers and the auth header the applier just computed, including a freshly minted OAuth2 bearer. An expression can therefore sign the finished Authorization value, which the motivating vendor scheme requires. The OAuth2 401 retry re-resolves from scratch (a nonce must not be replayed; a signature over the stale token would only fail again). executeInline and break-glass route through the same seam.

Kinds. CONSTANT, UUID, TIMESTAMP, EPOCH_MILLIS, RANDOM_HEX, HASH (SHA-256/MD5), HMAC (HMAC-SHA256/512 with an AES-256-GCM-encrypted shared key), ENCODE — over HEX / BASE64 / BASE64URL (unpadded, RFC 7515).

The motivating example works end to end. signature = kind=HMAC, expression={{request.headers.Authorization}}{{request.body}}; the body carries "HMAC": "{{signature}}"; the digest is substituted back in. Covered by DefaultApiConnectorVariableResolutionServiceTest and ApiConnectorVariablesIntegrationTest (real Postgres, real AES decryption).

Design decisions worth a reviewer's attention

  • {{request.*}} is the request before substitution. The vendor signs a body that still contains its own {{signature}} placeholder; resolving post-substitution would silently produce a digest the vendor rejects.
  • Single-pass rendering. A resolved value is never re-scanned. This is the containment property behind overrides, not a perf choice — tested as a named security property.
  • Not substituted, on purpose: header names and query keys (a variable-named header can't be reviewed), base_url (SSRF pivot), BINARY bodies and FILE form parts (base64 — substitution corrupts).
  • target is header:<Name> / query:<name> only. No whole-body form; partial-body injection needs a JSON pointer, which is a separate feature.
  • Explicit sort_order, deviating from the AF-518 ORDER BY created_at idiom: masking policies are order-insensitive, variable evaluation order is observable, and CURRENT_TIMESTAMP is transaction-constant in Postgres so same-transaction rows tie.
  • Cycles and dangling refs are 422s at save time, not failed runs after approval.
  • Leak fix included: api_requests.error_message is persisted and shown to reviewers, and the JDK's IOException embeds the substituted URI (possibly carrying a query:-targeted signature). Resolved values are scrubbed before the message escapes.

Also implements the issue's out-of-scope item

Per-request overrides, as requested. A variable may be marked overridable and given a value per request:

  • gated by a new per-connector can_override_variables grant, OR-merged like can_break_glass and never conferred by a JIT access grant;
  • a secret-bearing variable can never be overridable — service check plus a DB CHECK constraint, so it survives a manual data fix;
  • an override is an opaque literal, never rendered as a template, so it cannot expand into another variable's value;
  • names outside the overridable set are rejected with a uniform message so the set can't be enumerated; values are bounded and CR/LF/NUL-rejected (request splitting) at submit time;
  • overrides are persisted and shown to reviewers, so an approval covers exactly what will execute. The resolved outputs (nonce, signature) are never surfaced.

Request groups are explicitly out of scope — connector variables still resolve for a grouped member, but the group-item wire shape carries no variable_overrides, so groupBuilder.ts strips it rather than sending a field the server would silently drop, and the composer hides the tab there.

No preview endpoint — it would be a signing oracle. Admins validate through the existing inline "try it" path, which now routes through the resolver for free.

Backward compatibility

canOverrideVariables is boxed on the four permission request records: Jackson 3 rejects an absent primitive boolean with a 500, which would have broken every client predating this change (the e2e helper caught exactly this). Regression tests added.

Accepted gap

Config TOCTOU — a reviewer approves, an admin edits the variables, the scheduled run uses the new config. The identical gap already exists for default_headers, masking policies and auth credentials, so it is documented rather than fixed here alone; API_CONNECTOR_VARIABLE_UPDATED audit rows make it traceable.

Verification

  • Backend: mvn clean test — 5332 tests, 0 failures, incl. ApplicationModulesTest, ApiPackageDependencyTest, MessagesParityTest. (One pgvector Testcontainers startup flake in QuerySnapshotRepositoryIntegrationTest — unrelated workflow-module test, 6/6 green in isolation.)
  • Frontend: lint (0 errors, warnings unchanged from base), typecheck, 1277 tests, coverage 91.8% stmts / 83.8% branches / 93.8% lines, production build.
  • E2E: full suite on a clean stack — 244 passed, incl. the new api-connector-variables.spec.ts. (One unrelated flake in editor-query-templates.spec.ts under full-suite load; passes in isolation and touches only the SQL editor.)

Fixed along the way: the variables modal namespaces its AntD form ids (name="connectorVariable") — without it the generated id="name" collided with the Configuration tab's own field, breaking every <label for> association in the dialog. Found by the e2e run.

Docs & website updated

File Change
docs/03-data-model.md api_connector_variables table, api_requests.variable_overrides, can_override_variables on both permission tables
docs/04-api-spec.md ### Connector variables (AF-613), variableOverrides on the submit row, group-item exclusion note
docs/05-backend.md **Dynamic variables (AF-613)** sub-section + pipeline prose
docs/06-frontend.md Variables tab, composer tab, review surfaces
docs/07-security.md Secret handling, redaction, non-recursion, CRLF, override RBAC, TOCTOU
docs/09-deployment.md, CLAUDE.md ACCESSFLOW_APIGOV_MAX_VARIABLE_VALUE_BYTES
docs/12-roadmap.md AF-613 entry
docs/17-api-governance.md ## 6. Dynamic variables (AF-613) + audit actions
README.md apigov feature list
website/index.html "API Access Governance" tile
website/docs/index.html "Dynamic variables (request signing)" + "Per-request overrides" under #cfg-api-connectors
website/README.md content-source map row

Open risk to confirm before closing

{{request.body}} resolving to the pre-substitution body means the vendor cannot reproduce the signature from what it receives unless its documented scheme does the same placeholder dance. The spec quoted in the issue does exactly that — but it's an unusual scheme, and it's the most likely source of a "passes tests, fails against the real vendor" outcome. Worth verifying against the live vendor before closing.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Frontend Test Results

1 277 tests  +19   1 277 ✅ +19   4m 20s ⏱️ +2s
  168 suites + 2       0 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 8a8b078. ± Comparison against base commit 1a344de.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Backend Test Results

5 332 tests  +184   5 332 ✅ +184   17m 10s ⏱️ +6s
  669 suites +  9       0 💤 ±  0 
  669 files   +  9       0 ❌ ±  0 

Results for commit 8a8b078. ± Comparison against base commit 1a344de.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Backend Code Coverage

Overall Project 93.23% -0.14% 🍏
Files changed 94.52% 🍏

File Coverage
ApiVariableTargets.java 100% 🍏
DefaultApiConnectorPermissionLookupService.java 100% 🍏
ApiVariableTemplate.java 100% 🍏
ApiVariableEvaluationException.java 100% 🍏
ApiVariableGraph.java 100% 🍏
AccessGrantMaterializer.java 100% 🍏
ApigovRequestProperties.java 100% 🍏
ApiConnectorVariableEntity.java 100% 🍏
ApiRequestEntity.java 100% 🍏
AuditAction.java 100% 🍏
ApiConnectorVariableResponse.java 100% 🍏
ApiConnectorGroupPermissionResponse.java 100% 🍏
ApiConnectorVariableController.java 100% 🍏
UpdateApiConnectorVariableRequest.java 100% 🍏
CreateApiConnectorVariableRequest.java 100% 🍏
SubmitApiRequestRequest.java 100% 🍏
ApiConnectorVariableListResponse.java 100% 🍏
UpdateApiConnectorGroupPermissionRequest.java 100% 🍏
ApiRequestResponse.java 100% 🍏
ReorderApiConnectorVariablesRequest.java 100% 🍏
ApiConnectorPermissionResponse.java 100% 🍏
GrantApiConnectorGroupPermissionRequest.java 100% 🍏
UpdateApiConnectorPermissionRequest.java 100% 🍏
GrantApiConnectorPermissionRequest.java 100% 🍏
GrantApiConnectorGroupPermissionCommand.java 100% 🍏
IllegalApiConnectorVariableException.java 100% 🍏
ApiVariableTargetType.java 100% 🍏
ApiConnectorGroupPermissionView.java 100% 🍏
ApiConnectorPermissionLookupService.java 100% 🍏
ApiVariableAlgorithm.java 100% 🍏
ApiVariableEncoding.java 100% 🍏
UpdateApiConnectorPermissionCommand.java 100% 🍏
GrantApiConnectorPermissionCommand.java 100% 🍏
ApiConnectorVariableSummaryView.java 100% 🍏
ApiVariableKind.java 100% 🍏
ApiConnectorVariableView.java 100% 🍏
ApiConnectorVariableNotFoundException.java 100% 🍏
ApiRequestView.java 100% 🍏
ApiReviewService.java 100% 🍏
ApiConnectorPermissionView.java 100% 🍏
CreateApiConnectorVariableCommand.java 100% 🍏
UpdateApiConnectorVariableCommand.java 100% 🍏
SubmitApiRequestCommand.java 100% 🍏
UpdateApiConnectorGroupPermissionCommand.java 100% 🍏
ApiRequestVariableSubstitution.java 98.21% -1.79% 🍏
DefaultApiConnectorVariableAdminService.java 97.4% -2.6% 🍏
EffectiveApiConnectorPermissionResolver.java 96.39% 🍏
DefaultApiConnectorVariableResolutionService.java 93.41% -6.59% 🍏
ApiExecutionService.java 93.13% 🍏
ResolvedApiVariables.java 92.98% -7.02% 🍏
ApiConnectorController.java 91.82% -8.18% 🍏
ApiVariableEvaluator.java 91.69% -8.31% 🍏
DefaultApiConnectorAdminService.java 89.43% 🍏
ReorderApiConnectorVariablesCommand.java 84.62% -15.38% 🍏
DefaultApiReviewService.java 84.3% -5.23% 🍏
ApiVariableRequestContext.java 79.59% -20.41% 🍏
DefaultApiRequestService.java 74.4% -0.58% 🍏
ApiGovExceptionHandler.java 46.46% -6.57% 🍏
PendingApiReviewResponse.java 30.51% -3.39% 🍏
ApiConnectorVariableSummaryResponse.java 0% 🍏
ApiConnectorVariableSummaryListResponse.java 0% 🍏

An API connector could only send static values, which ruled out every API
whose contract requires a value computed per request — HMAC request signing,
nonces, timestamps, correlation ids, idempotency keys. A submitter cannot
hand-compute those for a governed request: the signature covers a body the
reviewer saw hours earlier, and the timestamp would be stale by execution.

A connector now declares named variables substituted into headers, path,
query and body via {{name}} placeholders. They resolve after auth headers
are built — including a freshly minted OAuth2 token — so an expression can
sign the finished Authorization value, and {{request.*}} describes the
request *before* substitution, so a body being signed still carries its own
placeholder. That is what the motivating vendor scheme requires.

Evaluation is template substitution over a fixed function set only: no
scripting engine, mirroring the engine plugins' rejection of $where and
Painless. Rendering is single-pass, so a resolved value is never re-scanned.
Cycles and dangling references are 422s at save time, not failed runs after
approval. Resolved values are never persisted, snapshotted or logged — and
are scrubbed from upstream failure messages, since the JDK's IOException
embeds the substituted URI into the reviewer-visible error_message.

Also implements the issue's out-of-scope per-request overrides: a variable
may be marked overridable and given a value per request, gated by a new
per-connector can_override_variables grant. A secret-bearing variable can
never be overridable (service check plus a DB CHECK), an override is an
opaque literal that cannot expand into another variable's value, and
overrides are persisted and shown to reviewers so an approval covers exactly
what executes. Grouped requests deliberately do not accept them.

The new grant flag is boxed on the wire so clients predating it still work —
an absent primitive boolean 500s under Jackson 3.
@babltiga
babltiga force-pushed the feature/AF-613-api-connector-dynamic-variables branch from 5382d96 to 8a8b078 Compare July 20, 2026 11:46
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Frontend Coverage (frontend)

Status Category Percentage Covered / Total
🟢 Lines 93.78% (🎯 90%) 2069 / 2206
🟢 Statements 91.76% (🎯 90%) 2307 / 2514
🟢 Functions 90.96% (🎯 90%) 634 / 697
🟢 Branches 83.82% (🎯 80%) 1296 / 1546
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
frontend/src/pages/requestGroups/groupBuilder.ts 100% 95.45% 100% 100%
frontend/src/utils/apiRequestComposition.ts 96% 93.75% 92.3% 100% 104
frontend/src/utils/enumLabels.ts 96.52% 100% 90.56% 96.5% 192, 200, 319, 443, 501
Generated in workflow #798 for commit 8a8b078 by the Vitest Coverage Report Action

@babltiga
babltiga merged commit edc5666 into main Jul 20, 2026
28 checks passed
@babltiga
babltiga deleted the feature/AF-613-api-connector-dynamic-variables branch July 20, 2026 12:11
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.

apigov: dynamic variables for API connectors (per-request HMAC signing, nonces, timestamps)

1 participant