nis2-agile/docs/GAP_TRPG_NIS2_ALIGNMENT.md
DevEnv nis2-agile e4f9e9179e [FEAT] Allineamento NIS2 ↔ TRPG (Fasi 1-5): SSO + Sessions + Reset + Impersonate + Branding
Implementazione completa del progetto allineamento alla suite Evix (TRPG/lg231),
basato sul doc canonico docs/GAP_TRPG_NIS2_ALIGNMENT.md (5 fasi, 18 gap).

Version 1.0.0 → 1.5.0

Fase 1 — SSO Federation (v1.1.0)
- Migration 015_sso_columns: users.sso_identity_id + password_version
- application/services/SsoHelper.php (client SSO dual-mode, cURL nativo, zero deps)
- AuthController::login() + changePassword() conditional SSO (SSO_MODE=local default)

Fase 2 — Multi-device Sessions (v1.2.0)
- Migration 016_active_sessions: tabella + refresh_tokens.session_jti
- BaseController::requireAuth() verifica jti + last_activity throttle + parseDeviceLabel
- login() genera jti, logout/changePassword revoca selettiva
- GET/DELETE /auth/sessions[/{id}]
- UI settings.html tab Sicurezza con lista device + revoca

Fase 3 — Password Reset + Tenant Switcher (v1.3.0)
- Migration 017_password_reset_tokens (TTL 30min, single-use)
- POST /auth/forgot-password (risposta opaca) + reset-password
- Pagine forgot-password.html + reset-password.html (con strength bar)
- EmailService::sendPasswordReset
- POST /auth/switchContext con rotazione JWT + organization_id claim
- Dropdown tenant in sidebar esposto a tutti gli utenti con ≥2 org

Fase 4 — Impersonate + Preferences + Versioning UI (v1.4.0)
- POST /auth/impersonate (super_admin o consulente stesso firm, TTL 1h, audit)
- Migration 018_user_preferences: users.theme/timezone/notif_email/notif_inapp
- GET/PUT /auth/preferences
- Sidebar footer mostra versione + changelog modal su click

Fase 5 — Branding white-label + Auth-gate (v1.5.0)
- Migration 019_firm_branding (logo/colori/brand_name per consulting firm)
- BrandingController GET /branding/current (auth opzionale) + PUT
- common.js auto-applica CSS variables al boot
- public/js/auth-gate.js (gate password client-side per docs riservati, da TRPG)

Skip motivati:
- G15 demo login: simulator esistenti coprono
- G18 refactor controllers: rinviato (~5gg, valore tecnico solo)

Cron sync SSO: AgileHub Ticket #220 aperto a team AGILEHUB per estendere
sso-password-sync.sh al DB nis2_agile_db. Prerequisito per switch SSO_MODE=dual.

Backup files: tutti i file modificati hanno .bak.pre-{fase}-{ts} sia in DEV
sia in /var/www/nis2-agile/.backups/ su Hetzner (rollback ready).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:18:35 +02:00

23 KiB

Progetto di Allineamento NIS2 → TRPG (Suite Evix)

Scopo: portare NIS2 Agile agli stessi standard di Auth/Tenant/Utenti/UX che TRPG ha consolidato nei 54 minor release dal 2026-04 ad oggi. Stato: PIANO (no codice applicato). Da revisionare con l'utente prima di passare alla Fase 1. Riferimento snapshot: TRPG v1.54.1 (build 20260527) — NIS2 v1.0.0 (build 20260415). Data piano: 2026-05-29. Owner proposto: NIS2 dev + supervisione cross-suite via agile-services.


