[FIX] Connettori per-azienda: aggiunti realmente i 4 metodi controller + UI card (commit 0dc2a11 era guscio vuoto, Edit fallite su ancore errate)

Verificato E2E in prod: list 200 (8 tipi), save m365 201, secret 'client_secret' STRIPPATO (assente da config DB), delete 200, openConnectors servito in companies.html.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-05-30 10:53:48 +02:00
co-authored by Claude Opus 4.8
parent 109aa57d04
commit 789f663419
2 changed files with 218 additions and 0 deletions
@@ -407,4 +407,133 @@ class OrganizationController extends BaseController
]);
}
}
// ══════════════════════════════════════════════════════════════════════
// CONNETTORI PER-AZIENDA (Evidence Automation) — config NON segreta
// I segreti vivono SOLO nel vault-steward (caricati via CLI admin); qui
// si salva solo config non sensibile + alias della chiave vault.
// ══════════════════════════════════════════════════════════════════════
private const CONNECTOR_TYPES = ['m365', 'google', 'aws', 'azure', 'idp', 'edr', 'siem', 'ticketing'];
private function connectorOrgGuard(int $orgId): void
{
$this->requireOrgAccess();
$role = $this->currentUser['role'] ?? '';
if ($role === 'super_admin') {
return;
}
if ($orgId !== $this->getCurrentOrgId()) {
$firmId = $this->currentUser['consulting_firm_id'] ?? null;
$owned = $firmId ? Database::fetchOne(
'SELECT id FROM organizations WHERE id = ? AND consulting_firm_id = ?',
[$orgId, $firmId]
) : null;
if (!$owned) {
$this->jsonError('Accesso negato a questa organizzazione', 403, 'ORG_FORBIDDEN');
}
}
if (!in_array($role, ['org_admin', 'compliance_manager'], true)) {
$this->jsonError('Ruolo non autorizzato a gestire i connettori', 403, 'ROLE_FORBIDDEN');
}
}
/** GET /api/organizations/{id}/connectors */
public function listConnectors(int $id): void
{
$this->connectorOrgGuard($id);
$rows = Database::fetchAll(
'SELECT id, connector_type, display_name, enabled, config, vault_key_alias,
secret_status, last_status, last_checked_at, updated_at
FROM org_connectors WHERE organization_id = ? ORDER BY connector_type',
[$id]
);
foreach ($rows as &$r) {
$r['config'] = $r['config'] ? json_decode($r['config'], true) : new stdClass();
$r['enabled'] = (bool) $r['enabled'];
}
unset($r);
$this->jsonSuccess([
'organization_id' => $id,
'available_types' => self::CONNECTOR_TYPES,
'connectors' => $rows,
]);
}
/** PUT /api/organizations/{id}/connectors — body: {type, display_name?, enabled?, config?, vault_key_alias?, secret_status?} */
public function saveConnector(int $id): void
{
$this->connectorOrgGuard($id);
$type = strtolower((string) $this->getParam('type'));
if (!in_array($type, self::CONNECTOR_TYPES, true)) {
$this->jsonError('Tipo connettore non valido', 422, 'INVALID_TYPE');
}
$config = $this->getParam('config');
if (is_string($config)) { $config = json_decode($config, true); }
if (!is_array($config)) { $config = []; }
foreach (['secret', 'client_secret', 'api_key', 'password', 'private_key', 'token'] as $banned) {
unset($config[$banned]); // difesa: mai segreti nel DB
}
$alias = $this->getParam('vault_key_alias');
if ($alias === null || $alias === '') {
$alias = 'tier1__nis2-app__connector_' . $type . '_org' . $id;
}
$secretStatus = $this->getParam('secret_status');
if (!in_array($secretStatus, ['not_set', 'pending', 'configured'], true)) {
$secretStatus = null;
}
$existing = Database::fetchOne(
'SELECT id FROM org_connectors WHERE organization_id = ? AND connector_type = ?',
[$id, $type]
);
$data = [
'display_name' => $this->getParam('display_name'),
'enabled' => $this->getParam('enabled') ? 1 : 0,
'config' => json_encode($config, JSON_UNESCAPED_UNICODE),
'vault_key_alias' => substr($alias, 0, 190),
];
if ($secretStatus !== null) { $data['secret_status'] = $secretStatus; }
if ($existing) {
$sets = []; $vals = [];
foreach ($data as $k => $v) { $sets[] = "$k = ?"; $vals[] = $v; }
$vals[] = $existing['id'];
Database::query('UPDATE org_connectors SET ' . implode(', ', $sets) . ' WHERE id = ?', $vals);
$connId = (int) $existing['id'];
} else {
$data['organization_id'] = $id;
$data['connector_type'] = $type;
$data['created_by'] = $this->getCurrentUserId();
if (!isset($data['secret_status'])) { $data['secret_status'] = 'not_set'; }
$connId = Database::insert('org_connectors', $data);
}
$this->logAudit('connector_configured', 'organization', $id, ['type' => $type, 'enabled' => $data['enabled']]);
$this->jsonSuccess([
'id' => $connId,
'connector_type' => $type,
'vault_key_alias' => $data['vault_key_alias'],
'cli_hint' => 'Carica il segreto nel vault: docker exec vault-steward node cli/vault-cli.js migrate ' . $alias . ' <key> <value>',
], 'Connettore salvato', $existing ? 200 : 201);
}
/** DELETE /api/organizations/{id}/connectors?type=xxx */
public function deleteConnector(int $id): void
{
$this->connectorOrgGuard($id);
$type = strtolower((string) ($this->getParam('type') ?? ($_GET['type'] ?? '')));
if ($type === '') {
$this->jsonError('Parametro type obbligatorio', 422, 'MISSING_TYPE');
}
Database::query(
'DELETE FROM org_connectors WHERE organization_id = ? AND connector_type = ?',
[$id, $type]
);
$this->logAudit('connector_removed', 'organization', $id, ['type' => $type]);
$this->jsonSuccess(null, 'Connettore rimosso');
}
}