[FEAT] Permessi documentali profilabili per ruolo (Impostazioni > Permessi documentali) — v1.24.2
I diritti sul ciclo di vita dei documenti SGSI sono ora configurabili per ruolo (matrice Ruolo x Azione, org-scoped) invece che cablati. - mig.061: tabella isms_doc_permissions (org x capability x role x allowed) - IsmsModelController: requireDocCapability() sostituisce i requireOrgRole cablati nelle 7 transizioni (edit/submit/approve/publish/reject/archive/new_version); default = comportamento precedente; super_admin sempre ok; endpoint GET/PUT /api/isms/docPermissions (canEdit) - settings.html: tab 'Permessi documentali' con matrice checkbox (lettura per i membri, salvataggio Amministratore/Direzione) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d29195b2c1
commit
e245e76671
@@ -323,7 +323,7 @@ class IsmsModelController extends BaseController
|
||||
/** PUT /api/isms/documents/{id} */
|
||||
public function updateDocument(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||
$this->requireDocCapability('edit');
|
||||
$model = $this->requireModel();
|
||||
$body = $this->getJsonBody();
|
||||
$row = Database::fetchOne('SELECT id, status FROM isms_documents WHERE id = ? AND isms_model_id = ?', [$id, $model['id']]);
|
||||
@@ -379,10 +379,44 @@ class IsmsModelController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/* ── Permessi documentali profilabili per ruolo (Impostazioni > Permessi documentali) ── */
|
||||
private const DOC_CAPS = ['edit', 'submit', 'approve', 'publish', 'reject', 'archive', 'new_version'];
|
||||
private const DOC_ROLES = ['org_admin', 'compliance_manager', 'board_member', 'auditor', 'employee', 'consultant'];
|
||||
private const DOC_CAP_DEFAULTS = [
|
||||
'edit' => ['org_admin', 'compliance_manager', 'board_member'],
|
||||
'submit' => ['org_admin', 'compliance_manager', 'board_member'],
|
||||
'approve' => ['org_admin', 'compliance_manager', 'board_member'],
|
||||
'publish' => ['org_admin', 'compliance_manager', 'board_member'],
|
||||
'reject' => ['org_admin', 'compliance_manager', 'board_member'],
|
||||
'archive' => ['org_admin', 'compliance_manager', 'board_member'],
|
||||
'new_version' => ['org_admin', 'compliance_manager', 'board_member'],
|
||||
];
|
||||
|
||||
private function docCapabilityAllowed(string $cap, string $role): bool
|
||||
{
|
||||
$row = Database::fetchOne(
|
||||
'SELECT allowed FROM isms_doc_permissions WHERE organization_id = ? AND capability = ? AND role = ?',
|
||||
[$this->getCurrentOrgId(), $cap, $role]);
|
||||
if (is_array($row) && array_key_exists('allowed', $row)) {
|
||||
return (bool) $row['allowed'];
|
||||
}
|
||||
return in_array($role, self::DOC_CAP_DEFAULTS[$cap] ?? [], true);
|
||||
}
|
||||
|
||||
private function requireDocCapability(string $cap): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
if ($this->isDemo) { return; }
|
||||
if ($this->currentOrgRole === 'super_admin') { return; }
|
||||
if (!$this->docCapabilityAllowed($cap, (string) $this->currentOrgRole)) {
|
||||
$this->jsonError('Permesso negato per questa azione sui documenti. Configurabile in Impostazioni > Permessi documentali.', 403, 'DOC_FORBIDDEN');
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /api/isms/documents/{id}/submit bozza -> in revisione */
|
||||
public function submitDocument(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||
$this->requireDocCapability('submit');
|
||||
$doc = $this->requireDocument($id);
|
||||
if ($doc['status'] !== 'draft') { $this->jsonError('Solo una bozza può essere inviata in revisione', 409, 'BAD_STATE'); }
|
||||
Database::update('isms_documents', ['status' => 'review', 'review_note' => null, 'updated_at' => date('Y-m-d H:i:s')], 'id = ?', [$id]);
|
||||
@@ -393,7 +427,7 @@ class IsmsModelController extends BaseController
|
||||
/** POST /api/isms/documents/{id}/approve in revisione -> approvato */
|
||||
public function approveDocument(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||
$this->requireDocCapability('approve');
|
||||
$doc = $this->requireDocument($id);
|
||||
if (!in_array($doc['status'], ['review', 'draft'], true)) { $this->jsonError('Stato non valido per l\'approvazione', 409, 'BAD_STATE'); }
|
||||
$now = date('Y-m-d H:i:s');
|
||||
@@ -408,7 +442,7 @@ class IsmsModelController extends BaseController
|
||||
/** POST /api/isms/documents/{id}/publish approvato -> pubblicato (in vigore) */
|
||||
public function publishDocument(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||
$this->requireDocCapability('publish');
|
||||
$doc = $this->requireDocument($id);
|
||||
if ($doc['status'] !== 'approved') { $this->jsonError('Solo un documento approvato può essere pubblicato', 409, 'BAD_STATE'); }
|
||||
$now = date('Y-m-d H:i:s'); $today = date('Y-m-d'); $nextReview = date('Y-m-d', strtotime('+1 year'));
|
||||
@@ -424,7 +458,7 @@ class IsmsModelController extends BaseController
|
||||
/** POST /api/isms/documents/{id}/reject in revisione -> bozza (con nota) */
|
||||
public function rejectDocument(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||
$this->requireDocCapability('reject');
|
||||
$doc = $this->requireDocument($id);
|
||||
if ($doc['status'] !== 'review') { $this->jsonError('Solo un documento in revisione può essere rimandato in bozza', 409, 'BAD_STATE'); }
|
||||
$note = trim((string) ($this->getJsonBody()['note'] ?? ''));
|
||||
@@ -436,7 +470,7 @@ class IsmsModelController extends BaseController
|
||||
/** POST /api/isms/documents/{id}/archive pubblicato -> archiviato */
|
||||
public function archiveDocument(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||
$this->requireDocCapability('archive');
|
||||
$doc = $this->requireDocument($id);
|
||||
if ($doc['status'] !== 'published') { $this->jsonError('Solo un documento pubblicato può essere archiviato', 409, 'BAD_STATE'); }
|
||||
$now = date('Y-m-d H:i:s');
|
||||
@@ -448,7 +482,7 @@ class IsmsModelController extends BaseController
|
||||
/** POST /api/isms/documents/{id}/newVersion pubblicato/archiviato -> nuova bozza */
|
||||
public function newVersionDocument(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||
$this->requireDocCapability('new_version');
|
||||
$doc = $this->requireDocument($id);
|
||||
if (!in_array($doc['status'], ['published', 'archived', 'approved'], true)) { $this->jsonError('Nuova versione possibile solo da documento approvato/pubblicato/archiviato', 409, 'BAD_STATE'); }
|
||||
$major = !empty($this->getJsonBody()['major']);
|
||||
@@ -563,6 +597,54 @@ class IsmsModelController extends BaseController
|
||||
exit;
|
||||
}
|
||||
|
||||
/** GET /api/isms/docPermissions — matrice effettiva Ruolo x Azione (default + override org). Lettura aperta ai membri. */
|
||||
public function getDocPermissions(): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$canEdit = in_array((string) $this->currentOrgRole, ['super_admin', 'org_admin', 'board_member'], true);
|
||||
$stored = [];
|
||||
foreach (Database::fetchAll('SELECT capability, role, allowed FROM isms_doc_permissions WHERE organization_id = ?', [$orgId]) as $r) {
|
||||
$stored[$r['capability'] . '|' . $r['role']] = (int) $r['allowed'];
|
||||
}
|
||||
$caps = [];
|
||||
foreach (self::DOC_CAPS as $cap) {
|
||||
$roles = [];
|
||||
foreach (self::DOC_ROLES as $role) {
|
||||
$key = $cap . '|' . $role;
|
||||
$roles[$role] = array_key_exists($key, $stored)
|
||||
? (bool) $stored[$key]
|
||||
: in_array($role, self::DOC_CAP_DEFAULTS[$cap] ?? [], true);
|
||||
}
|
||||
$caps[] = ['capability' => $cap, 'roles' => $roles];
|
||||
}
|
||||
$this->jsonSuccess(['capabilities' => $caps, 'roleList' => self::DOC_ROLES, 'canEdit' => $canEdit]);
|
||||
}
|
||||
|
||||
/** PUT /api/isms/docPermissions — salva la matrice. Body: { entries:[{capability,role,allowed}] } */
|
||||
public function saveDocPermissions(): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'board_member']);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$entries = $this->getJsonBody()['entries'] ?? [];
|
||||
if (!is_array($entries)) { $this->jsonError('Formato non valido', 400, 'BAD_REQUEST'); }
|
||||
$now = date('Y-m-d H:i:s'); $count = 0;
|
||||
foreach ($entries as $e) {
|
||||
$cap = (string) ($e['capability'] ?? ''); $role = (string) ($e['role'] ?? '');
|
||||
if (!in_array($cap, self::DOC_CAPS, true) || !in_array($role, self::DOC_ROLES, true)) { continue; }
|
||||
$allowed = !empty($e['allowed']) ? 1 : 0;
|
||||
$existing = Database::fetchOne('SELECT id FROM isms_doc_permissions WHERE organization_id = ? AND capability = ? AND role = ?', [$orgId, $cap, $role]);
|
||||
if ($existing) {
|
||||
Database::update('isms_doc_permissions', ['allowed' => $allowed, 'updated_at' => $now], 'id = ?', [$existing['id']]);
|
||||
} else {
|
||||
Database::insert('isms_doc_permissions', ['organization_id' => $orgId, 'capability' => $cap, 'role' => $role, 'allowed' => $allowed, 'updated_at' => $now]);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
$this->logAudit('isms_doc_permissions_updated', 'organization', $orgId, ['count' => $count]);
|
||||
$this->jsonSuccess(['saved' => $count], 'Permessi documentali aggiornati');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/isms/documents/ai-generate
|
||||
* Body: { doc_type, title? } - genera una bozza con AI (grounding fonti certe).
|
||||
|
||||
Reference in New Issue
Block a user