0. Premesse e principi guida

  1. Standard suite: l'autorità è agile-services (172.18.0.1:4214 Tenant MS + nexus_tenant_db.sso_identities). TRPG è un peer che ha già adottato lo standard. NIS2 deve adottarlo, non copiare TRPG.
  2. PHP vanilla resta: NIS2 mantiene la sua architettura (no porting a Node.js). I componenti PHP di TRPG (SsoHelper.php, HubJwtVerifier.php, schema active_sessions) sono riusabili 1:1 previo adattamento namespace/config.
  3. Backward compatibility: ogni cambio è additivo. Utenti esistenti continuano a loggare con flusso locale fino a che il backfill SSO non li collega.
  4. Dual-mode SSO obbligatorio: se Tenant MS down → fallback locale automatico (no down NIS2).
  5. No big bang: 5 fasi rilasciabili indipendentemente, ognuna con rollback semplice (drop colonne / disable flag).
  6. Audit trail: ogni nuova feature critica (impersonate, sessions revoke, password reset) logga in audit_logs con severity appropriata.

1. Quadro sinottico delle gap

ID Area Priorità Effort Rischio Fase
G01 SSO sync DB (sso_identity_id, password_version) P0 M Basso 1
G02 SsoHelper + login federato (dual-mode) P0 M Medio 1
G03 change-password propagato a Tenant MS P0 S Basso 1
G04 Cron sync password_hash da sso_identities P0 S Basso 1
G05 Tabella active_sessions + JWT con jti P0 M Medio 2
G06 Endpoint sessions: GET /list, DELETE /revoke P0 M Basso 2
G07 UI Settings → tab "Sessioni attive" con device list P1 S Basso 2
G08 forgot-password + reset-password + pagina HTML P0 M Basso 3
G09 switchContext endpoint (rotazione JWT) P1 M Medio 3
G10 Dropdown tenant in sidebar/navbar P1 M Basso 3
G11 impersonate per super_admin / consulente P1 S Medio 4
G12 updatePreferences separato da updateProfile P2 S Basso 4
G13 Versioning live: bump version.json + footer UI P1 S Basso 4
G14 Cron agent auto-bump PATCH dopo fix P2 S Basso 4
G15 Demo login dedicato (UX) P2 M Basso 5
G16 BrandingController white-label per consulente P2 M Basso 5
G17 Auth-gate per documenti riservati (presentation/roadmap) P3 XS Nullo 5
G18 Split monolite AuthController / OrganizationController in controller dedicati (ApiKey, ConsultingFirm, …) P2 L Medio 5

Effort: XS<1g, S=1-2g, M=3-5g, L>5g.


2. Fase 1 — SSO Federation (P0)

Obiettivo: NIS2 partecipa al Single Sign-On suite. Utenti riusano password unica tra TRPG/lg231/NIS2.

G01 — Schema DB

Migration docs/sql/015_sso_columns.sql (creata 2026-05-29, non ancora applicata):

ALTER TABLE users
  ADD COLUMN sso_identity_id INT NULL COMMENT 'FK verso nexus_tenant_db.sso_identities.id',
  ADD COLUMN password_version INT NOT NULL DEFAULT 1 COMMENT 'contatore versione password SSO',
  ADD INDEX idx_sso_identity (sso_identity_id);

Acceptance: colonne presenti, utenti esistenti hanno sso_identity_id=NULL e password_version=1. Nessun controller esistente si rompe.

Rollback: ALTER TABLE users DROP COLUMN sso_identity_id, DROP COLUMN password_version;

G02 — SsoHelper + login federato

File nuovo: application/services/SsoHelper.php — clone adattato dal TRPG shared/SsoHelper.php (~150 righe, cURL nativo, zero deps).

Env nuovi in .env:

SSO_ENDPOINT=http://172.18.0.1:4214
SSO_TIMEOUT_MS=3000
SSO_MODE=dual     # local | dual | sso_only
SSO_INTERNAL_KEY=<da vault-steward>

Modifica AuthController::login():

  1. Se SSO_MODE != local: prova SsoHelper::login(email, password, 'nis2')
  2. Se SSO 200 OK → estrai sso_identity_id + password_version → upsert utente locale (link o crea) → sync password_hash
  3. Se SSO 401 → ritorna 401 (no fallback se utente esiste in SSO)
  4. Se SSO unreachable (timeout/connessione) → fallback locale (dual-mode)

