node-postgrest-sidecar
node-postgrest-sidecar
/apiV1/ → /api/.In sintesi
Thin wrapper Fastify davanti a PostgREST: tutte le chiamate dati del tenant transitano da qui invece di toccare PostgREST direttamente. Il servizio non possiede modello di dominio proprio — il suo lavoro è prendere l'identità autenticata da nginx (header X-User-*), tradurla in un JWT firmato con il ruolo Postgres corretto e iniettarlo come Authorization: Bearer prima di inoltrare la richiesta. PostgREST lato downstream legge il claim role e attiva la Row-Level Security del database. Lo stesso flusso di iniezione viene riusato su tre transport: HTTP proxy (catch-all /api/*), NATS request/reply e modulo @pzeta/orchestrator-node.
Funzionalità principali
- HTTP passthrough verso PostgREST con rewrite
/apiV1/<x>→/api/<x>lato ingress e/api/→/verso l'upstream PostgREST, basato su@fastify/http-proxy(catch-all su/api/*) - JWT injection per RLS PostgreSQL: estrae 9 header canonici
X-User-*(x-user-id,x-user-email,x-user-roles,x-user-tenant,x-user-groups,x-user-department, ecc.), determina il ruolo PostgREST e firma un JWT HS256 oppure delega la generazione a un IdP OAuth2 viaclient_credentials - Token cache TTL-bound (
SecureTokenCache): chiavejwt:<userId>:<roles>:<tenant>per gli autenticati ejwt:anonymousper gli accessi pubblici; cleanup automatico ognimin(JWT_EXPIRATION/2, 5min) - Anti-bypass:
JwtInjectionService.injectJWT()azzera sempre qualsiasiAuthorizationin ingresso prima di iniettare il proprio JWT, impedendo che un client possa presentare un token arbitrario a PostgREST - Triplo transport, una sola pipeline: lo stesso
JwtInjectionServiceviene chiamato da HTTP, NATS e Orchestrator module — la lista canonica degli headerX-User-*è centralizzata inHeaderConstants.tsper evitare drift fra transport - Public routes bypass: lista CSV
PUBLIC_ROUTES(default/live,/ready,/metrics) servita senza autenticazione versoPUBLIC_SERVICE_URL(default =POSTGREST_URL), con match esatto + wildcard sotto-path - Role mapping:
X-User-Roles(CSV, primo vince) → fallbackX-User-Groups→ fallbackANONYMOUS_ROLE; nessun ruolo gestito staticamente nel codice, tutto guidato dal token e dalla configurazione - NATS proxy headers-based (opzionale): un solo subject (
<NATS_NAMESPACE>.postgrest.request), routing letto dagli headerX-PostgREST-Method/X-PostgREST-Path, validazione mittente HMAC viaNATS_INTERNAL_SECRET - Integrazione Orchestrator (opzionale): si registra come modulo
jwt-sidecarcon taskpostgrest-proxysu@pzeta/orchestrator-node, esponendo il proxy come step di workflow
Architettura
Stack: Fastify v5 · @fastify/http-proxy · Inversify (DI con constructor injection) · jsonwebtoken (HS256) · pg driver · NATS · @pzeta/fastify-utils (securityPlugin per CORS + rate limit) · @pzeta/orchestrator-node · @pzeta/log · Zod per validazione body PostgREST sui codici di errore. Build production via bundle esbuild (scripts/build-prod.mjs).
Layout DDD (src/):
| Layer | Contenuto |
|---|---|
application/services/ | AuthService (extract / determineRole / OAuth2 flow / JWT sign / cache), JwtInjectionService (punto unico di iniezione condiviso dai tre transport), SecureTokenCache |
application/constants/ | HeaderConstants.ts — lista canonica USER_HEADER_NAMES (9 header, lowercase + variante PascalCase per NATS) |
application/dtos/ · application/ports/ | Interfacce UserInfo, JwtClaims, OAuth2TokenResponse, ProxyConfig, contratto IConfig |
infrastructure/ | Server.ts (wiring Fastify), config/Config.ts (lettura env), di/Container.ts (composition root Inversify, binding condizionale NATSPostgRESTProxy se natsProxy.enabled), http/PostgRESTHttpClient.ts, nats/* (NatsPostgrestProxy, HeadersParser, NATSIdentityValidator HMAC, NATSMessageHandler, NATSMetricsCollector), orchestrator/OrchestratorAdapter.ts |
presentation/http/ | plugins/ProxyPlugin.ts (preHandler JWT + catch-all /api/*), routes/PublicRoutes.ts, routes/DebugRoutes.ts |
Pattern adottati: Sidecar (un solo container davanti a PostgREST, deploy 1:1 con il backend dati), thin wrapper, Dependency Injection (Inversify), single-source-of-truth per gli header utente, anti-bypass su Authorization.
Ordine di registrazione dei plugin in Server.initialize() — l'ordine è significativo perché @fastify/http-proxy registra un catch-all su /api/*:
securityPlugindi@pzeta/fastify-utils(CORS opzionale + rate limit)- Handler inline
/livee/ready(probe interne sempre locali, evitano self-loop quandoPUBLIC_SERVICE_URLpunta al sidecar) DebugRoutes(solo seENABLE_DEBUG_ROUTES=trueeNODE_ENV !== production— il flag è forzato afalsein produzione)registerProxyPlugin— deve essere ultimo: registra lePUBLIC_ROUTESesplicitamente e poi il catch-all/api/*verso PostgREST
Override rispetto allo stack standard: questo servizio non usa authPlugin di @pzeta/fastify-utils perché è lui stesso il layer di autenticazione (genera/inietta JWT a monte di PostgREST). Per lint e format usa Biome invece di ESLint+Prettier.
Casi d'uso
- Frontend Vue → dati del tenant: il frontend chiama
https://ditta.pzeta.it/apiV1/<tabella>?<filtri-postgrest>invece diPOSTGREST_URL/<tabella>perché (a) PostgREST non è esposto pubblicamente, (b) richiede un JWT firmato che il browser non possiede, (c) il sidecar traduce l'identità autenticata da nginx in quel JWT senza che il client veda mai un secret - Microservizio interno → query PostgREST autenticata:
node-orchestrator,node-print,node-rendererenode-notificationchiamano il sidecar via DNS interno (node-postgrest-sidecar-ditta.ditta.svc.cluster.local) propagando gli stessiX-User-*ricevuti dal proprio chiamante a monte. Il sidecar emette il JWT con il ruolo dell'utente originale e PostgREST applica la RLS coerente con quella sessione - Step di workflow orchestrato:
node-orchestratorrichiama il taskpostgrest-proxyesposto dal modulo@pzeta/orchestrator-noderegistrato dal sidecar quandoORCHESTRATOR_URLè valorizzato — utile per pipeline data-driven che alternano query PostgREST a step custom - Trasporto NATS per servizi event-driven: quando
natsProxy.enabled = true, un microservizio interno pubblica su<NATS_NAMESPACE>.postgrest.requestcon headerX-PostgREST-Method/X-PostgREST-Pathe riceve in reply lo stesso payload che otterrebbe via HTTP; utile per esecuzioni batch con back-pressure NATS invece di connection pool HTTP - Accesso anonimo controllato: richieste senza
X-User-*ricevono il ruoloANONYMOUS_ROLEconfigurato (in modalità autonomous via JWT firmato, in modalità OAuth2 senza JWT — PostgREST applicadb-anon-roledirettamente). Permette di esporre viste pubbliche del DB senza un secondo deploy del sidecar - Health esposti pubblicamente:
/live,/readye/metrics(e qualsiasi entry inPUBLIC_ROUTES) bypassano l'iniezione JWT — utili per probe Kubernetes e scraping Prometheus
Identità & esposizione
| Campo | Valore |
|---|---|
| Categoria | data-access |
| Versione cluster | 1.0.22 |
| Image | gitea.pzetatouch.it/pzeta_touch/node-postgrest-sidecar:1.0.22 |
| URL pubblico | https://ditta.pzeta.it/apiV1 |
| Path regex ingress | `/apiV1(/ |
| Rewrite a backend | /api/$2 |
| DNS interno | node-postgrest-sidecar-ditta.ditta.svc.cluster.local:3000 |
| Auth nginx | auth_request → node-user-auth |
| Repository | node-postgrest-sidecar |
Endpoint operazionali
Endpoint convenzionali esposti da tutti i microservizi PZeta basati su @pzeta/fastify-utils:
| Path pubblico | Scopo |
|---|---|
https://ditta.pzeta.it/apiV1/health | liveness probe |
https://ditta.pzeta.it/apiV1/ready | readiness probe |
https://ditta.pzeta.it/apiV1/metrics | metriche Prometheus |
Note specifiche del sidecar: /live e /ready sono serviti direttamente dal processo Fastify (non proxati) per evitare self-loop quando PUBLIC_SERVICE_URL coincide con POSTGREST_URL puntando al sidecar stesso. Lo spec OpenAPI completo dell'API dati è renderizzato dinamicamente da PostgREST a GET /apiV1/ — non è snapshottato in questa pagina perché varia in funzione dello schema database del tenant.
Configurazione
Variabili d'ambiente che un integratore deve conoscere (per la lista completa vedi .env.example del repo):
| Variabile | Ruolo |
|---|---|
POSTGREST_URL | URL upstream PostgREST (obbligatorio) — target del proxy /api/* |
PUBLIC_SERVICE_URL | Target delle PUBLIC_ROUTES; default = POSTGREST_URL. Distinguilo se le rotte pubbliche vivono altrove |
PUBLIC_ROUTES | CSV di path che bypassano l'iniezione JWT (default /live,/ready,/metrics). Match esatto + wildcard sotto-path |
JWT_GENERATION_MODE | autonomous (firma locale HS256, richiede JWT_SECRET ≥ 32 char) oppure centralized (delega a AUTH_SERVICE_URL) |
JWT_SECRET / JWT_EXPIRATION / JWT_ISSUER / JWT_AUDIENCE | Parametri di firma in modalità autonomous; TTL del token e dell'entry di cache |
OAUTH2_ENABLED | Se true, sostituisce la firma locale con client_credentials verso OAUTH2_TOKEN_ENDPOINT (passando X-User-* come user_headers). In OAuth2 gli anonimi non ricevono JWT — PostgREST applica db-anon-role |
OAUTH2_TOKEN_ENDPOINT / OAUTH2_CLIENT_ID / OAUTH2_CLIENT_SECRET / OAUTH2_SCOPE | Configurazione IdP esterno |
ANONYMOUS_ROLE | Ruolo PostgREST per richieste senza identità (deve esistere come ruolo Postgres nel DB del tenant) |
TOKEN_CACHE_CAPACITY | Numero massimo di entry nella cache JWT (1 entry = userId+roles+tenant) |
CORS_ALLOWED_ORIGINS | CSV di origini abilitate; stringa vuota = CORS disabilitato (non aperto). Imposta esplicitamente per accesso browser cross-origin |
RATE_LIMIT_MAX / RATE_LIMIT_WINDOW | Throttle in entrata via securityPlugin |
NATS_ENABLED / NATS_URL / NATS_NAMESPACE | Abilita il proxy NATS headers-based; il subject finale è <NATS_NAMESPACE>.postgrest.request (namespace vuoto = nessun prefisso) |
NATS_INTERNAL_SECRET | HMAC sender validation per il proxy NATS (≥ 32 char). Obbligatorio quando NATS_ENABLED=true |
ORCHESTRATOR_URL | Se valorizzato, registra il sidecar come modulo @pzeta/orchestrator-node (moduleId: jwt-sidecar, task postgrest-proxy) |
ENABLE_DEBUG_ROUTES | Attiva endpoint diagnostici (/debug/user-info); forzato a false in NODE_ENV=production anche se settato a true |
Quality gate consigliato in CI: npm run validate (typecheck + lint:check + format:check + build + test). Build production via npm run build:prod (bundle esbuild, non tsc).
Note eventing NATS
Il servizio non pubblica eventi di dominio: PostgREST è un layer di accesso dati sincrono e il sidecar è un thin wrapper che si limita a brokerare richieste/risposte. Non emette events.* né si sottoscrive a subject di altri domini.
L'unica integrazione NATS è il proxy headers-based (NATSPostgRESTProxy), opzionale e attivato da NATS_ENABLED=true. È un pattern request/reply puro:
- Un solo subject configurabile (default
postgrest.request, prefissato daNATS_NAMESPACEse presente →<ns>.postgrest.request) - Routing letto dagli header NATS
X-PostgREST-Method(GET|POST|PUT|PATCH|DELETE) eX-PostgREST-Path - Validazione mittente via HMAC SHA-256 con
NATS_INTERNAL_SECRET(NATSIdentityValidator) — previene proxy non autorizzati sul bus interno - Stesso
JwtInjectionServiceusato dal proxy HTTP: gliX-User-*passati negli header NATS vengono trasformati inAuthorization: Bearerverso PostgREST - Queue group per load-balancing fra repliche del sidecar
- Risposta sincrona via reply subject NATS con body PostgREST originale + metriche (
NATSMetricsCollector)
Per questo motivo la tabella subject auto-generata è (e resta) vuota o limitata al solo subject di proxy: non c'è un dominio eventato dietro questo servizio.
Dipendenze e dipendenti
Dipende da (servizi che questo servizio chiama):
Consumato da (chi chiama questo servizio):
Infrastruttura (PostgreSQL, NATS, Redis, MinIO) non è elencata qui — vedi sezione Architettura del singolo servizio.