node-user-profiling
node-user-profiling
In sintesi
Servizio di autorizzazione enterprise: risponde alle domande "chi è questo utente?" e "cosa può fare?" per l'intero portale ditta. Mantiene la proiezione locale dell'identità applicativa — ruoli, permessi granulari, gruppi, team, policy ABAC dinamiche — e ne pubblica il risultato come decisione autorizzativa cache-friendly. Si distingue dal complementare node-user-auth, che gestisce come l'utente si autentica (cookie nginx, JWT, OAuth M2M): qui si concentra esclusivamente la logica RBAC/ABAC e l'analisi del rischio. Le route REST sono il canale primario; NATS è usato in modo puntuale per restare allineato a eventi di ciclo vita utente.
Funzionalità principali
- RBAC gerarchico su cinque livelli (
superadmin > admin > manager > operatore > viewer) con ereditarietà permessi e prevenzione cicli a livello DB - ABAC engine con policy
allow/deny/conditional, condizioni runtime su orari, dipartimenti e attributi della richiesta, modalitàfail_open/fail_closed - Gestione gruppi e team: CRUD completo, membership, ruoli e permessi collettivi su
auth.gruppi(tipogruppoprogettooorganizzativo), invalidazione cache transitiva - Check autorizzazione singolo e batch (
POST /api/v1/authorization/checke/bulk-check) con cache distribuita Redis e TTL distinti per esiti positivi, sensibili e negativi - Endpoint nginx
auth_request: verifica l'autorizzazione di una richiesta a partire dagli header inoltrati dal reverse proxy (/nginx-auth) - Anomaly detection con risk scoring 0-100, finestre di osservazione configurabili, alert su soglia, indagine pattern per singolo utente
- Audit trail delle decisioni autorizzative tramite event bus interno (audit, metrics, security alert) con correlation ID propagato
- Reconciliation utenti orfani (
/api/v1/admin/users/exists) e endpoint internal S2S (/api/v1/internal/*) per fallback hard-delete e operazioni batch protette daX-Internal-Token - Migrazioni SQL idempotenti con checksum tracking eseguite all'avvio del processo
Architettura
Stack: Fastify v5 · Inversify (DI con @injectable / @inject e profili default / high-performance / high-security / development) · PostgreSQL via driver pg (schema auth) · Redis opzionale per cache decisioni · NATS opzionale per un singolo subject di sincronizzazione · @pzeta/fastify-utils (authPlugin, securityPlugin, errorHandlerPlugin, openapiPlugin, healthcheckPlugin) · @pzeta/authorization (libreria interna che fornisce AuthorizationOrchestrator, PermissionEvaluationService, BusinessPolicyService, SecurityAnalyticsService) · @pzeta/log per logging strutturato · Zod per validazione input.
Layout DDD (src/):
| Layer | Contenuto |
|---|---|
domain/authorization/ | Motori RBACEngine e ABACEngine (con OPAPolicyEngine), entità Role, Permission, Policy, PermissionConflict, AuthorizationDecision |
domain/entities/ | Group e Team con value object di membership, ruoli e permessi |
domain/events/ | Eventi di dominio PermissionGrantedEvent, PermissionDeniedEvent, PolicyEvaluatedEvent, AnomalyDetectedEvent, CacheInvalidatedEvent + DomainEventPublisher |
domain/services/ | UserBehaviorAnalysisService per analisi pattern accessi e timing profile |
application/services/ | AuthorizationAppService — orchestra i use case combinando l'orchestrator della libreria con CRUD policy/group/team e mapping HTTP↔domain |
infrastructure/database/ | PostgresConnectionPool (pool tunato), MigrationRunner con checksum |
infrastructure/repositories/ | RBACRepository e OptimizedRBACRepository (variante con prepared statement per profilo high-performance) |
infrastructure/authorization/ | AuthorizationFactory, PostgresABACRepository, GroupRepository, TeamRepository |
infrastructure/cache/ | PermissionCache Redis-backed e NullPermissionCache per esecuzione senza cache |
infrastructure/messaging/ | NatsEventBus (wrapper riconnessione automatica) e UserDeletedSubscriber |
infrastructure/di/ | AuthorizationContainer — wiring Inversify e registrazione subscriber dell'event bus in-memory |
presentation/routes/ | 13 moduli route (RoleRoutes, PermissionRoutes, UserRoleRoutes, UserPermissionRoutes, AuthorizationCheckRoutes, PolicyRoutes, GroupRoutes, TeamRoutes, StatsRoutes, AnomalyRoutes, NginxAuthRoutes, AdminRoutes, InternalRoutes) |
presentation/middleware/ | Rate limiting per-user/IP, RequirePermission per protezione granulare |
Pattern adottati: Clean Architecture + DDD, Dependency Injection (Inversify), Repository, Factory, Strategy (profili AUTH_SERVICE_PROFILE), in-memory event bus per side-effects post-decisione (audit / metrics / security alert), Result monad per error handling funzionale nei servizi di dominio.
RBAC e ABAC: le route applicano requirePermission() sulla coppia resource:action derivata dal contesto. Il check finale è risolto dall'AuthorizationOrchestrator che combina decisione RBAC (materializzata dai ruoli effettivi dell'utente: diretti + ereditati + da gruppi/team), valutazione ABAC della policy applicabile e politica di fallback fail_open / fail_closed per ogni policy.
Casi d'uso
- Gestione ruoli da UI admin: il frontend Vue (sezione amministrativa) crea un nuovo ruolo via
POST /api/v1/roles, ne dichiara ilparentRoleId, associa permessiresource:actione lo assegna a gruppi/team o singoli utenti con eventuale scadenza - Check permessi runtime da altri microservizi: un servizio applicativo (es.
node-orchestrator) inviaPOST /api/v1/authorization/checkcon{ userId, resource, action, context }per decidere se eseguire un task sensibile; le decisioni sono cacheable con TTL configurabili - Autorizzazione del reverse proxy: nginx invoca
/nginx-authcon headerx-user-ide path target; il servizio risponde 200/403 e popola gli header inoltrati alla risorsa protetta - Sincronizzazione utenti da provider esterno:
node-user-auth, dopo aver registrato un nuovo utente nell'identity provider, può triggerare la reconciliation viaGET /api/v1/admin/users/existso pubblicare un evento di cancellazione su NATS (vedi sezione eventing) per propagare l'hard-delete GDPR - Investigazione security: l'operatore SOC consulta
/api/v1/anomaliesper identificare utenti con risk score elevato, ricostruisce la timeline dei tentativi negati tramite gli aggregati esposti da/api/v1/statse disabilita ruoli compromessi - Operazioni batch internal: pipeline di sistema chiamano
/api/v1/internal/users/:userId/roles/batch(protette daX-Internal-Token) per riassegnare massivamente ruoli a fronte di una riorganizzazione
Helper SQL pubblici (public.*)
Per evitare round-trip HTTP al microservizio quando si vogliono fare check di permesso all'interno di una query SQL (filtri di lista, viste, RLS policy, funzioni di dominio in altri schemi), il servizio installa via migrazione (migrations/z_public_helpers.sql) una suite di funzioni helper nello schema public che incapsulano l'engine RBAC+ABAC e leggono lo userId implicito dai claims JWT della sessione PostgreSQL.
Il pattern è progettato per essere riusato trasversalmente da tutti i microservizi e gli schemi applicativi del portale: ogni servizio che si connette al database condiviso (sia via pool Node.js diretto, sia tramite PostgREST/Hasura) può chiamare public.can(...), public.has_role(...), public.resource_permissions(...) senza sapere come funziona internamente l'RBAC.
Modello di sessione: chi è l'utente?
L'utente corrente viene letto dal GUC PostgreSQL request.jwt.claims (convenzione PostgREST/Hasura) che contiene una stringa JSON con almeno il campo userId e opzionalmente departmentId e teamId:
{ "userId": 42, "departmentId": 5, "teamId": 12 }
| Origine claims | Setup |
|---|---|
| PostgREST | Automatico: il proxy valida il JWT e imposta request.jwt.claims per ogni request |
| Node.js (pool diretto) | Esplicito a inizio transazione: SELECT set_config('request.jwt.claims', $1, true) |
| Hasura | Automatico via Hasura-Claims-Map |
| Nessun setup | Tutte le funzioni can_* / has_* / is_* ritornano FALSE (fail-secure) |
Tutte le funzioni sono SECURITY DEFINER con search_path bloccato (auth, public, pg_temp): i microservizi consumatori non necessitano alcun grant su auth.* — basta EXECUTE su public.* (già concesso a PUBLIC).
Quick reference
Lettura sessione
| Funzione | Ritorno | Note |
|---|---|---|
public.current_user_id() | INTEGER | Estrae userId dai claims; NULL se assente |
public.current_user_dept_id() | INTEGER | Estrae departmentId; NULL se assente |
public.current_user_team_id() | INTEGER | Estrae teamId; NULL se assente |
Check permesso (boolean)
| Funzione | Ritorno | Uso tipico |
|---|---|---|
public.can(resource, action) | BOOLEAN | Scope all / default |
public.can_own(resource, action, owner_id) | BOOLEAN | Filtro ownership riga |
public.can_dept(resource, action, dept_id) | BOOLEAN | Filtro dipartimento riga |
public.can_team(resource, action, team_id) | BOOLEAN | Filtro team riga |
public.can_full(resource, action, owner, dept, team, ctx) | BOOLEAN | Tutti i parametri + condizioni JSONB |
public.authorize(resource, action) | BOOLEAN | RBAC+ABAC combinati (include policy) |
public.authorize_detail(...) | JSONB | Decisione strutturata per audit/debug |
Ruoli
| Funzione | Ritorno |
|---|---|
public.has_role(nome) | BOOLEAN |
public.has_any_role(text[]) | BOOLEAN |
public.is_admin() | BOOLEAN (alias per has_role('admin')) |
public.is_superuser() | BOOLEAN (alias per has_role('superuser')) |
Introspezione
| Funzione | Ritorno | Uso |
|---|---|---|
public.get_my_roles() | SETOF role_row | Ruoli effettivi (direct + inherited + group) |
public.get_my_permissions() | SETOF permission_row | Permessi materializzati |
public.resource_permissions(resource) | 1 row × 12 boolean | Matrice {read, write, delete} × {all, dept, team, own} per UI badge / filtri |
Esempi backend — Node.js / Fastify
1. Setup claims per request (middleware Fastify)
Estrai userId/deptId/teamId dall'header che nginx inoltra (o dal JWT decodificato) e inietta i claims all'inizio della transazione:
import { FastifyPluginAsync } from 'fastify';
import { Pool, PoolClient } from 'pg';
export const withAuthorizedClient = (pool: Pool): FastifyPluginAsync => async (app) => {
app.decorateRequest('db', null);
app.addHook('preHandler', async (req, _reply) => {
const userId = Number(req.headers['x-user-id']);
if (!Number.isFinite(userId)) return;
const client = await pool.connect();
await client.query('BEGIN');
await client.query(
"SELECT set_config('request.jwt.claims', $1, true)",
[JSON.stringify({
userId,
departmentId: req.user?.departmentId ?? null,
teamId: req.user?.teamId ?? null
})]
);
(req as any).db = client;
});
app.addHook('onResponse', async (req) => {
const client = (req as any).db as PoolClient | null;
if (!client) return;
await client.query('COMMIT').catch(() => client.query('ROLLBACK'));
client.release();
});
};
Da questo momento, ogni query eseguita su req.db ha l'utente in sessione e le helper public.* funzionano automaticamente.
2. Check imperativo prima di un'azione
app.post('/ordini/:id/approva', async (req, reply) => {
const { rows: [{ allowed }] } = await req.db.query(
"SELECT public.can('ordini', 'approve') AS allowed"
);
if (!allowed) return reply.code(403).send({ error: 'forbidden' });
await req.db.query('UPDATE ordini SET stato = $1 WHERE id = $2', ['approvato', req.params.id]);
return { ok: true };
});
3. Filtro lista dipendente da ownership
app.get('/ordini', async (req) => {
const { rows } = await req.db.query(`
SELECT o.*
FROM ordini o
WHERE public.can_own('ordini', 'read', o.idutentecreazione)
OR public.can_team('ordini', 'read', o.idteam)
OR public.can('ordini', 'read') -- shortcut per scope 'all'
`);
return rows;
});
4. Endpoint "i miei permessi su X" per UI
app.get('/permissions/:resource', async (req) => {
const { rows: [perms] } = await req.db.query(
'SELECT * FROM public.resource_permissions($1)',
[req.params.resource]
);
return perms;
// → { can_read_all: false, can_read_dept: false,
// can_read_team: true, can_read_own: true,
// can_write_all: false, can_write_dept: false,
// can_write_team: true, can_write_own: true,
// can_delete_all: false, can_delete_dept: false,
// can_delete_team: false, can_delete_own: false }
});
5. Verifica ruolo + breakdown ruoli effettivi
app.get('/me/roles', async (req) => {
const { rows: roles } = await req.db.query('SELECT * FROM public.get_my_roles()');
const { rows: [{ admin }] } = await req.db.query(
"SELECT public.is_admin() AS admin"
);
return { admin, roles };
});
Esempi backend — Vista filtrante
6. Vista "i miei record o del mio team"
CREATE OR REPLACE VIEW public.v_ordini_miei_team
WITH (security_barrier = true) AS
SELECT o.*
FROM public.ordini o
WHERE o.idutentecreazione = public.current_user_id()
OR o.idteam = public.current_user_team_id();
security_barrier = trueimpedisce al planner di "spingere" filtri esterni prima del WHERE → niente leakage informativo- Se i claims non sono in sessione, le
current_user_*_id()ritornanoNULL→=produceUNKNOWN→ 0 righe visibili
7. Vista permission-aware (rispetta i permessi RBAC configurati)
Una sola vista che si auto-adatta al livello di permesso concesso all'utente:
CREATE OR REPLACE VIEW public.v_ordini_visibili
WITH (security_barrier = true) AS
SELECT o.*
FROM public.ordini o,
public.resource_permissions('ordini') p
WHERE p.can_read_all
OR (p.can_read_dept AND o.iddipartimento = public.current_user_dept_id())
OR (p.can_read_team AND o.idteam = public.current_user_team_id())
OR (p.can_read_own AND o.idutentecreazione = public.current_user_id());
Comportamento:
- Utente con
ordini/read/all→ vede tutto - Utente con solo
ordini/read/team→ vede solo gli ordini del suo team - Utente con
read/team+read/own→ vede entrambi (OR) - Utente senza permessi
read/*→ 0 righe
resource_permissions(...) è STABLE, quindi viene valutata una sola volta per query, non per riga.
Esempi backend — Row Level Security
8. RLS protezione tabella
Differenza chiave rispetto alla vista: l'RLS protegge anche INSERT/UPDATE/DELETE, non solo SELECT.
ALTER TABLE public.ordini ENABLE ROW LEVEL SECURITY;
CREATE POLICY p_ordini_select ON public.ordini FOR SELECT USING (
public.can('ordini', 'read')
OR public.can_dept('ordini', 'read', iddipartimento)
OR public.can_team('ordini', 'read', idteam)
OR public.can_own ('ordini', 'read', idutentecreazione)
);
CREATE POLICY p_ordini_update ON public.ordini FOR UPDATE USING (
public.can_own('ordini', 'write', idutentecreazione)
OR public.is_admin()
);
CREATE POLICY p_ordini_delete ON public.ordini FOR DELETE USING (
public.can('ordini', 'delete')
OR public.is_admin()
);
9. RLS condizionale (es. solo lettura per ruolo viewer)
CREATE POLICY p_documenti_select ON public.documenti FOR SELECT USING (
public.has_role('admin')
OR public.has_role('manager')
OR (public.has_role('viewer') AND public.can_own('documenti', 'read', idutentecreazione))
);
Esempi backend — Funzioni di dominio in altri schemi
10. Funzione autisti.schedule_corsa con check inline
CREATE OR REPLACE FUNCTION autisti.schedule_corsa(p_idcorsa INTEGER, p_idautista INTEGER)
RETURNS VOID
LANGUAGE plpgsql
AS $$
BEGIN
IF NOT public.can('autisti', 'schedule') THEN
RAISE EXCEPTION 'Permesso negato: utente % non puo'' schedulare corse',
public.current_user_id();
END IF;
UPDATE autisti.corse SET idautista = p_idautista WHERE idcorsa = p_idcorsa;
END;
$$;
11. Vista cross-schema con flag di permesso
Espone i flag di permesso direttamente nelle righe ritornate, così il frontend non deve fare query separate:
CREATE OR REPLACE VIEW autisti.v_corse_con_permessi
WITH (security_barrier = true) AS
SELECT
c.*,
public.can_own('autisti', 'write', c.idutentecreazione) AS can_edit,
public.can_own('autisti', 'delete', c.idutentecreazione) AS can_delete,
public.can('autisti', 'approve') AS can_approve
FROM autisti.corse c
WHERE c.idutentecreazione = public.current_user_id()
OR c.idteam = public.current_user_team_id();
Esempi frontend — Vue 3
12. Composable useResourcePermissions
// composables/useResourcePermissions.ts
import { ref, computed } from 'vue';
import { apiClient } from '@/services/api';
export interface ResourcePermissions {
can_read_all: boolean; can_read_dept: boolean;
can_read_team: boolean; can_read_own: boolean;
can_write_all: boolean; can_write_dept: boolean;
can_write_team: boolean; can_write_own: boolean;
can_delete_all: boolean; can_delete_dept: boolean;
can_delete_team: boolean; can_delete_own: boolean;
}
export function useResourcePermissions(resource: string) {
const perms = ref<ResourcePermissions | null>(null);
const loading = ref(false);
const load = async () => {
loading.value = true;
perms.value = await apiClient.get<ResourcePermissions>(`/permissions/${resource}`);
loading.value = false;
};
const canRead = computed(() =>
!!perms.value && (perms.value.can_read_all || perms.value.can_read_dept ||
perms.value.can_read_team || perms.value.can_read_own));
const canWrite = computed(() =>
!!perms.value && (perms.value.can_write_all || perms.value.can_write_dept ||
perms.value.can_write_team || perms.value.can_write_own));
const canDelete = computed(() =>
!!perms.value && (perms.value.can_delete_all || perms.value.can_delete_dept ||
perms.value.can_delete_team || perms.value.can_delete_own));
const readScope = computed<'all'|'department'|'team'|'own'|null>(() => {
if (!perms.value) return null;
if (perms.value.can_read_all) return 'all';
if (perms.value.can_read_dept) return 'department';
if (perms.value.can_read_team) return 'team';
if (perms.value.can_read_own) return 'own';
return null;
});
load();
return { perms, loading, canRead, canWrite, canDelete, readScope, reload: load };
}
13. Disabilitazione bottoni in base ai permessi
<script setup lang="ts">
import { useResourcePermissions } from '@/composables/useResourcePermissions';
const props = defineProps<{ ordineId: number; ownerId: number }>();
const { perms } = useResourcePermissions('ordini');
const { user } = useAuth();
const canEdit = computed(() =>
perms.value?.can_write_all || (perms.value?.can_write_own && props.ownerId === user.value.id)
);
const canDelete = computed(() =>
perms.value?.can_delete_all || (perms.value?.can_delete_own && props.ownerId === user.value.id)
);
</script>
<template>
<div class="flex gap-2">
<button :disabled="!canEdit"
class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-300">
Modifica
</button>
<button :disabled="!canDelete"
class="px-3 py-1 rounded bg-red-600 text-white disabled:bg-gray-300">
Elimina
</button>
</div>
</template>
14. Filtro lista auto-adattivo
Il frontend decide quale filtro server-side applicare in base allo scope concesso:
<script setup lang="ts">
import { useResourcePermissions } from '@/composables/useResourcePermissions';
const { readScope } = useResourcePermissions('ordini');
const { user } = useAuth();
const ordini = ref([]);
watch(readScope, async (scope) => {
if (!scope) { ordini.value = []; return; }
const query = {
all: {},
department: { iddipartimento: user.value.departmentId },
team: { idteam: user.value.teamId },
own: { idutentecreazione: user.value.id }
}[scope];
ordini.value = await apiClient.get('/ordini', { params: query });
}, { immediate: true });
</script>
<template>
<div v-if="readScope === null" class="p-4 bg-red-50 text-red-700 rounded">
Non hai i permessi per visualizzare gli ordini
</div>
<DataTable v-else :rows="ordini" />
</template>
15. Badge "scope di accesso" per UI admin
<script setup lang="ts">
const props = defineProps<{ scope: 'all'|'department'|'team'|'own'|null }>();
const label = {
all: 'Tutti i record',
department: 'Solo mio dipartimento',
team: 'Solo mio team',
own: 'Solo i miei',
[null as any]: 'Nessun accesso'
};
const color = {
all: 'bg-green-100 text-green-800',
department: 'bg-blue-100 text-blue-800',
team: 'bg-indigo-100 text-indigo-800',
own: 'bg-yellow-100 text-yellow-800',
[null as any]: 'bg-red-100 text-red-800'
};
</script>
<template>
<span :class="['px-2 py-0.5 rounded text-xs font-medium', color[props.scope ?? 'null']]">
{{ label[props.scope ?? 'null'] }}
</span>
</template>
16. Directive v-can per nascondere/disabilitare elementi
// directives/vCan.ts
import type { Directive } from 'vue';
import { useResourcePermissionsCache } from '@/composables/useResourcePermissionsCache';
export const vCan: Directive<HTMLElement, string> = {
async mounted(el, binding) {
const [resource, action] = binding.value.split(':');
const perms = await useResourcePermissionsCache().get(resource);
const allowed =
perms[`can_${action}_all`] || perms[`can_${action}_dept`] ||
perms[`can_${action}_team`] || perms[`can_${action}_own`];
if (!allowed) {
if (binding.modifiers.disable) el.setAttribute('disabled', 'true');
else el.style.display = 'none';
}
}
};
Uso:
<button v-can="'ordini:write'">Nuovo ordine</button>
<button v-can.disable="'ordini:delete'">Elimina selezionati</button>
Pattern di errore comuni
| Sintomo | Causa | Fix |
|---|---|---|
Tutte le can_* ritornano FALSE | request.jwt.claims non impostato (claims = null) | Verificare il middleware/hook che chiama set_config('request.jwt.claims', ...) su ogni request |
| Funziona in PostgREST ma non in Node.js | set_config(..., false) invece di true → claims solo per la query corrente | Usare true come terzo parametro (LOCAL alla transazione) e tenere aperta la transazione per tutte le query del request |
can_team sempre FALSE anche se l'utente ha il permesso | Manca teamId nei claims JWT | Aggiungere teamId al payload del token (lato node-user-auth) o ai claims set_config |
permission denied for schema auth | Helper chiamate senza SECURITY DEFINER (es. proxy custom mal scritto) | Usare le funzioni public.* originali — sono già SECURITY DEFINER con grant EXECUTE TO PUBLIC |
| Vista mostra righe a un utente senza permesso | Manca WITH (security_barrier = true) → planner pusha filtri esterni dentro la vista | Aggiungere security_barrier = true a tutte le viste autorizzanti |
| Performance degradata su grandi dataset | Funzioni helper chiamate per riga invece che una volta a query | Strutturare query con CROSS JOIN su resource_permissions(...) (è STABLE, valutata una volta) |
Identità & esposizione
| Campo | Valore |
|---|---|
| Categoria | identity |
| Versione cluster | 1.0.0 |
| Image | gitea.pzetatouch.it/pzeta_touch/node-user-profiling:1.0.14 |
| URL pubblico | https://ditta.pzeta.it/user-profiling |
| Path regex ingress | `/user-profiling(/ |
| Rewrite a backend | /$2 |
| DNS interno | node-user-profiling-ditta.ditta.svc.cluster.local:3000 |
| Auth nginx | auth_request → node-user-auth |
| Repository | node-user-profiling |
| Endpoint REST | 64 (vedi sezione "API reference") |
Endpoint operazionali
Endpoint convenzionali esposti da tutti i microservizi PZeta basati su @pzeta/fastify-utils:
| Path pubblico | Scopo |
|---|---|
https://ditta.pzeta.it/user-profiling/health | liveness probe |
https://ditta.pzeta.it/user-profiling/ready | readiness probe |
https://ditta.pzeta.it/user-profiling/metrics | metriche Prometheus |
https://ditta.pzeta.it/user-profiling/api-docs.json | spec OpenAPI runtime (richiede OPENAPI_EXPOSE_IN_PRODUCTION=true) |
https://ditta.pzeta.it/user-profiling/api-docs | Swagger UI (solo in NODE_ENV !== production) |
Configurazione
Variabili d'ambiente che un integratore deve conoscere (per la lista completa vedi .env.example del repo):
| Variabile | Ruolo |
|---|---|
DATABASE_HOST / _PORT / _USER / _PASSWORD / _NAME | Connessione PostgreSQL — ruoli, permessi, gruppi, team, policy, audit |
DATABASE_SCHEMA | Default auth; isola gli oggetti di profilazione dal resto del DB applicativo |
DATABASE_MAX_CONNECTIONS | Pool size del driver pg; alzare nei profili high-performance |
AUTH_SERVICE_PROFILE | Profilo di runtime: default, high-performance (repository ottimizzati + Redis aggressivo), high-security (policy fail_closed, soglie anomaly stringenti), development (Swagger UI esposto, error verbose) |
REDIS_ENABLED / REDIS_HOST / _PORT / _PASSWORD / _DB | Cache distribuita delle decisioni autorizzative; off in dev |
CACHE_STANDARD_TTL / CACHE_SENSITIVE_TTL / CACHE_DENY_TTL | TTL in secondi per esiti standard, sensibili e di rifiuto |
NGINX_AUTH_ENABLED / NGINX_AUTH_STRICT | Abilita l'endpoint /nginx-auth e ne governa la modalità strict (rifiuta header malformati) |
TRUST_X_USER_ID_HEADER | Forza il rispetto di x-user-id solo se ricevuto da nginx upstream; false in produzione esposta |
SECURITY_RISK_THRESHOLD / SECURITY_ANALYSIS_WINDOW_HOURS | Soglia di rischio e finestra di analisi per anomaly detection |
RATE_LIMIT_ENABLED / RATE_LIMIT_MAX_REQUESTS | Rate limit sliding window per utente/IP via @pzeta/fastify-utils |
MIGRATION_STRICT_CHECKSUM | Se true, una migrazione modificata rispetto al checksum registrato blocca l'avvio |
INTERNAL_API_TOKEN | Token (min 16 char) per le route /api/v1/internal/* di operazioni S2S |
NATS_URL / NATS_USER / NATS_PASSWORD / NATS_NAMESPACE | Connessione NATS opzionale; se assente, il servizio gira in modalità solo-HTTP |
USER_DELETED_SUBJECT | Override del subject NATS per eventi di cancellazione utente; default ${NATS_NAMESPACE}.authz.user.deleted |
Swagger UI e api-docs.json runtime sono esposti solo quando AUTH_SERVICE_PROFILE=development o, in produzione, se OPENAPI_EXPOSE_IN_PRODUCTION=true.
Note eventing NATS
Il servizio è principalmente HTTP-driven e usa NATS come canale secondario di sincronizzazione, non come trasporto primario delle decisioni autorizzative. La connessione NATS è opzionale: se NATS_URL non è configurato o non raggiungibile, il servizio prosegue in modalità degradata mantenendo intatte tutte le funzionalità REST.
Il subscriber NATS effettivamente registrato è uno solo: UserDeletedSubscriber ascolta ${NATS_NAMESPACE}.authz.user.deleted (override via USER_DELETED_SUBJECT) per ricevere notifiche di hard-delete GDPR pubblicate da node-user-auth. Alla ricezione invoca AuthorizationAppService.cleanupUser, idempotente per costruzione (DELETE su righe assenti = no-op). Le semantiche di delivery sono at-most-once (Core NATS senza JetStream): un errore nell'handler scarta il messaggio; il recupero avviene via endpoint HTTP /api/v1/internal/users/:userId/cleanup (manuale) o reconciliation batch /api/v1/admin/users/exists. L'upgrade a JetStream a livello infrastruttura aggiunge at-least-once senza modifiche al codice.
I dieci subject che lo scanner statico rileva nel file nats.json (permission.granted, permission.denied, anomaly.detected, policy.evaluated, cache.invalidated) non sono subject NATS: sono sottoscrizioni a un InMemoryEventPublisher interno usato per pipeline di side-effect (audit, metrics, security alert) generate dalle decisioni autorizzative. Vengono restituite dallo scanner perché l'API subscribe() è omonima a quella NATS, ma il dispatch avviene in-process. In altre parole, il servizio mantiene una proiezione locale dell'identità ma la mantiene per pull (HTTP) o per evento puntuale di lifecycle (cancellazione utente), non per stream continuo.
Dipendenze e dipendenti
Dipende da (servizi che questo servizio chiama):
- Nessuna dipendenza applicativa diretta.
Consumato da (chi chiama questo servizio):
node-user-authfrontend Vue
Infrastruttura (PostgreSQL, NATS, Redis, MinIO) non è elencata qui — vedi sezione Architettura del singolo servizio.