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>
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
- Standard suite: l'autorità è
agile-services(172.18.0.1:4214Tenant MS +nexus_tenant_db.sso_identities). TRPG è un peer che ha già adottato lo standard. NIS2 deve adottarlo, non copiare TRPG. - PHP vanilla resta: NIS2 mantiene la sua architettura (no porting a Node.js). I componenti PHP di TRPG (
SsoHelper.php,HubJwtVerifier.php, schemaactive_sessions) sono riusabili 1:1 previo adattamento namespace/config. - Backward compatibility: ogni cambio è additivo. Utenti esistenti continuano a loggare con flusso locale fino a che il backfill SSO non li collega.
- Dual-mode SSO obbligatorio: se Tenant MS down → fallback locale automatico (no down NIS2).
- No big bang: 5 fasi rilasciabili indipendentemente, ognuna con rollback semplice (drop colonne / disable flag).
- Audit trail: ogni nuova feature critica (impersonate, sessions revoke, password reset) logga in
audit_logsconseverityappropriata.
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():
- Se
SSO_MODE != local: provaSsoHelper::login(email, password, 'nis2') - Se SSO 200 OK → estrai
sso_identity_id+password_version→ upsert utente locale (link o crea) → syncpassword_hash - Se SSO 401 → ritorna 401 (no fallback se utente esiste in SSO)
- 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: chiamataPOST {SSO_ENDPOINT}/auth/sso/change-passwordcon 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, comparapassword_version, aggiornapassword_hash+password_versionlocale
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 conrevoked_reason='logout'DELETE /api/auth/sessions→ revoca tutte tranne corrente (cambio password forzato)POST /api/auth/loginmodificato: generajti = bin2hex(random_bytes(16)), insert inactive_sessions, includejtinel JWTBaseController::requireAuth()verifica chejtiesista inactive_sessionse 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-passwordbody{email}→ genera token (SHA256, expires 30min, salvato in tabellapassword_reset_tokensnuova), invia email con linkhttps://nis2.agile.software/reset-password.html?token=XXXPOST /api/auth/reset-passwordbody{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}:
- Verifica
MembershipService::isMember(userId, orgId)(o equivalente NIS2 viauser_organizations) - Revoca sessione corrente con
revoked_reason='context_switch' - Crea nuova
active_sessionsrow con stesso device, nuovo jti - Emette nuovo JWT con
organization_idclaim 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_logsconseverity='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 a1.1.0(con questo allineamento), data corrente, changelogpublic/js/common.js→ fetchversion.jsonall'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.jsonPATCH (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
AuthControllerin 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?" funzionantepublic/forgot-password.html(NUOVO)public/reset-password.html(NUOVO con strength bar)public/settings.html— tab Sicurezza con lista sessioni device + revocapublic/js/common.js— sidebar version footer (click → changelog modal), tenant switcher esposto a chi ha ≥2 org, branding CSS vars auto-loadpublic/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_*.sqlusers_schema_pre_015_*.sqlrefresh_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.shal DBnis2_agile_db - [NIS2] Quando ticket risolto + 7gg passati → switch
SSO_MODE=local→dual(modifica.env+docker compose up -d --force-recreate app) - [NIS2] Recupero valore reale
SSO_INTERNAL_KEYda 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.json→ 1.1.0 (decisione §10.5) a switch confermato
Vedi §3-§6 per Fase 2-5.