[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:
parent
109aa57d04
commit
789f663419
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -432,6 +432,9 @@
|
||||
|
||||
<div class="company-card-footer">
|
||||
<span class="company-card-sector">${escapeHtml(sectorLabel)}</span>
|
||||
<button class="btn btn-ghost btn-sm" onclick="openConnectors(${orgId}, '${escapeHtml(name).replace(/'/g, "\\'")}')" title="Connettori Evidence Automation">
|
||||
Connettori
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="manageCompany(${orgId})">
|
||||
Gestisci
|
||||
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
@ -440,6 +443,92 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Connettori per-azienda (Evidence Automation) ─────────────
|
||||
const CONNECTOR_LABELS = {
|
||||
m365: 'Microsoft 365 / Entra ID', google: 'Google Workspace', aws: 'Amazon Web Services',
|
||||
azure: 'Microsoft Azure', idp: 'Identity Provider', edr: 'EDR / XDR',
|
||||
siem: 'SIEM / SOC', ticketing: 'Ticketing (Jira/ServiceNow)'
|
||||
};
|
||||
let _connOrgId = null;
|
||||
|
||||
async function openConnectors(orgId, orgName) {
|
||||
_connOrgId = orgId;
|
||||
try {
|
||||
const res = await fetch(`/api/organizations/${orgId}/connectors`, {
|
||||
headers: { 'Authorization': `Bearer ${api.token}`, 'X-Organization-Id': orgId }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) { showConnModal(orgName, `<p style="color:#ef4444;">${escapeHtml(data.message || 'Errore')}</p>`); return; }
|
||||
const existing = {};
|
||||
(data.data.connectors || []).forEach(c => existing[c.connector_type] = c);
|
||||
const rows = (data.data.available_types || []).map(t => {
|
||||
const c = existing[t] || {};
|
||||
const cfg = (c.config && typeof c.config === 'object') ? c.config : {};
|
||||
const ss = c.secret_status || 'not_set';
|
||||
const ssBadge = { not_set: ['Segreto non impostato', '#9ca3af'], pending: ['Segreto in attesa', '#eab308'], configured: ['Segreto configurato', '#22c55e'] }[ss];
|
||||
return `<div class="conn-row" data-type="${t}" style="border:1px solid var(--gray-200);border-radius:8px;padding:12px;margin-bottom:10px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<strong>${escapeHtml(CONNECTOR_LABELS[t] || t)}</strong>
|
||||
<label style="font-size:.8rem;"><input type="checkbox" class="c-en" ${c.enabled ? 'checked' : ''}> Attivo</label>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px;">
|
||||
<input class="c-tenant" placeholder="tenant_id / account_id" value="${escapeHtml(cfg.tenant_id || cfg.account_id || '')}" style="padding:6px;border:1px solid var(--gray-300);border-radius:6px;">
|
||||
<input class="c-client" placeholder="client_id / app_id" value="${escapeHtml(cfg.client_id || cfg.app_id || '')}" style="padding:6px;border:1px solid var(--gray-300);border-radius:6px;">
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
|
||||
<span style="font-size:.72rem;color:${ssBadge[1]};">● ${ssBadge[0]}</span>
|
||||
<button class="btn btn-sm btn-primary" onclick="saveConn('${t}', this)">Salva</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
showConnModal(orgName, `
|
||||
<p style="font-size:.82rem;color:var(--gray-600);margin-bottom:12px;">
|
||||
Configura i connettori di <strong>Evidence Automation</strong>. Qui si salvano solo i parametri
|
||||
NON segreti (tenant/client id). Il <strong>client secret</strong> va caricato nel vault dall'amministratore
|
||||
(il comando viene mostrato dopo il salvataggio).</p>${rows}`);
|
||||
} catch (e) { showConnModal(orgName, '<p style="color:#ef4444;">Errore di connessione</p>'); }
|
||||
}
|
||||
|
||||
function showConnModal(orgName, body) {
|
||||
const old = document.getElementById('conn-modal'); if (old) old.remove();
|
||||
document.body.insertAdjacentHTML('beforeend', `
|
||||
<div id="conn-modal" onclick="if(event.target===this)closeConn()" style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px;">
|
||||
<div style="background:var(--card-bg,#fff);border-radius:12px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;padding:22px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
|
||||
<h3 style="margin:0;">Connettori — ${escapeHtml(orgName)}</h3>
|
||||
<button class="btn btn-ghost btn-sm" onclick="closeConn()">✕</button>
|
||||
</div>${body}
|
||||
</div></div>`);
|
||||
}
|
||||
function closeConn() { const m = document.getElementById('conn-modal'); if (m) m.remove(); }
|
||||
|
||||
async function saveConn(type, btn) {
|
||||
const row = btn.closest('.conn-row');
|
||||
const payload = {
|
||||
type,
|
||||
enabled: row.querySelector('.c-en').checked,
|
||||
config: { tenant_id: row.querySelector('.c-tenant').value.trim(), client_id: row.querySelector('.c-client').value.trim() },
|
||||
secret_status: 'pending'
|
||||
};
|
||||
btn.disabled = true; btn.textContent = '...';
|
||||
try {
|
||||
const res = await fetch(`/api/organizations/${_connOrgId}/connectors`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': `Bearer ${api.token}`, 'X-Organization-Id': _connOrgId, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const j = await res.json();
|
||||
btn.disabled = false; btn.textContent = 'Salva';
|
||||
if (j.success) {
|
||||
if (typeof showNotification === 'function') showNotification('Connettore salvato', 'success');
|
||||
if (j.data && j.data.cli_hint) alert('Connettore salvato.\n\nPer caricare il SEGRETO nel vault, l\'amministratore esegue:\n\n' + j.data.cli_hint);
|
||||
} else if (typeof showNotification === 'function') showNotification(j.message || 'Errore', 'error');
|
||||
} catch (e) {
|
||||
btn.disabled = false; btn.textContent = 'Salva';
|
||||
if (typeof showNotification === 'function') showNotification('Errore di connessione', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderEntityBadge(type, voluntary) {
|
||||
if (voluntary) {
|
||||
return `<span class="entity-badge voluntary">Volontaria</span>`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user