Acceptance:

  • Login con credenziali SSO valide → JWT emesso anche per utenti senza row locale
  • Login locale di utenti pre-SSO continua a funzionare
  • Test con Tenant MS spento → login locale OK (dual)
  • SSO_MODE=sso_only → blocca login locali

G03 — Change password propagato

Modifica AuthController::changePassword():

  • Se currentUser['sso_identity_id'] != NULL: chiamata POST {SSO_ENDPOINT}/auth/sso/change-password con JWT utente
  • Se SSO ok → cambio anche locale + password_version++ + invalidazione tutti i refresh_tokens
  • Se utente non SSO: solo cambio locale (comportamento attuale)

Acceptance: cambio password da NIS2 di utente SSO si riflette entro 5 min su TRPG/lg231.

G04 — Cron sync password

Già implementato a livello cron host (vedi /opt/devenv/scripts/sso-password-sync.sh e CLAUDE.md). Da verificare/richiedere:

  • Sync include il DB nis2_agile_db.users
  • Match per sso_identity_id, compara password_version, aggiorna password_hash + password_version locale

Action item: aprire ticket al manutentore agile-services per estendere lo script. Nessun codice in NIS2.

Acceptance: cambio password da TRPG si propaga a NIS2 in ≤5 min.


3. Fase 2 — Session management multi-device (P0)

Obiettivo: l'utente vede e revoca le sue sessioni attive, come su Google/GitHub.

G05 — Schema active_sessions

Migration docs/sql/016_active_sessions.sql (mutuata da TRPG 078_active_sessions.sql):

