feat(AF-613): dynamic variables for API connectors#619
Merged
Conversation
Contributor
Contributor
Contributor
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
force-pushed
the
feature/AF-613-api-connector-dynamic-variables
branch
from
July 20, 2026 11:46
5382d96 to
8a8b078
Compare
Contributor
Coverage Report for Frontend Coverage (frontend)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 afterbuildHeaders— 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 finishedAuthorizationvalue, 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).executeInlineand 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— overHEX/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 byDefaultApiConnectorVariableResolutionServiceTestandApiConnectorVariablesIntegrationTest(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.base_url(SSRF pivot),BINARYbodies andFILEform parts (base64 — substitution corrupts).targetisheader:<Name>/query:<name>only. No whole-body form; partial-body injection needs a JSON pointer, which is a separate feature.sort_order, deviating from the AF-518ORDER BY created_atidiom: masking policies are order-insensitive, variable evaluation order is observable, andCURRENT_TIMESTAMPis transaction-constant in Postgres so same-transaction rows tie.api_requests.error_messageis persisted and shown to reviewers, and the JDK'sIOExceptionembeds the substituted URI (possibly carrying aquery:-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
overridableand given a value per request:can_override_variablesgrant, OR-merged likecan_break_glassand never conferred by a JIT access grant;CHECKconstraint, so it survives a manual data fix;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, sogroupBuilder.tsstrips 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
canOverrideVariablesis 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_UPDATEDaudit rows make it traceable.Verification
mvn clean test— 5332 tests, 0 failures, incl.ApplicationModulesTest,ApiPackageDependencyTest,MessagesParityTest. (OnepgvectorTestcontainers startup flake inQuerySnapshotRepositoryIntegrationTest— unrelated workflow-module test, 6/6 green in isolation.)api-connector-variables.spec.ts. (One unrelated flake ineditor-query-templates.spec.tsunder 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 generatedid="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
docs/03-data-model.mdapi_connector_variablestable,api_requests.variable_overrides,can_override_variableson both permission tablesdocs/04-api-spec.md### Connector variables (AF-613),variableOverrideson the submit row, group-item exclusion notedocs/05-backend.md**Dynamic variables (AF-613)**sub-section + pipeline prosedocs/06-frontend.mddocs/07-security.mddocs/09-deployment.md,CLAUDE.mdACCESSFLOW_APIGOV_MAX_VARIABLE_VALUE_BYTESdocs/12-roadmap.mddocs/17-api-governance.md## 6. Dynamic variables (AF-613)+ audit actionsREADME.mdwebsite/index.htmlwebsite/docs/index.html#cfg-api-connectorswebsite/README.mdOpen 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.