plugin-api-manager
v1.0.5
Published
API proxy gateway with centralized payload encryption. Register inbound/outbound routes, forward requests with timeout/retry, and encrypt/decrypt payloads (AES-256-GCM or PGP via Crypto Toolkit) before they leave or enter the system.
Maintainers
Readme
plugin-api-manager
API proxy gateway with a centralized payload-encryption layer (a WSO2-style register/forward gateway, reduced to the essentials).
- Outbound: internal caller →
POST /api/apim/outbound/<routeName>→ payload encrypted → partner URL. - Inbound: partner →
POST /api/apim/inbound/<inboundPath>→ payload decrypted → internal backend URL.
Authentication
Every gateway request requires an X-API-Key header. Keys are created in the admin UI (Settings → API Manager → API Keys); the plaintext key is shown once and only its SHA-256 hash is stored. Scopes are inbound, outbound, or route-scoped inbound:<routeName> / outbound:<routeName> (a bare scope allows every route of that direction). Keys support expiry and revocation.
Authentication Modes
Each route has an authMode field controlling which credential types it accepts:
| Mode | API Key (X-API-Key) | App Bearer Token (Authorization) | ACL Check |
|------|--------------------------|-------------------------------------|-----------|
| both (default) | ✅ Scope check | ✅ Role ACL check | Bearer only |
| api-key | ✅ Scope check | ❌ Rejected (403) | None |
| role | ❌ Rejected (403) | ✅ Role ACL check | Role ACL |
API Key flow (plugin-managed): The gateway hashes the presented key via SHA-256, looks it up in apiManagerApiKeys, verifies expiry/revocation, and checks the route scope (inbound, outbound, or route-scoped inbound:<routeName> / outbound:<routeName>). API keys are never role-bound.
App Bearer Token flow (role-based): The gateway decodes the NocoBase JWT (same secret as login sessions and plugin-api-keys tokens), extracts the roleName from the token payload, and enforces apimRoutes:call:<routeName> ACL via the role's snippets. The role must be granted the per-route snippet (pm.plugin-api-manager.routes.<routeName>) or the all-routes snippet (pm.plugin-api-manager.routes). Configure grants in Settings → Roles.
Choosing a mode:
- Use
api-keywhen the caller is an external system that cannot manage app tokens. - Use
rolewhen the caller is a NocoBase user (or another service using the app JWT) and you want to enforce role-based access. - Use
bothwhen you want to support both credential types side by side (e.g., during migration).
Encryption per route
encryptionMode is one of none | aes-256-gcm | pgp | rsa-oaep; wireFormat is binary (raw container bytes) or json ({"container":"...","encoding":"base64","ciphertext":"...","contentType":"..."} envelope). The JSON envelope carries the plaintext content type so the decrypting side can restore it; the binary format has no place to store it, so decrypted payloads without a content type are sniffed from the first bytes ({/[ → JSON, < → XML, anything else → octet-stream). Decryption accepts both wire formats regardless of the route setting.
responseEncrypted defaults to true; set it to false when the other side sends plaintext responses (outbound then skips response decryption, inbound skips response encryption).
AES-256-GCM
Uses the Crypto Toolkit NCB1 container (AES-256-GCM). The shared secret is resolved in this order:
aesSecretEnvVar— name of an environment variable holding the secret.aesSecret— stored encrypted at rest (via the application AES encryptor) and masked in all admin API responses.
If the secret base64-decodes to exactly 32 bytes it is used as the raw key; otherwise it is treated as a passphrase (scrypt-derived key).
PGP
Key names reference cryptoKeys records from plugin-crypto-toolkit (public material in the DB, private material only in environment variables).
| Route field | Outbound request | Outbound response | Inbound request | Inbound response |
| --- | --- | --- | --- | --- |
| pgpEncryptKeyName | encrypt to partner public key | — | — | encrypt to partner public key |
| pgpDecryptKeyName | — | decrypt with own private key | decrypt with own private key | — |
| pgpSignKeyName | sign with own private key | — | — | sign with own private key |
| pgpVerifyKeyName | — | verify partner signature | verify partner signature | — |
Private key convention: the own key's privateEnvVar must be a Crypto Toolkit-managed variable (CRYPTO_TOOLKIT_<NAME>_PRIVATE). If the private key is passphrase-protected, provide the passphrase in a companion variable named <privateEnvVar>_PASSPHRASE.
When pgpVerifyKeyName is configured and the incoming signature does not verify, the request is rejected with 400 APIM_SIGNATURE_INVALID. A PGP message that is not signed at all is also rejected in that case — configure pgpVerifyKeyName only when the partner always signs.
RSA-OAEP hybrid
Uses the Crypto Toolkit NCR1 container (RSA-OAEP wraps a random AES-256-GCM session key). Key names reference cryptoKeys records of kind rsa-*:
- Outbound:
rsaEncryptKeyName(partner RSA public key) encrypts the request;rsaDecryptKeyName(own key with private material) decrypts encrypted responses. - Inbound:
rsaDecryptKeyNamedecrypts partner requests;rsaEncryptKeyNameencrypts responses back to the partner.
Request signing (HMAC) and JWT
Per-route, independent of the encryption mode:
- HMAC (
hmacVerifyEnabledinbound /hmacSignEnabledoutbound): the signature covers the canonical stringtimestamp\nnonce\nMETHOD\npath\nsha256hex(body). Thepathcomponent includes the query string, so callers must sign the full request path and any forwarded query parameters are tamper-protected. Headers areX-APIM-Timestamp,X-APIM-Nonce,X-APIM-Signature(hex HMAC-SHA256). Timestamps outsidehmacToleranceSec(clamped 1–3600 s) and replayed nonces are rejected with401 APIM_HMAC_INVALID. - JWT (
jwtVerifyEnabledinbound /jwtSignEnabledoutbound):Authorization: Bearer <token>; the scheme is case-insensitive. HS256 uses the sharedjwtSecret/jwtSecretEnvVar; RS256 uses a Crypto Toolkit key (jwtVerifyKeyName/jwtSignKeyName). Theexpclaim is mandatory — tokens without a numericexpare rejected.jwtIssuer/jwtAudienceare validated when configured.
Forwarding behavior
- The caller's query string is forwarded to the target URL (appended with
?, or&when the target already has one). - Synchronous forwarding with per-route
timeoutMs(504APIM_TIMEOUTwhen exceeded) andretryCount/retryDelayMs(retries on network errors and HTTP 5xx only). maxBodyMbcaps the request body (413APIM_BODY_TOO_LARGE), clamped to 1–100 MB.forwardHeadersallow-lists incoming headers to pass through;staticHeadersinjects fixed headers. Hop-by-hop headers,Authorization,X-API-Key, and cookies are always stripped.- Streaming is not supported: request and response bodies are buffered.
- Upstream failures return a generic
502 APIM_UPSTREAM_ERROR("Upstream request failed"); connection details are written to the server log and the request log only, never to the caller.
Audit log
Every gateway request writes an apiRequestLogs row (timing, sizes, SHA-256 of both bodies, status). Full payloads are stored only when the route has logPayloads enabled (stored base64-encoded). Note what "full payload" means per direction: for outbound routes the logged request payload is the caller's plaintext (captured before encryption); for inbound routes it is the raw ciphertext exactly as the partner sent it. Enable logPayloads only when storing that data is acceptable. Logs older than the retention window (default 30 days) are pruned on startup and every 24 h.
IP allowlist
ipAllowlist accepts exact IPv4/IPv6 addresses and IPv4 CIDR ranges (e.g. 10.0.0.0/8). An empty list allows all clients. Limitations: CIDR matching is IPv4-only — IPv6 entries match by exact address only (zone ids are stripped and ::ffff:a.b.c.d is normalized to a.b.c.d before comparison).
SSRF / outbound whitelist
Outbound forwarding uses the shared serverRequest helper. When SERVER_REQUEST_WHITELIST is unset, all hosts are allowed (with SSRF warnings for private/loopback targets). When you configure SERVER_REQUEST_WHITELIST, remember to include the internal backend hosts used by inbound routes as well as any partner hosts.
Capacity guard (concurrent requests / total bytes)
The gateway refuses to buffer unbounded concurrent payloads. Before a request body is read, the gateway acquires a capacity lease keyed on the request Content-Length size estimate:
- APIM_MAX_CONCURRENT_REQUESTS — maximum in-flight gateway requests (default 50).
- APIM_MAX_TOTAL_BYTES — maximum sum of in-flight request payload bytes (default 512 * 1024 * 1024 = 512 MB).
- APIM_MAX_REQUEST_BYTES — hard per-request ceiling; requests above it fail with 413 APIM_BODY_TOO_LARGE (default � = disabled; use the per-route maxBodyMb for the buffering cap instead).
- APIM_QUEUE_ENABLED — when
true(default), excess requests wait in a FIFO queue instead of being rejected. - APIM_QUEUE_SIZE — max waiting requests (default 1000); beyond this requests are rejected immediately with 429 APIM_CAPACITY_EXCEEDED.
- APIM_QUEUE_TIMEOUT_MS — how long a queued request may wait before being rejected with 503 APIM_QUEUE_TIMEOUT (default 30000).
A queued request that is admitted later also gets a X-APIM-Queued-Ms response header with the number of milliseconds it waited. The limiter is process-local (in-memory), matching the rate limiter; configure service-level connection/pod limits for horizontal deployments.
Circuit breaker
Each target URL has a process-local circuit breaker. Consecutive upstream failures (network errors, HTTP 5xx, and timeouts) open the circuit after the configured threshold; while open, requests are rejected immediately with 503 APIM_CIRCUIT_OPEN and a Retry-After header without touching the target. After the open duration the breaker transitions to half-open and allows one probe request.
- APIM_CIRCUIT_BREAKER_ENABLED — enable/disable the breaker (default true).
- APIM_CIRCUIT_BREAKER_FAILURE_THRESHOLD — consecutive failures before opening (default 5).
- APIM_CIRCUIT_BREAKER_OPEN_DURATION_MS — how long an open circuit rejects requests (default 30000).
- APIM_CIRCUIT_BREAKER_COUNT_SERVER_ERRORS — count HTTP 5xx as failures (default true).
All capacity and circuit-breaker values can also be edited at runtime through the Settings tab; env vars take precedence over the stored settings.
ACL
Admin access to routes/partners/keys/logs is guarded by the pm.plugin-api-manager ACL snippet. Gateway paths bypass the resourcer and are protected solely by API-key authentication.