CREATE TABLE IF NOT EXISTS active_sessions (
    id CHAR(32) NOT NULL PRIMARY KEY COMMENT 'jti del JWT (bin2hex(random_bytes(16)))',
    user_id INT NOT NULL,
    organization_id INT NULL COMMENT 'org attiva al momento del login',
    ip_address VARCHAR(45) NOT NULL,
    user_agent VARCHAR(512) NULL,
    device_label VARCHAR(120) NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    last_activity_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NOT NULL,
    revoked_at TIMESTAMP NULL DEFAULT NULL,
    revoked_reason ENUM(
        'logout','force','password_change','admin','expired_idle','context_switch'
    ) NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_active (user_id, revoked_at, last_activity_at),
    INDEX idx_last_activity (last_activity_at),
    INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

ALTER TABLE refresh_tokens
    ADD COLUMN session_jti CHAR(32) NULL AFTER user_id,
    ADD INDEX idx_refresh_jti (session_jti);

Differenza vs TRPG: aggiunto organization_id (NIS2 ha tenant esplicito) e context_switch già incluso nell'ENUM dal day 1.

G06 — Endpoint sessions

Modificare AuthController aggiungendo:

  • GET /api/auth/sessions → lista sessioni attive utente corrente (id, ip, device_label, created_at, last_activity_at, is_current)
  • DELETE /api/auth/sessions/{id} → revoca singola con revoked_reason='logout'
  • DELETE /api/auth/sessions → revoca tutte tranne corrente (cambio password forzato)
  • POST /api/auth/login modificato: genera jti = bin2hex(random_bytes(16)), insert in active_sessions, include jti nel JWT
  • BaseController::requireAuth() verifica che jti esista in active_sessions e non sia revoked

Acceptance:

  • Login da 3 browser → 3 righe in active_sessions
  • Revoca sessione X → token X non più valido (verify in requireAuth)
  • Last activity aggiornato a ogni richiesta autenticata

G07 — UI Settings: tab Sessioni

Aggiungere a public/settings.html tab "Sicurezza" con:

  • Lista device (icona, browser, OS, IP, ultima attività, "Questo dispositivo" badge)
  • Pulsante "Esci da questo dispositivo" per ogni riga
  • Pulsante "Esci da tutti i dispositivi tranne questo" in cima

Pattern UX copiato da TRPG settings (da screenshottare per riferimento, non da analizzare oltre).


4. Fase 3 — Password reset + Tenant switcher (P0/P1)

G08 — Forgot/Reset password

Endpoint in AuthController:

  • POST /api/auth/forgot-password body {email} → genera token (SHA256, expires 30min, salvato in tabella password_reset_tokens nuova), invia email con link https://nis2.agile.software/reset-password.html?token=XXX
  • POST /api/auth/reset-password body {token, new_password} → verifica token, aggiorna password (+ propaga via SSO se utente federato), invalida tutti i refresh_tokens

Migration docs/sql/017_password_reset.sql:

CREATE TABLE password_reset_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    token_hash CHAR(64) NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,
    used_at TIMESTAMP NULL,
    ip_address VARCHAR(45) NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_token (token_hash),
    INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Pagine HTML nuove:

  • public/forgot-password.html (form email)
  • public/reset-password.html (form new password con strength bar, mutuata da TRPG)

Modifica public/login.html: link "Password dimenticata?" → forgot-password.html (rimuove l'alert "contatta presidenza@...").

Email template: estendere EmailService.php con sendPasswordResetEmail().

Rate limit: 3 richieste/ora per IP+email.

G09 — switchContext endpoint

Endpoint POST /api/auth/switch-context body {organization_id}:

  1. Verifica MembershipService::isMember(userId, orgId) (o equivalente NIS2 via user_organizations)
  2. Revoca sessione corrente con revoked_reason='context_switch'
  3. Crea nuova active_sessions row con stesso device, nuovo jti
  4. Emette nuovo JWT con organization_id claim aggiornato + ruolo org corrispondente

Acceptance: il consulente cambia cliente da dropdown → nuovo JWT con nuovo organization_id → dashboard riflette il cliente nuovo senza F5 hard.

G10 — Dropdown tenant in sidebar

Modificare public/js/common.js:

  • All'avvio: se currentUser.organizations.length > 1 → mostra dropdown nell'header con nome org corrente + freccia
  • Click → modale lista organizations (nome, ruolo, ultimo accesso)
  • Click su org → api.switchContext(orgId) → ricarica con nuovo token

Pattern visivo allineato a TRPG (icona azienda + nome troncato + chevron).


5. Fase 4 — Impersonation + Preferences + Versioning (P1/P2)

G11 — Impersonate

Endpoint POST /api/auth/impersonate body {user_id} (solo super_admin o consulente verso utenti dei clienti del firm):

  • Verifica permessi
  • Emette JWT con claim aggiuntivo impersonated_by = currentUserId
  • Logga in audit_logs con severity='warning'
  • TTL JWT ridotto a 1h (no refresh)

UI: pulsante in admin/users.html ("Entra come questo utente") + banner permanente in tutte le pagine quando si è in modalità impersonate ("Sei loggato come X — esci impersonate").

G12 — User preferences

Endpoint PUT /api/auth/preferences per: preferred_language, theme (chiaro/scuro), timezone, notification_email, notification_inapp.

Separato da PUT /api/auth/profile (full_name, phone) per ragioni di audit e granularità permessi futuri.

Migration docs/sql/018_user_preferences.sql:

ALTER TABLE users
  ADD COLUMN theme ENUM('light','dark','auto') DEFAULT 'auto',
  ADD COLUMN timezone VARCHAR(64) DEFAULT 'Europe/Rome',
  ADD COLUMN notification_email TINYINT(1) DEFAULT 1,
  ADD COLUMN notification_inapp TINYINT(1) DEFAULT 1;

G13 — Versioning UI live

  • public/version.json → bump a 1.1.0 (con questo allineamento), data corrente, changelog
  • public/js/common.js → fetch version.json all'avvio → mostra in footer sidebar (es. v1.1.0 (build 20260530))
  • Tooltip su click apre modale con changelog

G14 — Cron agent bump PATCH

Coordinarsi con AgentAI (vedi CLAUDE.md sez. AgileHub Agent) per:

  • Dopo APPLY_END di un ticket, bumpare version.json PATCH (es. 1.1.0 → 1.1.1)
  • Log VERSION_BUMP in audit trail standard

No codice NIS2: solo ticket di coordinamento.


6. Fase 5 — Polishing & extras (P2/P3)

G15 — Demo login

Endpoint dedicato POST /api/auth/demo-login?scenario=ransomware che pre-loada un demo user con flag is_demo=1 e org-clone con dati seed scenari simulazione esistenti (simulate-nis2.php).

G16 — Branding consulente

Nuovo BrandingController.php + tabella firm_branding (logo_url, primary_color, secondary_color, custom_domain).

Header/sidebar/login leggono branding da consulting_firm_id dell'utente corrente.

G17 — Auth-gate documenti

Copy 1:1 di app/js/auth-gate.js da TRPG → public/js/auth-gate.js. Includere nei documenti riservati (es. eventuali presentation.html, roadmap.html, listino.html futuri).

G18 — Refactor controller

Spezzare AuthController (~600 righe oggi → 1000+ dopo fasi 1-3) in:

  • AuthController (login/logout/refresh/sessions)
  • PasswordController (forgot/reset/change)
  • ProfileController (me/profile/preferences)
  • SsoController (impersonate/switchContext/sso-callback)

Estrarre anche ApiKeyController, ConsultingFirmController se serve (probabilmente sì, in linea con TRPG).


7. Cosa NON è in scope

  • Porting a Node.js: NIS2 resta PHP. La frammentazione microservizi di TRPG (services/auth, services/ai, …) non è obiettivo.
  • 2FA/MFA: rinviato a progetto separato (richiede UX dedicata, TOTP, recovery codes).
  • OAuth2 / social login: rinviato.
  • WebAuthn / passkey: rinviato.

8. Sequenza release e dipendenze

Fase 1 (SSO)          →  Fase 3 (reset password usa SSO propagation)
Fase 1 (SSO)          →  Fase 4 (impersonate logga in SSO audit)
Fase 2 (sessions)     →  Fase 3 (switchContext revoca sessione)
Fase 2 (sessions)     →  Fase 4 (impersonate crea sessione marcata)
Fase 3 (forgot pwd)   ⊥  indipendente da Fase 4/5
Fase 4 (versioning)   ⊥  indipendente
Fase 5                ⊥  tutto opzionale

Path critico: Fase 1 → Fase 2 → Fase 3. Le altre possono parallelizzare.


9. Stima cumulativa

Fase Effort Settimane-uomo
Fase 1 SSO M+M+S+S 1.5
Fase 2 Sessions M+M+S 1.0
Fase 3 Reset+Switch+UI M+M+M 1.5
Fase 4 Imperson.+Prefs+Ver. S+S+S+S 0.8
Fase 5 Demo+Brand+Refactor M+M+XS+L 1.5
Totale ~6 settimane-uomo

10. Decisioni prese (confermate dall'utente 2026-05-29)

# Decisione Scelta Note operative
1 SSO_MODE iniziale local Partiamo prudenti. Switch a dual dopo validazione (almeno 7 giorni post-Fase 1).
2 Backfill utenti SSO A) Lazy on-login Nessuno script massivo. Il link users.sso_identity_id viene popolato al primo login post-SSO se l'email matcha nexus_tenant_db.sso_identities. Utenti che non loggano restano slegati (accettato).
3 Sessioni concorrenti per utente Nessun limite Allineato a TRPG/Google/GitHub. Il consulente lavora da N device, vede tutto in Settings → Sicurezza e revoca a piacere.
4 TTL token reset password 30 min Token SHA256, single-use (used_at), expires_at = NOW() + 30 min. Rate limit 3/h per IP+email.
5 Cadenza version bump B) Per fase (MINOR) Fine Fase 1 → 1.1.0 · Fine Fase 2 → 1.2.0 · Fine Fase 3 → 1.3.0 · Fine Fase 4 → 1.4.0 · Fine Fase 5 → 1.5.0. PATCH automatico via cron agent attivato in Fase 4 (G14).
6 Branding white-label (G16) B) Mantenuto in Fase 5 BrandingController + tabella firm_branding (logo, primary_color, secondary_color, custom_domain) abilitano white-label consulente. Effort M, valore commerciale (upsell).

