node-user-profiling

Profilazione utente, ruoli, gruppi e permessi applicativi.
identity
/user-profilingauth: nginx

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 (tipogruppo progetto o organizzativo), invalidazione cache transitiva
  • Check autorizzazione singolo e batch (POST /api/v1/authorization/check e /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 da X-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/):

LayerContenuto
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 il parentRoleId, associa permessi resource:action e lo assegna a gruppi/team o singoli utenti con eventuale scadenza
  • Check permessi runtime da altri microservizi: un servizio applicativo (es. node-orchestrator) invia POST /api/v1/authorization/check con { 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-auth con header x-user-id e 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 via GET /api/v1/admin/users/exists o pubblicare un evento di cancellazione su NATS (vedi sezione eventing) per propagare l'hard-delete GDPR
  • Investigazione security: l'operatore SOC consulta /api/v1/anomalies per identificare utenti con risk score elevato, ricostruisce la timeline dei tentativi negati tramite gli aggregati esposti da /api/v1/stats e disabilita ruoli compromessi
  • Operazioni batch internal: pipeline di sistema chiamano /api/v1/internal/users/:userId/roles/batch (protette da X-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 claimsSetup
PostgRESTAutomatico: 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)
HasuraAutomatico via Hasura-Claims-Map
Nessun setupTutte 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

FunzioneRitornoNote
public.current_user_id()INTEGEREstrae userId dai claims; NULL se assente
public.current_user_dept_id()INTEGEREstrae departmentId; NULL se assente
public.current_user_team_id()INTEGEREstrae teamId; NULL se assente

Check permesso (boolean)

FunzioneRitornoUso tipico
public.can(resource, action)BOOLEANScope all / default
public.can_own(resource, action, owner_id)BOOLEANFiltro ownership riga
public.can_dept(resource, action, dept_id)BOOLEANFiltro dipartimento riga
public.can_team(resource, action, team_id)BOOLEANFiltro team riga
public.can_full(resource, action, owner, dept, team, ctx)BOOLEANTutti i parametri + condizioni JSONB
public.authorize(resource, action)BOOLEANRBAC+ABAC combinati (include policy)
public.authorize_detail(...)JSONBDecisione strutturata per audit/debug

Ruoli

FunzioneRitorno
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

FunzioneRitornoUso
public.get_my_roles()SETOF role_rowRuoli effettivi (direct + inherited + group)
public.get_my_permissions()SETOF permission_rowPermessi materializzati
public.resource_permissions(resource)1 row × 12 booleanMatrice {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 = true impedisce al planner di "spingere" filtri esterni prima del WHERE → niente leakage informativo
  • Se i claims non sono in sessione, le current_user_*_id() ritornano NULL= produce UNKNOWN → 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

SintomoCausaFix
Tutte le can_* ritornano FALSErequest.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.jsset_config(..., false) invece di true → claims solo per la query correnteUsare 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 permessoManca teamId nei claims JWTAggiungere teamId al payload del token (lato node-user-auth) o ai claims set_config
permission denied for schema authHelper 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 permessoManca WITH (security_barrier = true) → planner pusha filtri esterni dentro la vistaAggiungere security_barrier = true a tutte le viste autorizzanti
Performance degradata su grandi datasetFunzioni helper chiamate per riga invece che una volta a queryStrutturare query con CROSS JOIN su resource_permissions(...)STABLE, valutata una volta)

Identità & esposizione

CampoValore
Categoriaidentity
Versione cluster1.0.0
Imagegitea.pzetatouch.it/pzeta_touch/node-user-profiling:1.0.14
URL pubblicohttps://ditta.pzeta.it/user-profiling
Path regex ingress`/user-profiling(/
Rewrite a backend/$2
DNS internonode-user-profiling-ditta.ditta.svc.cluster.local:3000
Auth nginxauth_requestnode-user-auth
Repositorynode-user-profiling
Endpoint REST64 (vedi sezione "API reference")

Endpoint operazionali

Endpoint convenzionali esposti da tutti i microservizi PZeta basati su @pzeta/fastify-utils:

Path pubblicoScopo
https://ditta.pzeta.it/user-profiling/healthliveness probe
https://ditta.pzeta.it/user-profiling/readyreadiness probe
https://ditta.pzeta.it/user-profiling/metricsmetriche Prometheus
https://ditta.pzeta.it/user-profiling/api-docs.jsonspec OpenAPI runtime (richiede OPENAPI_EXPOSE_IN_PRODUCTION=true)
https://ditta.pzeta.it/user-profiling/api-docsSwagger UI (solo in NODE_ENV !== production)

Configurazione

Variabili d'ambiente che un integratore deve conoscere (per la lista completa vedi .env.example del repo):

VariabileRuolo
DATABASE_HOST / _PORT / _USER / _PASSWORD / _NAMEConnessione PostgreSQL — ruoli, permessi, gruppi, team, policy, audit
DATABASE_SCHEMADefault auth; isola gli oggetti di profilazione dal resto del DB applicativo
DATABASE_MAX_CONNECTIONSPool size del driver pg; alzare nei profili high-performance
AUTH_SERVICE_PROFILEProfilo 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 / _DBCache distribuita delle decisioni autorizzative; off in dev
CACHE_STANDARD_TTL / CACHE_SENSITIVE_TTL / CACHE_DENY_TTLTTL in secondi per esiti standard, sensibili e di rifiuto
NGINX_AUTH_ENABLED / NGINX_AUTH_STRICTAbilita l'endpoint /nginx-auth e ne governa la modalità strict (rifiuta header malformati)
TRUST_X_USER_ID_HEADERForza il rispetto di x-user-id solo se ricevuto da nginx upstream; false in produzione esposta
SECURITY_RISK_THRESHOLD / SECURITY_ANALYSIS_WINDOW_HOURSSoglia di rischio e finestra di analisi per anomaly detection
RATE_LIMIT_ENABLED / RATE_LIMIT_MAX_REQUESTSRate limit sliding window per utente/IP via @pzeta/fastify-utils
MIGRATION_STRICT_CHECKSUMSe true, una migrazione modificata rispetto al checksum registrato blocca l'avvio
INTERNAL_API_TOKENToken (min 16 char) per le route /api/v1/internal/* di operazioni S2S
NATS_URL / NATS_USER / NATS_PASSWORD / NATS_NAMESPACEConnessione NATS opzionale; se assente, il servizio gira in modalità solo-HTTP
USER_DELETED_SUBJECTOverride 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):

Infrastruttura (PostgreSQL, NATS, Redis, MinIO) non è elencata qui — vedi sezione Architettura del singolo servizio.

Loading OpenAPI…
Loading NATS contracts…