" * * Sicurezza by design: * - DRY-RUN di default: senza --apply NON scrive nulla, stampa solo il piano. * - Target per email/cognome ESATTI (IN parametrico), mai UPDATE ciechi/LIKE. * - --expect=N: se il match non risolve ESATTAMENTE N utenti -> ABORT (anti grant accidentale). * - Password NON in argv: letta da env PROV_PASSWORD (non compare in `ps`). * - password_version lasciato invariato -> la sync SSO non sovrascrive (vedi sso-password-sync.sh). * - must_change_password=0; membership idempotente (UNIQUE user+org). * - Tutte le scritture in UNA transazione (rollback on error). Nessun segreto hardcoded. * * Args: * --match=Cognome1,Cognome2 cognomi da cercare (CSV) [opz.] * --emails=a@b,c@d email esatte (CSV) [opz.] * --role=super_admin users.role da impostare [opz.; se assente non tocca il ruolo] * --orgs=151:primary,152 membership da garantire (org[:primary]) CSV [opz.] * --set-password imposta password_hash da env PROV_PASSWORD [opz.] * --expect=N n. atteso di utenti risolti (safety) [opz. ma consigliato] * --apply esegue le scritture (default: dry-run) */ error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE); /* --- prime DB_* da PID1 (creds vault-injected non presenti nell'env di docker exec) --- */ $environ = @file_get_contents('/proc/1/environ'); if ($environ) { foreach (explode("\0", $environ) as $kv) { if ($kv === '' || strpos($kv, '=') === false) continue; [$k, $v] = explode('=', $kv, 2); if (strncmp($k, 'DB_', 3) === 0) { putenv("$k=$v"); $_ENV[$k] = $v; $_SERVER[$k] = $v; } } } require_once '/var/www/nis2-agile/application/config/env.php'; require_once '/var/www/nis2-agile/application/config/database.php'; /* --- arg parsing --- */ $args = []; foreach (array_slice($argv, 1) as $a) { if (preg_match('/^--([^=]+)(?:=(.*))?$/', $a, $m)) $args[$m[1]] = $m[2] ?? true; } $matchNames = isset($args['match']) ? array_filter(array_map('trim', explode(',', $args['match']))) : []; $emails = isset($args['emails']) ? array_filter(array_map('trim', explode(',', $args['emails']))) : []; $role = $args['role'] ?? null; $orgsRaw = isset($args['orgs']) ? array_filter(array_map('trim', explode(',', $args['orgs']))) : []; $setPw = isset($args['set-password']); $expect = isset($args['expect']) ? (int)$args['expect'] : null; $apply = isset($args['apply']); if (!$matchNames && !$emails) { fwrite(STDERR, "ERRORE: specifica --match e/o --emails\n"); exit(2); } $validRoles = ['super_admin','org_admin','compliance_manager','board_member','auditor','employee','consultant']; if ($role !== null && !in_array($role, $validRoles, true)) { fwrite(STDERR, "ERRORE: role '$role' non valido\n"); exit(2); } $pw = null; if ($setPw) { $pw = getenv('PROV_PASSWORD') ?: ''; if ($pw === '') { fwrite(STDERR, "ERRORE: --set-password ma env PROV_PASSWORD vuota\n"); exit(2); } } /* parse orgs: ['151'=>true(primary), '152'=>false] */ $orgs = []; foreach ($orgsRaw as $o) { $parts = explode(':', $o); $orgs[(int)$parts[0]] = (isset($parts[1]) && strtolower($parts[1]) === 'primary'); } echo "==================================================================\n"; echo " provision_testers.php — " . ($apply ? "*** APPLY (scrive) ***" : "DRY-RUN (sola lettura)") . "\n"; echo "==================================================================\n"; /* --- risoluzione utenti (esatti) --- */ $conds = []; $params = []; if ($matchNames) { $like = []; foreach ($matchNames as $n) { $like[] = 'full_name LIKE ?'; $params[] = '%' . $n . '%'; } $conds[] = '(' . implode(' OR ', $like) . ')'; } if ($emails) { $conds[] = 'email IN (' . implode(',', array_fill(0, count($emails), '?')) . ')'; $params = array_merge($params, $emails); } $sql = "SELECT id,email,full_name,role,is_active,sso_identity_id,password_version,must_change_password FROM users WHERE " . implode(' OR ', $conds) . " ORDER BY id"; $users = Database::fetchAll($sql, $params); if (!$users) { fwrite(STDERR, "Nessun utente risolto.\n"); exit(1); } echo "\nUTENTI RISOLTI (" . count($users) . "):\n"; foreach ($users as $u) { $mem = Database::fetchAll( "SELECT organization_id, role, is_primary FROM user_organizations WHERE user_id=?", [$u['id']] ); $u['_mem'] = $mem; echo sprintf(" #%-4d %-38s %-22s role=%-16s active=%s sso=%-5s pwver=%-3s mcp=%s orgs=%s\n", $u['id'], $u['email'], $u['full_name'], $u['role'], $u['is_active'] ?? '-', $u['sso_identity_id'] ?? '-', $u['password_version'] ?? '-', $u['must_change_password'] ?? '-', $mem ? implode(',', array_map(fn($m)=>$m['organization_id'].($m['is_primary']?'(P)':''), $mem)) : 'NESSUNA(!! onboarding)'); } /* --- safety: expect --- */ if ($expect !== null && count($users) !== $expect) { fwrite(STDERR, "\nABORT: risolti " . count($users) . " utenti ma --expect=$expect. Niente modifiche.\n"); exit(3); } /* --- piano --- */ echo "\nPIANO:\n"; echo $role ? " - role -> '$role' (dove diverso)\n" : " - role: invariato\n"; echo $setPw ? " - password_hash -> (da PROV_PASSWORD), password_version INVARIATO, must_change_password=0\n" : " - password: invariata\n"; echo $orgs ? " - membership garantita su org: " . implode(', ', array_map(fn($k)=>$k.($orgs[$k]?':primary':''), array_keys($orgs))) . " (role org_admin, idempotente)\n" : " - membership: invariata\n"; if (!$apply) { echo "\n[DRY-RUN] nessuna scrittura. Aggiungi --apply per eseguire.\n"; exit(0); } /* --- APPLY (transazione) --- */ $pwHash = $setPw ? password_hash($pw, PASSWORD_DEFAULT) : null; $changes = []; try { Database::beginTransaction(); foreach ($users as $u) { $uid = (int)$u['id']; $line = []; if ($role !== null && $u['role'] !== $role) { Database::update('users', ['role'=>$role], 'id=?', [$uid]); $line[] = "role {$u['role']}->$role"; } if ((int)$u['is_active'] !== 1) { Database::update('users', ['is_active'=>1], 'id=?', [$uid]); $line[] = "is_active 0->1"; } if ($setPw) { Database::update('users', ['password_hash'=>$pwHash, 'must_change_password'=>0], 'id=?', [$uid]); $line[] = "password set (ver invariato)"; } foreach ($orgs as $oid => $isPrimary) { // idempotente: UNIQUE (user_id, organization_id) Database::query( "INSERT INTO user_organizations (user_id, organization_id, role, is_primary) VALUES (?,?, 'org_admin', ?) ON DUPLICATE KEY UPDATE role=VALUES(role), is_primary=VALUES(is_primary)", [$uid, (int)$oid, $isPrimary ? 1 : 0] ); $line[] = "org $oid".($isPrimary?'(P)':''); } $changes[$uid] = $line ? implode('; ', $line) : 'nessuna modifica (gia conforme)'; } Database::commit(); } catch (\Throwable $e) { Database::rollback(); fwrite(STDERR, "\nROLLBACK — errore: " . $e->getMessage() . "\n"); exit(4); } echo "\nAPPLICATO (commit ok):\n"; foreach ($changes as $uid => $c) echo " #$uid: $c\n"; /* --- verifica post --- */ echo "\nVERIFICA POST:\n"; $ids = implode(',', array_map(fn($u)=>(int)$u['id'], $users)); $after = Database::fetchAll("SELECT id,email,role,is_active,must_change_password, (SELECT COUNT(*) FROM user_organizations uo WHERE uo.user_id=users.id) AS org_count FROM users WHERE id IN ($ids) ORDER BY id"); foreach ($after as $a) { echo sprintf(" #%-4d %-38s role=%-14s active=%s mcp=%s org_count=%d %s\n", $a['id'], $a['email'], $a['role'], $a['is_active'], $a['must_change_password'], $a['org_count'], $a['org_count'] > 0 ? 'OK (no onboarding)' : '!! ANCORA 0 ORG'); } echo "\nFatto.\n";