11. Riferimenti

  • TRPG schema sessions: /var/www/trpg-agile/sql/078_active_sessions.sql (Hetzner, accesso via chiave temp)
  • TRPG SsoHelper: /var/www/trpg-agile/shared/SsoHelper.php
  • TRPG HubJwtVerifier: /var/www/trpg-agile/shared/HubJwtVerifier.php
  • TRPG AuthController: /var/www/trpg-agile/services/auth/controllers/AuthController.php (v1.54.1, 1365 righe)
  • Standard SSO suite: agile-services/docs/SPEC_SSO_SINGLE_SIGN_ON.md + ISTRUZIONI_SSO_PRODOTTI.md
  • Cron sync attuale: agile-services/scripts/sso-password-sync.sh
  • NIS2 schema utenti: docs/sql/001_initial_schema.sql (righe 46-80)

12. Stato esecuzione

Tutte le 5 fasi — COMPLETATE 2026-05-29

Fase Esito Version
Fase 1 — SSO Federation 1.1.0
Fase 2 — Multi-device Sessions 1.2.0
Fase 3 — Password Reset + Context Switch 1.3.0
Fase 4 — Impersonate + Preferences + Versioning UI 1.4.0
Fase 5 — Branding + Auth-gate (G15 e G18 skip) 1.5.0

