Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ com.bablsoft.accessflow/
| `ACCESSFLOW_APIGOV_REVIEW_TIMEOUT` | ISO-8601 duration. How long an API request may sit in `PENDING_REVIEW` before `ApiRequestTimeoutJob` auto-rejects it (default `PT24H`). |
| `ACCESSFLOW_APIGOV_MAX_REQUEST_BODY_BYTES` | Cap on the total encoded size of a submitted API request body (#517) — raw text, x-www-form-urlencoded, or the base64-decoded size of form-data / binary file parts (files ride inline as bounded base64 since AccessFlow has no object storage). Exceeding it rejects the submission with HTTP 422. Default `5242880` (5 MiB). Binds `accessflow.apigov.max-request-body-bytes`. |
| `ACCESSFLOW_APIGOV_MAX_RESPONSE_BYTES` | System-wide hard ceiling (#521) on a stored — and therefore downloadable — API response body; the absolute backstop above any per-connector `max_response_bytes` (effective cap is the **min** of the two). The body lives in a Postgres `text` column and is read fully into JVM memory during the call. Default `10485760` (10 MiB). Binds `accessflow.apigov.max-response-bytes`. |
| `ACCESSFLOW_APIGOV_MAX_VARIABLE_VALUE_BYTES` | Cap on a single resolved connector dynamic-variable value (AF-613). A digest or nonce is tens of bytes, so a larger value means a runaway expression or an abusive per-request override; exceeding it fails the execution, and a submitted override over the cap is rejected with HTTP 422 at submit time. Default `8192`. Binds `accessflow.apigov.max-variable-value-bytes`. |
| `ACCESSFLOW_APIGOV_RESPONSE_PREVIEW_BYTES` | How much of the stored response snapshot is embedded inline in the API-request **detail** view (#521); beyond it the detail sets `response_snapshot_preview_truncated=true` and the full body is fetched via `GET /api-requests/{id}/response`. Default `65536` (64 KiB). Binds `accessflow.apigov.response-preview-bytes`. |
| `ACCESSFLOW_APIGOV_OAUTH2_TOKEN_CACHE_SKEW` | ISO-8601 duration (#506). Safety skew subtracted from a fetched outbound-OAuth2 token's `expires_in` before `ConnectorOAuth2TokenService` caches it in Redis (`apigov:oauth2:token:<connectorId>`) (default `PT30S`). Binds `accessflow.apigov.oauth2-token-cache-skew`. |
| `ACCESSFLOW_APIGOV_OAUTH2_TOKEN_REQUEST_TIMEOUT` | ISO-8601 duration (#506). Connect/read timeout for the `apigovOAuth2RestClient` posting to a connector's OAuth2 token endpoint (default `PT10S`). Binds `accessflow.apigov.oauth2-token-request-timeout`. |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ A glance at the day-to-day flows engineers and approvers actually use.
- **Elasticsearch & OpenSearch (NoSQL search)** — first-class search connectors, shipped as the fifth on-demand engine plugin (`engines/elasticsearch/`, low-level REST client); the same plugin JAR serves both **Elasticsearch** and the wire-compatible **OpenSearch**. Users write a JSON query envelope (`{"search":"logs-*","query":{…}}`, plus `count`, `get`/`mget`, `index`/`bulk`, `update_by_query`/`delete_by_query`, and index management) classified onto the same approval workflow; row-level security injects `bool.filter` clauses on keyword fields (fail-closed on writes into a policied index), and field masking applies recursively to `_source` fields including nested dot-paths. Server-side scripting (`script`, `runtime_mappings`, Painless) and cluster/system-index APIs are rejected up front. Each datasource authenticates with basic auth or an API key.
- **Amazon DynamoDB (NoSQL key-value)** — a first-class key-value connector speaking **PartiQL**, shipped as the sixth on-demand engine plugin (`engines/dynamodb/`, AWS SDK for Java v2 over the url-connection HTTP client — no Netty). It is the first engine whose connection is **cloud credentials + region** rather than host/port: the region, access key id, secret access key, and an optional custom endpoint (DynamoDB Local / VPC) map onto the existing datasource fields. PartiQL SELECT / INSERT / UPDATE / DELETE classify onto the same approval workflow, and table management (`CreateTable`/`DeleteTable`/`UpdateTable`) arrives as a JSON command document; transaction/batch statements are rejected. Row-level security splices predicates into the PartiQL WHERE clause (positional parameters, any attribute), failing closed on INSERT-into-policied and deny-all, and field masking applies recursively by dot-path including nested maps/lists.
- **Neo4j (NoSQL graph)** — a first-class graph connector speaking **Cypher** over Bolt, shipped as the seventh on-demand engine plugin (`engines/neo4j/`, native Neo4j Java driver). Cypher is clause-based, so the query type is the strongest write clause present (DELETE/REMOVE → DELETE, CREATE/MERGE → INSERT, SET → UPDATE, else a `MATCH … RETURN` / `SHOW` read → SELECT), with index/constraint/database/role schema commands as DDL; `LOAD CSV`, procedure calls outside a read-only allow-list, and multi-statement input are rejected up front. Row-level security ANDs property predicates onto each `MATCH`'s `WHERE` (Cypher named parameters, node-label policies), failing closed on anonymous / write-creates-policied-label shapes; field masking is label-aware and recursive. Connection is host/port + database with the SSL mode encoded in the Bolt scheme, **or** a full `bolt://` / `neo4j+s://` URI (Neo4j Aura / clustered routing).
- **API Access Governance (AF-500)** — govern outbound **API** calls (REST / SOAP / GraphQL / gRPC), not just databases. An admin registers an **API connector** (URL + auth + protocol + admin-defined default headers; secrets AES-256-GCM encrypted) — including **OAuth2 with automatic token fetch, caching & refresh** (client-credentials / refresh-token / resource-owner password), so no bearer token is pasted by hand — uploads its schema (OpenAPI / WSDL / GraphQL SDL / gRPC proto, by paste, file upload, or URL fetch) which is parsed into a normalized operation catalog with read/write classification, and shares governed connectivity with the team via per-user permissions. Users **compose calls like Postman** — query params, custom headers, raw / form-data / x-www-form-urlencoded / binary file bodies. Every call flows through the same machinery as a database query: AI risk scoring, attribute-based routing, multi-stage human approval (no self-approval), **connector-level response masking** (policies targeting a schema field, JSON path, XML/XPath, or regex — with a masking strategy and role / group / user reveal scoping) and **data-classification tags** (PII/PCI/PHI/GDPR/FINANCIAL/SENSITIVE that auto-derive a masking policy and raise the AI risk — AF-518), immutable response snapshots (downloadable in full), W3C trace-context propagation (filterable by trace/span id), break-glass, scheduled execution, and natural-language **text-to-API**. See [`docs/17-api-governance.md`](https://github.com/bablsoft/accessflow/blob/main/docs/17-api-governance.md).
- **API Access Governance (AF-500)** — govern outbound **API** calls (REST / SOAP / GraphQL / gRPC), not just databases. An admin registers an **API connector** (URL + auth + protocol + admin-defined default headers; secrets AES-256-GCM encrypted) — including **OAuth2 with automatic token fetch, caching & refresh** (client-credentials / refresh-token / resource-owner password), so no bearer token is pasted by hand — uploads its schema (OpenAPI / WSDL / GraphQL SDL / gRPC proto, by paste, file upload, or URL fetch) which is parsed into a normalized operation catalog with read/write classification, and shares governed connectivity with the team via per-user permissions. Users **compose calls like Postman** — query params, custom headers, raw / form-data / x-www-form-urlencoded / binary file bodies. Every call flows through the same machinery as a database query: AI risk scoring, attribute-based routing, multi-stage human approval (no self-approval), **connector-level response masking** (policies targeting a schema field, JSON path, XML/XPath, or regex — with a masking strategy and role / group / user reveal scoping) and **data-classification tags** (PII/PCI/PHI/GDPR/FINANCIAL/SENSITIVE that auto-derive a masking policy and raise the AI risk — AF-518), immutable response snapshots (downloadable in full), W3C trace-context propagation (filterable by trace/span id), **dynamic variables** that compute a value per request and substitute it into headers, path, query or body via `{{name}}` placeholders — HMAC request signing, nonces, timestamps, idempotency keys — resolved *after* authentication so a signature can cover the freshly minted token, with opt-in per-request overrides for requesters who hold the grant (AF-613), break-glass, scheduled execution, and natural-language **text-to-API**. See [`docs/17-api-governance.md`](https://github.com/bablsoft/accessflow/blob/main/docs/17-api-governance.md).
- **Request chaining & grouping (AF-501)** — bundle several steps into one **grouped request** reviewed and approved as a single element, then executed as an **ordered sequence**. Members can mix **queries** across different datasources and **API calls** against governed connectors; a builder lets you add steps, drag-reorder them, and author each one in a **full-parity editing drawer** — query steps get the complete Query Editor surface (schema autocomplete, AI analyze, dry-run, text-to-SQL, templates), API steps the complete API Editor surface (operation picker, request composer, AI analyze, text-to-API) — with an aggregate risk badge, and saved **drafts re-open for editing** without losing the composed request. Bundling never weakens a member's policy: each member is validated against your permission for its target (break-glass groups require `can_break_glass` on **every** target), the required approvers are the **union** across all member plans, and the group is approved only when **every** member plan is satisfied — the submitter can never approve their own group. On execute, members run in order; on the first failure the run stops and the rest are skipped (`continue-on-error` runs them all instead). There is **no distributed rollback** — an approved group is not atomic, already-applied members stay, and this is surfaced clearly. Each member records its own snapshot + audit row alongside group-level audit and live WebSocket progress.
- **Data Lifecycle Manager (AF-499, AF-519)** — govern not just *who reads* data, but *when it is retired*. Admins define **retention/erasure rules** per datasource — target a table / column set / classification tag with a retention window plus **arbitrary conditions** (a structured, parameter-bound predicate builder and a JSqlParser-validated raw-`WHERE` escape hatch) and an action (**hard-delete**, **soft-delete**, or **pseudonymize**) — with an optional **cron schedule**, a clustered scan job, a **dry-run preview**, and **automatic execution** through the proxy. Any user can file a GDPR/CCPA **right-to-erasure** request with the *same* rich configuration; it flows through AI-assisted scope detection and **review-plan-based peer review** (REVIEWER-eligible, multi-stage, no self-approval, auto-reject on timeout) before executing. Enforcement is transparent: soft-deleted rows vanish from reads, `DELETE`s become marker updates, and aged PII resolves to an irreversible **salted hash** at read time — so aggregates survive while the PII does not — all with tamper-evident **proof-of-deletion** audit records and a retention-adherence compliance report.
- **Deploy anywhere** — `docker compose up` for local and small environments; Helm chart for Kubernetes production.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ private void materializeConnectorGrant(AccessGrantRequestEntity entity, UUID app
Instant expiresAt) {
requireNoStandingConnectorPermission(entity);
// grantPermission upserts on (connector_id, user_id), so an existing time-boxed row is
// replaced in place. Break-glass and response-field restrictions are never JIT-grantable.
// replaced in place. Break-glass, variable overrides (AF-613) and response-field
// restrictions are never JIT-grantable — each is an explicit admin decision per connector.
var command = new GrantApiConnectorPermissionCommand(
entity.getRequesterId(),
entity.isCanRead(),
entity.isCanWrite(),
false,
false,
expiresAt,
toList(entity.getAllowedOperations()),
null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public record ApiConnectorGroupPermissionView(
boolean canRead,
boolean canWrite,
boolean canBreakGlass,
boolean canOverrideVariables,
Instant expiresAt,
List<String> allowedOperations,
List<String> restrictedResponseFields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ record ApiConnectorPermissionLookupView(
boolean canRead,
boolean canWrite,
boolean canBreakGlass,
boolean canOverrideVariables,
List<String> allowedOperations,
Instant expiresAt) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public record ApiConnectorPermissionView(
boolean canRead,
boolean canWrite,
boolean canBreakGlass,
boolean canOverrideVariables,
Instant expiresAt,
List<String> allowedOperations,
List<String> restrictedResponseFields,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.bablsoft.accessflow.apigov.api;

import java.util.List;
import java.util.UUID;

/**
* Admin CRUD for per-connector dynamic variables (AF-613). All methods are organization-scoped: the
* connector must belong to {@code organizationId}, otherwise an {@link ApiConnectorNotFoundException}
* is thrown — so a cross-org id is indistinguishable from a missing one.
*
* <p>Validation is authoritative at save time, not execution time. Every mutation re-materializes
* the connector's full variable set with the candidate applied and rejects a dependency cycle, a
* reference to an unknown variable, or a duplicate name or injection target. {@link #delete} runs
* the inverse check and refuses to remove a variable another one still references.
*/
public interface ApiConnectorVariableAdminService {

List<ApiConnectorVariableView> listForConnector(UUID connectorId, UUID organizationId);

ApiConnectorVariableView create(UUID connectorId, UUID organizationId,
CreateApiConnectorVariableCommand command);

ApiConnectorVariableView update(UUID variableId, UUID connectorId, UUID organizationId,
UpdateApiConnectorVariableCommand command);

void delete(UUID variableId, UUID connectorId, UUID organizationId);

List<ApiConnectorVariableView> reorder(UUID connectorId, UUID organizationId,
ReorderApiConnectorVariablesCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.bablsoft.accessflow.apigov.api;

import java.util.List;
import java.util.Set;
import java.util.UUID;

/**
* Submitter-facing reads over a connector's variables (AF-613) — what the request composer needs to
* offer overrides, and what the submit path needs to validate them. Carries no secret, no
* expression, and no algorithm.
*/
public interface ApiConnectorVariableLookupService {

/** Every variable on the connector, projected to the submitter-safe fields. */
List<ApiConnectorVariableSummaryView> summariesForConnector(UUID connectorId, UUID organizationId);

/** The names a submitter is allowed to override. Anything outside this set is rejected. */
Set<String> overridableNames(UUID connectorId, UUID organizationId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.bablsoft.accessflow.apigov.api;

import java.util.UUID;

public class ApiConnectorVariableNotFoundException extends ApiGovException {

public ApiConnectorVariableNotFoundException(UUID variableId) {
super("API connector variable not found: " + variableId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.bablsoft.accessflow.apigov.api;

import java.util.Map;
import java.util.UUID;

/**
* Evaluates a connector's dynamic variables for one outbound call (AF-613).
*
* <p>Called from the execution path <em>after</em> auth headers have been resolved — including a
* freshly minted OAuth2 bearer token — so an expression may sign the finished {@code Authorization}
* header. Variables are evaluated in dependency order over the DAG formed by their
* {@code {{var.x}}} references.
*
* <p>{@code overrides} carries per-request submitter-supplied values for variables marked
* overridable. An override <strong>replaces</strong> the value entirely — the kind, algorithm and
* encoding are not re-applied — and is inserted as an opaque literal that is never itself expanded
* as a template. Variables that depend on an overridden one still recompute over the new value.
* Names not marked overridable are ignored here; the submit path rejects them up front.
*/
public interface ApiConnectorVariableResolutionService {

ResolvedApiVariables resolve(UUID organizationId, UUID connectorId,
ApiVariableRequestContext context, Map<String, String> overrides);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.bablsoft.accessflow.apigov.api;

/**
* The submitter-visible projection of a connector variable (AF-613): enough to compose a request
* that references {@code {{name}}} and to offer an override, and nothing more.
*
* <p>Deliberately omits {@code expression}, {@code algorithm}, {@code encoding} and any hint of a
* stored secret — a submitter may reference a signature variable but must never learn how it is
* computed or what key backs it.
*/
public record ApiConnectorVariableSummaryView(
String name,
ApiVariableKind kind,
String description,
boolean overridable) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.bablsoft.accessflow.apigov.api;

import java.time.Instant;
import java.util.UUID;

/**
* Admin-facing view of a connector variable (AF-613). The stored secret is never exposed — only
* {@code hasSecret} reports whether one is configured, mirroring the connector credential rule.
*/
public record ApiConnectorVariableView(
UUID id,
UUID connectorId,
String name,
ApiVariableKind kind,
String expression,
ApiVariableAlgorithm algorithm,
ApiVariableEncoding encoding,
boolean hasSecret,
String target,
boolean overridable,
String description,
int sortOrder,
Instant createdAt,
Instant updatedAt) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
Expand Down Expand Up @@ -35,6 +36,12 @@ public record ApiRequestView(
Integer aiRiskScore,
String aiSummary,
ApiBodyType bodyType,
/**
* AF-613: the submitter-supplied variable overrides, so a reviewer approves exactly what
* will execute. These are the signing <em>inputs</em>; the resolved outputs (nonce,
* signature) are execution-time only and are never surfaced.
*/
Map<String, String> variableOverrides,
Instant scheduledFor,
String traceId,
String spanId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ record PendingApiReview(
UUID apiRequestId, UUID connectorId, String connectorName, UUID submittedByUserId,
String verb, String requestPath, boolean write, String justification, UUID aiAnalysisId,
RiskLevel aiRiskLevel, Integer aiRiskScore, String aiSummary, int currentStage,
/**
* AF-613: how many connector variables this submitter overrode. A count rather than
* the values, so the queue can badge affected requests without joining the body; the
* values themselves are on the detail view.
*/
int variableOverrideCount,
Instant createdAt) {
}

Expand Down
Loading
Loading