Commita governance, user-wizard, operational-feed e catálogo RBAC; adiciona deploy-desk-full.sh, smoke-desk.sh e regra anti-deploy parcial; documenta credencial VM112 @betinplace. Co-authored-by: Cursor <cursoragent@cursor.com>
169 lines
6.7 KiB
JavaScript
169 lines
6.7 KiB
JavaScript
/**
|
|
* Controle de acesso — delega ao Access Control Hub
|
|
* Spec 040 · DS-FE-005
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
const ROLE_CODES = {
|
|
super_admin: 'SU',
|
|
ops_lead: 'CO',
|
|
technician: 'TEC',
|
|
noc: 'NOC',
|
|
sales_admin: 'SAD',
|
|
sales_support: 'SSU',
|
|
finance: 'FIN',
|
|
marketing: 'MKT',
|
|
seo: 'SEO',
|
|
developer: 'DEV',
|
|
devops: 'DVO',
|
|
security_analyst: 'SOC',
|
|
content_editor: 'CMS',
|
|
agentic_operator: 'AIO',
|
|
partner: 'PTR',
|
|
root: 'RO',
|
|
};
|
|
|
|
const ACCESS_ACTIONS = [
|
|
{ id: 'create_user', actionId: 'desk.auth.user.create', label: 'Criar usuário', hint: 'Provisionar conta directa' },
|
|
{ id: 'edit_user', actionId: 'desk.auth.user.edit', label: 'Editar usuário', hint: 'Nome, perfil e metadados' },
|
|
{ id: 'freeze_user', actionId: 'desk.auth.user.freeze', label: 'Congelar conta', hint: 'Bloquear login — SSU nunca' },
|
|
{ id: 'approve_registration', actionId: 'desk.auth.user.approve_registration', label: 'Aprovar pedidos de cadastro', hint: 'SU + CO' },
|
|
{ id: 'manage_passwords', actionId: 'desk.auth.user.password.reset', label: 'Gerenciar senhas', hint: 'Reset admin' },
|
|
{ id: 'reset_2fa', actionId: 'desk.auth.user.2fa.reset', label: 'Resetar 2FA', hint: 'Recuperação autenticador' },
|
|
{ id: 'manage_modules', actionId: 'desk.auth.modules.toggle', label: 'Módulos Desk ON/OFF', hint: 'Feature flags' },
|
|
{ id: 'purge_domain', actionId: 'vm112.domain.purge', label: 'Purge domínio', hint: 'Irreversível' },
|
|
{ id: 'openpanel_delete', actionId: 'vm123_openpanel.site.delete', label: 'Deletar instância OpenPanel', hint: 'SU + CO' },
|
|
{ id: 'validate_billing', actionId: 'desk.billing.state.validate', label: 'Validar billing', hint: 'billing_state' },
|
|
{ id: 'agent_approve', actionId: 'desk.agent.runbook.approve', label: 'Aprovar remediação A7', hint: 'Agentes' },
|
|
];
|
|
|
|
let levelsCache = null;
|
|
let levelsRole = null;
|
|
let catalogEditable = false;
|
|
|
|
function esc(s) {
|
|
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
async function loadLevelsForRole(roleId) {
|
|
if (levelsCache && levelsRole === roleId) return levelsCache;
|
|
const r = await fetchWithTimeout('/api/v1/rbac/actions', { headers: authHeaders() });
|
|
if (!r.ok) throw new Error(String(r.status));
|
|
const data = await r.json();
|
|
catalogEditable = !!data.editable;
|
|
const map = {};
|
|
(data.actions || []).forEach((a) => {
|
|
map[a.id] = {
|
|
effective: a.effective?.[roleId] || 'none',
|
|
default: a.defaults?.[roleId] || 'none',
|
|
overridden: a.overridden_roles?.includes(roleId),
|
|
};
|
|
});
|
|
levelsCache = map;
|
|
levelsRole = roleId;
|
|
return map;
|
|
}
|
|
|
|
function actionOn(level) {
|
|
return level === 'full' || level === 'approve' || level === 'api' || level === 'system';
|
|
}
|
|
|
|
function renderPermToggles(selectedRole, roleMeta, levels, editable) {
|
|
const code = ROLE_CODES[selectedRole] || selectedRole?.slice(0, 3).toUpperCase() || '—';
|
|
const rows = ACCESS_ACTIONS.map((a) => {
|
|
const lv = levels[a.actionId]?.effective || 'none';
|
|
const on = actionOn(lv);
|
|
const partial = on && lv !== 'full';
|
|
const ro = !editable ? ' acs-toggle--readonly' : '';
|
|
const dis = editable ? '' : ' disabled';
|
|
return `
|
|
<div class="acs-perm-row" title="${esc(a.hint)} · ${esc(lv)}">
|
|
<span class="acs-perm-label">${esc(a.label)}${partial ? ` <em class="acs-partial">(${esc(lv)})</em>` : ''}</span>
|
|
<label class="acs-toggle${ro}" aria-label="${esc(a.label)}" data-acs-action="${esc(a.actionId)}">
|
|
<input type="checkbox" ${on ? 'checked' : ''}${dis} data-acs-toggle="${esc(a.actionId)}"/>
|
|
<span class="acs-toggle-slider"></span>
|
|
</label>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
const editHint = editable
|
|
? 'Toggles ligados ao catálogo Spec 039 — alterações gravam na Matriz «Quem faz o quê».'
|
|
: 'Modo consulta — use a aba «Quem faz o quê» com edição activa para alterar.';
|
|
|
|
return `
|
|
<div class="acs-perms-card acs-card lb-card" style="padding:16px">
|
|
<header class="acs-perms-head">
|
|
<span class="acs-role-code" title="Código da função">${esc(code)}</span>
|
|
<div>
|
|
<h3>Capacidades da função</h3>
|
|
<p class="acs-perms-desc">Função <strong>${esc(roleMeta?.label || selectedRole)}</strong>. ${editHint}</p>
|
|
</div>
|
|
</header>
|
|
<div class="acs-perm-list">${rows}</div>
|
|
<p class="ticket-meta"><a href="#" class="acs-goto-exec" data-goto-exec>Abrir mapa completo «Quem faz o quê»</a></p>
|
|
</div>`;
|
|
}
|
|
|
|
async function patchToggle(actionId, roleId, checked) {
|
|
const level = checked ? 'full' : 'none';
|
|
const r = await fetchWithTimeout('/api/v1/rbac/actions/override', {
|
|
method: 'PATCH',
|
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
|
body: JSON.stringify({ action_id: actionId, role_id: roleId, level, reset: !checked }),
|
|
});
|
|
if (!r.ok) throw new Error(await r.text());
|
|
levelsCache = null;
|
|
return r.json();
|
|
}
|
|
|
|
async function paintCapabilitiesOnly(host, opts = {}) {
|
|
if (!host) return;
|
|
const selectedRole = opts.selectedRole || 'super_admin';
|
|
const roleMeta = opts.roleMeta || {};
|
|
const editable = !!opts.editable;
|
|
host.innerHTML = '<p class="loading">Carregando capacidades…</p>';
|
|
let levels = {};
|
|
try {
|
|
levels = await loadLevelsForRole(selectedRole);
|
|
} catch (e) {
|
|
host.innerHTML = `<p class="loading">Catálogo indisponível: ${esc(e.message)}</p>`;
|
|
return;
|
|
}
|
|
const canEdit = editable && catalogEditable;
|
|
host.innerHTML = renderPermToggles(selectedRole, roleMeta, levels, canEdit);
|
|
host.querySelector('[data-goto-exec]')?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
document.querySelector('[data-am-tab="quem-faz-o-que"]')?.click();
|
|
});
|
|
host.querySelectorAll('[data-acs-toggle]').forEach((inp) => {
|
|
inp.addEventListener('change', async () => {
|
|
if (!canEdit) return;
|
|
const actionId = inp.dataset.acsToggle;
|
|
try {
|
|
await patchToggle(actionId, selectedRole, inp.checked);
|
|
} catch (err) {
|
|
inp.checked = !inp.checked;
|
|
window.alert(`Erro ao gravar: ${err.message}`);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function paint(host, opts = {}) {
|
|
if (!host) return;
|
|
if (window.DeskAccessControlHub?.paint) {
|
|
return window.DeskAccessControlHub.paint(host, opts);
|
|
}
|
|
return paintCapabilitiesOnly(host, opts);
|
|
}
|
|
|
|
window.DeskAccessControlPanel = {
|
|
ROLE_CODES,
|
|
ACCESS_ACTIONS,
|
|
paint,
|
|
paintCapabilitiesOnly,
|
|
invalidateCache: () => { levelsCache = null; levelsRole = null; },
|
|
resetUserMgmt: () => window.DeskUserManagement?.reset?.(),
|
|
};
|
|
})();
|