Skip motivati

  • G15 Demo login: i simulator esistenti (simulate-nis2.php, simulate-nis2-big.php, simulate-nis2-b2b.php) già coprono i flussi demo end-to-end. Un endpoint dedicato non aggiungerebbe valore — i simulator creano org reali con dati seed, esperienza più ricca di un demo-mode singleton.
  • G18 Refactor controllers: split di AuthController in 4 controller separati è investimento tecnico (~5gg) senza valore funzionale immediato. Rinviato a sessione dedicata se diventa necessario per leggibilità.

Migrations SQL applicate (DB host Hetzner)

File Tabella/Colonne
015_sso_columns.sql users.sso_identity_id, users.password_version
016_active_sessions.sql active_sessions (jti tracking), refresh_tokens.session_jti
017_password_reset.sql password_reset_tokens
018_user_preferences.sql users.theme/timezone/notif_email/notif_inapp
019_firm_branding.sql firm_branding (white-label)

Endpoint API nuovi (dopo Fase 1-5)

POST   /api/auth/forgot-password    (Fase 3 / G08)
POST   /api/auth/reset-password     (Fase 3 / G08)
POST   /api/auth/switchContext      (Fase 3 / G09)
POST   /api/auth/impersonate        (Fase 4 / G11)
GET    /api/auth/preferences        (Fase 4 / G12)
PUT    /api/auth/preferences        (Fase 4 / G12)
GET    /api/auth/sessions           (Fase 2 / G06)
DELETE /api/auth/sessions           (Fase 2 / G06)
DELETE /api/auth/sessions/{id}      (Fase 2 / G06)
GET    /api/branding/current        (Fase 5 / G16)
PUT    /api/branding                (Fase 5 / G16)

UI changes

  • public/login.html — link "Password dimenticata?" funzionante
  • public/forgot-password.html (NUOVO)
  • public/reset-password.html (NUOVO con strength bar)
  • public/settings.html — tab Sicurezza con lista sessioni device + revoca
  • public/js/common.js — sidebar version footer (click → changelog modal), tenant switcher esposto a chi ha ≥2 org, branding CSS vars auto-load
  • public/js/auth-gate.js (NUOVO) — gate password lato client per documenti riservati

File PHP nuovi / modificati

File Tipo Note
application/services/SsoHelper.php NUOVO Client SSO dual-mode
application/controllers/BrandingController.php NUOVO White-label firm
application/controllers/AuthController.php MODIFICATO +12 metodi: login (jti+SSO), changePassword, refresh, logout, forgotPassword, resetPassword, switchContext, impersonate, getPreferences, updatePreferences, listSessions, revokeSession, revokeAllSessions
application/controllers/BaseController.php MODIFICATO requireAuth verifica jti + last_activity throttle, parseDeviceLabel, generateRefreshToken con session_jti
application/services/EmailService.php MODIFICATO +sendPasswordReset
application/config/config.php MODIFICATO +RATE_LIMIT_AUTH_FORGOT, +PASSWORD_RESET_TTL_SECONDS
public/index.php MODIFICATO +12 route + branding controller

Comportamento utente attivo OGGI

  • SSO dormiente (SSO_MODE=local) → login esattamente come prima
  • Multi-device sessions ATTIVE → ogni login crea row in active_sessions, JWT senza jti rifiutato
  • Password reset ATTIVO → link funzionante da login.html
  • Context switch ATTIVO → utenti multi-org vedono dropdown in sidebar
  • Impersonate ATTIVO → solo super_admin o consulente stesso firm (NB: nessuna UI ancora, solo endpoint)
  • Preferences ATTIVO → endpoint disponibili (NB: UI tab dedicato non ancora aggiunto)
  • Versioning footer ATTIVO → versione visibile in sidebar di ogni pagina
  • Branding ATTIVO → defaults applicati; consulente può PUT branding del suo firm

Cron sync SSO (dipendenza esterna)

AgileHub Ticket #220 aperto a team AGILEHUB per estendere sso-password-sync.sh al DB nis2_agile_db. Da risolvere prima del switch SSO_MODE=dual.

Backup completi (rollback ready)

Tutti i file modificati hanno backup .bak.pre-{fase}-{timestamp} in:

  • /projects/nis2-agile/application/controllers/
  • /projects/nis2-agile/public/
  • /var/www/nis2-agile/.backups/ (Hetzner)

DB backups schema:

  • host_users_pre_017_*.sql
  • users_schema_pre_015_*.sql
  • refresh_tokens_pre_016_*.sql

Fase 1 — SSO Federation: COMPLETATA 2026-05-29

Step Esito Note
G01 — Migration 015_sso_columns.sql Applicata su prod Backup .backups/users_schema_pre_015_20260529-073134.sql. Idempotenza verificata. 0 utenti in prod (nessun cliente).
G02 — application/services/SsoHelper.php Deployato Mutuato da TRPG shared/SsoHelper.php. Lint OK in container. getMode()=local.
G03 — AuthController::login() + changePassword() Modificato Backup .bak.pre-sso-20260529. Smoke test 401 OK. Logica dietro if(!$sso->isLocalOnly()) → no-op con SSO_MODE=local.
G04 — Vault + .env Configurato Vault: tier1__nis2-app__sso/internal_key (placeholder). .env: SSO_ENDPOINT/SSO_TIMEOUT_MS/SSO_MODE=local.
Cron sync (out-of-scope NIS2) Ticket aperto AgileHub Ticket #220 a team AGILEHUB.

Comportamento utente: invariato. Tutte le modifiche sono dietro feature flag SSO_MODE. Switch a dual previsto dopo almeno 7gg di validazione (decisione §10.1).

Prossimi step

  • [manutentore] AgileHub Ticket #220 → estensione sso-password-sync.sh al DB nis2_agile_db
  • [NIS2] Quando ticket risolto + 7gg passati → switch SSO_MODE=localdual (modifica .env + docker compose up -d --force-recreate app)
  • [NIS2] Recupero valore reale SSO_INTERNAL_KEY da Tenant MS prima dello switch e replace in vault con: docker exec vault-steward node /app/cli/vault-cli.js migrate tier1__nis2-app__sso internal_key '<real>'
  • [NIS2] Bump public/version.json1.1.0 (decisione §10.5) a switch confermato

Vedi §3-§6 per Fase 2-5.