feat: manage existing credential actions with audited replay
This commit is contained in:
@@ -1,8 +1,34 @@
|
||||
(() => {
|
||||
const $ = id => document.getElementById(id);
|
||||
const ui = { after: '', next: '', selected: '', detail: null, loading: false, busy: false, pending: null, rotating: null, result: null, saved: true, mode: 'CREATE', helpCredential: '' };
|
||||
const ui = { after: '', next: '', selected: '', detail: null, loading: false, busy: false, pending: null, rotating: null, result: null, saved: true, mode: 'CREATE', helpCredential: '', editNamespace: '' };
|
||||
const escape = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
const time = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '长期有效';
|
||||
|
||||
// 与 platform/auth/token.go 的精确动作目录一致;不按能力组自动增加权限。
|
||||
const permissionGroups = [
|
||||
['任务', false, [['tasks:submit', '提交任务'], ['tasks:read', '查询任务'], ['tasks:cancel', '取消任务']]],
|
||||
['队列', false, [['queues:read', '查询队列'], ['queues:manage', '管理队列']]],
|
||||
['Worker 执行', false, [['workers:connect', '建立连接'], ['workers:consume', '领取与执行任务']]],
|
||||
['结果订阅', false, [['events:read', '查询订阅与结果'], ['events:manage', '管理订阅'], ['events:consume', '消费与确认结果']]],
|
||||
['定时调度', false, [['schedules:read', '查询计划'], ['schedules:manage', '管理计划']]],
|
||||
['流程编排', false, [['workflows:definitions:read', '查询定义'], ['workflows:definitions:publish', '发布定义'], ['workflows:start', '启动流程'], ['workflows:read', '查询流程'], ['workflows:control', '控制流程'], ['workflows:signal', '发送信号']]],
|
||||
['高级管理 · 人工恢复与审计', true, [['tasks:review', '核查任务结果'], ['tasks:retry', '人工重试任务'], ['tasks:audit:read', '读取任务操作审计'], ['workflows:recover', '人工恢复流程'], ['workflows:audit:read', '读取流程审计']]],
|
||||
['高级管理 · 应用额度', true, [['application:resources:read', '查询应用额度'], ['application:resources:manage', '调整应用额度']]]
|
||||
];
|
||||
const permissionActions = new Set(permissionGroups.flatMap(([, , actions]) => actions.map(([action]) => action)));
|
||||
function unsupportedPermissions(credential) {
|
||||
const actions = credential.permissions.actions;
|
||||
return actions.some(action => !permissionActions.has(action)) || new Set(actions).size !== actions.length;
|
||||
}
|
||||
function renderPermissionEditor(credential) {
|
||||
const selected = new Set(credential.permissions.actions);
|
||||
$('access-permission-scope').textContent = `命名空间:${ui.editNamespace} · 凭据:${credential.id} · 版本 ${credential.revision}`;
|
||||
$('access-permission-current').textContent = credential.permissions.actions.join(' · ');
|
||||
$('access-permission-queues').textContent = credential.permissions.queues.join(' · ');
|
||||
$('access-permission-lifetime').textContent = `当前 Token 有效期:${time(credential.expires_at)}。${credential.previous_valid_until ? `旧 Token 过渡截止:${time(credential.previous_valid_until)}。` : '无旧 Token 过渡。'}`;
|
||||
$('access-permission-warning').hidden = !unsupportedPermissions(credential);
|
||||
$('access-permission-options').innerHTML = permissionGroups.map(([name, advanced, actions]) => `<fieldset class="access-action-group${advanced ? ' access-action-advanced' : ''}"><legend>${escape(name)}</legend>${actions.map(([action, label]) => `<label class="access-action-option"><input type="checkbox" value="${escape(action)}" ${selected.has(action) ? 'checked' : ''}><span>${escape(label)}<code>${escape(action)}</code></span></label>`).join('')}</fieldset>`).join('');
|
||||
}
|
||||
let tokenRequest = 0, tokenTarget = null, selectionSequence = 0;
|
||||
const resourceMessages = {
|
||||
'resource is retired; history remains available':'资源已退役,历史仍可查询。',
|
||||
@@ -102,7 +128,7 @@
|
||||
<div class="access-token-summary"><div><strong>接入配置 · 当前 Token</strong><span>${c.revoked_at ? '凭据已撤销' : c.token_stored ? '•••••••••••••••• · 已加密保管' : '历史凭据 · 导入当前 Token 后可随时查看'}</span></div></div>
|
||||
<div class="access-form-actions"><button type="button" class="primary-button compact" data-access-token="${escape(c.id)}" ${c.revoked_at ? 'disabled' : ''}>${c.token_stored ? '查看接入信息' : '导入当前 Token'}</button><button type="button" class="secondary-button compact" data-access-copy-config="${escape(c.id)}" ${c.revoked_at || !c.token_stored ? 'disabled' : ''}>复制接入配置 ↗</button></div>
|
||||
${rotationHelp}
|
||||
<details class="credential-controls"><summary>凭据管理与权限</summary><div class="access-form-actions"><button class="text-button" data-access-expiry="${escape(c.id)}" ${c.revoked_at ? 'disabled' : ''}>调整有效期</button>${c.previous_valid_until && !c.revoked_at ? `<button class="text-button" data-access-finalize="${escape(c.id)}">结束过渡,停用旧 Token</button>` : ''}</div>
|
||||
<details class="credential-controls"><summary>凭据管理与权限</summary><div class="access-form-actions"><button class="text-button" data-access-permissions="${escape(c.id)}" ${c.revoked_at ? 'disabled' : ''}>调整权限</button><button class="text-button" data-access-expiry="${escape(c.id)}" ${c.revoked_at ? 'disabled' : ''}>调整有效期</button>${c.previous_valid_until && !c.revoked_at ? `<button class="text-button" data-access-finalize="${escape(c.id)}">结束过渡,停用旧 Token</button>` : ''}</div>
|
||||
<div class="access-permissions">${groups.map(a => `<span>${escape(a)}</span>`).join('')}</div><details class="access-permission-details"><summary>查看具体权限</summary><p>${c.permissions.actions.map(escape).join(' · ')}<br>授权队列:${escape(c.permissions.queues.join(', '))}</p></details></details>
|
||||
</section>`;
|
||||
}).join('') || '<p class="panel-description">该环境暂无凭据。</p>';
|
||||
@@ -168,23 +194,28 @@
|
||||
}
|
||||
function open(credential = null, mode = 'ROTATE') {
|
||||
if (ui.busy) return;
|
||||
ui.rotating = credential; ui.mode = credential ? mode : 'CREATE'; ui.pending = null; ui.result = null; ui.saved = true;
|
||||
ui.rotating = credential; ui.mode = credential ? mode : 'CREATE'; ui.editNamespace = ui.selected; ui.pending = null; ui.result = null; ui.saved = true;
|
||||
$('access-form').reset(); $('access-form').querySelectorAll('input,select,button').forEach(el => { el.disabled = false; }); $('access-form').hidden = false; $('access-result').hidden = true; $('access-backup').hidden = true; $('access-form-error').textContent = '';
|
||||
$('access-result-config').value = ''; $('access-clipboard-status').textContent = '';
|
||||
$('access-create-fields').hidden = !!credential;
|
||||
$('access-create-fields').querySelectorAll('input,select').forEach(el => { el.disabled = !!credential; });
|
||||
const titles = { CREATE: '新建应用', ROTATE: '平滑换证', SET_EXPIRY: '调整有效期', FINALIZE: '结束过渡' };
|
||||
$('access-permission-editor').hidden = ui.mode !== 'SET_PERMISSIONS';
|
||||
if (ui.mode === 'SET_PERMISSIONS') renderPermissionEditor(credential);
|
||||
$('access-permission-options').querySelectorAll('input').forEach(el => { el.disabled = ui.mode !== 'SET_PERMISSIONS'; });
|
||||
const titles = { CREATE: '新建应用', ROTATE: '平滑换证', SET_EXPIRY: '调整有效期', FINALIZE: '结束过渡', SET_PERMISSIONS: '调整权限' };
|
||||
$('access-form-title').textContent = titles[ui.mode];
|
||||
$('access-valid-field').hidden = ui.mode === 'FINALIZE';
|
||||
$('access-valid-field').hidden = ['FINALIZE', 'SET_PERMISSIONS'].includes(ui.mode);
|
||||
$('access-grace-field').hidden = ui.mode !== 'ROTATE';
|
||||
$('access-form-note').textContent = {
|
||||
CREATE: '为应用创建独立的环境、队列与调用凭据,默认长期有效。',
|
||||
ROTATE: '生成新 Token 后,旧 Token 在过渡期内继续可用。逐台更新服务配置并正常收尾在途任务;确认全部切换后,再结束过渡。',
|
||||
SET_EXPIRY: '只调整当前 Token 的有效期,Token 和业务配置保持不变。已有旧 Token 的过渡截止不会延长。',
|
||||
FINALIZE: '此操作立即停用旧 Token,新 Token 继续有效。请先确认全部服务、Worker 和结果消费者已切换,在途执行已收尾。'
|
||||
FINALIZE: '此操作立即停用旧 Token,新 Token 继续有效。请先确认全部服务、Worker 和结果消费者已切换,在途执行已收尾。',
|
||||
SET_PERMISSIONS: '勾选的动作将完整替换当前权限,取消勾选会收回对应权限。当前和过渡期 Token 均使用调整后的权限;Token、有效期、过渡截止及队列范围保持不变。'
|
||||
}[ui.mode];
|
||||
$('access-token-once-note').hidden = !['CREATE', 'ROTATE'].includes(ui.mode);
|
||||
$('access-submit').textContent = ui.mode === 'CREATE' ? '创建并生成凭据 →' : `确认${titles[ui.mode]} →`;
|
||||
$('access-submit').disabled = ui.mode === 'SET_PERMISSIONS' && unsupportedPermissions(credential);
|
||||
$('access-dialog').showModal(); (credential ? $('access-reason') : $('access-name')).focus();
|
||||
}
|
||||
function close() {
|
||||
@@ -204,9 +235,16 @@
|
||||
const form = $('access-form'); const disabled = [...form.elements].map(el => [el, el.disabled]);
|
||||
try {
|
||||
if (!ui.pending) {
|
||||
const input = { kind: ui.mode, valid_days: ui.mode === 'FINALIZE' ? 0 : Number($('access-valid-days').value), reason: $('access-reason').value.trim(), expected_revision: ui.rotating ? String(ui.rotating.revision) : '0' };
|
||||
const input = { kind: ui.mode, valid_days: ['FINALIZE', 'SET_PERMISSIONS'].includes(ui.mode) ? 0 : Number($('access-valid-days').value), reason: $('access-reason').value.trim(), expected_revision: ui.rotating ? String(ui.rotating.revision) : '0' };
|
||||
if (ui.mode === 'ROTATE') input.grace_days = Number($('access-grace-days').value);
|
||||
if (ui.rotating) Object.assign(input, { namespace_id: ui.selected, credential_id: ui.rotating.id });
|
||||
if (ui.mode === 'SET_PERMISSIONS') {
|
||||
if (unsupportedPermissions(ui.rotating)) throw new Error('当前凭据包含页面不支持或重复的动作,请更新控制台并核对权限后再操作。');
|
||||
input.actions = [...$('access-permission-options').querySelectorAll('input:checked')].map(el => el.value).sort();
|
||||
if (!input.actions.length) throw new Error('请至少保留一项权限。');
|
||||
if (input.actions.some(action => !permissionActions.has(action)) || new Set(input.actions).size !== input.actions.length) throw new Error('权限选择无效,请重新打开调整权限。');
|
||||
input.grace_days = 0;
|
||||
}
|
||||
if (ui.rotating) Object.assign(input, { namespace_id: ui.editNamespace, credential_id: ui.rotating.id });
|
||||
else Object.assign(input, { application: $('access-name').value.trim(), namespace: $('access-namespace').value.trim(), environment: $('access-environment').value, queue: $('access-queue-name').value.trim(), capabilities: [...document.querySelectorAll('.access-capabilities input:checked')].map(el => el.value) });
|
||||
form.querySelectorAll('input,select,button').forEach(el => { el.disabled = true; });
|
||||
ui.pending = await api('/prepare', input);
|
||||
@@ -220,13 +258,14 @@
|
||||
await select(op.namespace_id);
|
||||
const credential = ui.detail?.entry.namespace_id === op.namespace_id && ui.detail.credentials.find(c => c.id === op.credential_id);
|
||||
const expected = op.input.kind === 'CREATE' ? '1' : String(BigInt(op.input.expected_revision) + 1n);
|
||||
if (!credential || String(credential.revision) !== expected || credential.revoked_at) throw new Error('操作已提交,但凭据已被更新或暂时无法核对,请刷新应用详情;不要使用旧操作中的 Token。');
|
||||
if (!credential || String(credential.revision) !== expected || credential.revoked_at) throw new Error('操作已提交,但凭据已被更新或暂时无法核对,请刷新应用详情后核对当前状态。');
|
||||
if (op.input.kind === 'SET_PERMISSIONS' && JSON.stringify([...credential.permissions.actions].sort()) !== JSON.stringify([...op.input.actions].sort())) throw new Error('操作已提交,但当前权限暂时无法核对,请保留原操作重试确认。');
|
||||
if (!credential.permissions.queues.includes('*') && !credential.permissions.queues.includes(queue)) queue = ui.detail.queues.find(q => credential.permissions.queues.includes(q.id))?.id || '';
|
||||
const preparedConfig = op.token ? config(op.token, queue, op.namespace_id) : '';
|
||||
ui.result = op; ui.pending = null;
|
||||
$('access-form').hidden = true; $('access-result').hidden = false;
|
||||
$('access-token-once-note').hidden = true;
|
||||
$('access-result-title').textContent = { CREATE: '应用已创建,可以接入', ROTATE: '新 Token 已生效', SET_EXPIRY: '有效期已更新,原 Token 继续使用', FINALIZE: '过渡已结束,旧 Token 已停用' }[op.input.kind];
|
||||
$('access-result-title').textContent = { CREATE: '应用已创建,可以接入', ROTATE: '新 Token 已生效', SET_EXPIRY: '有效期已更新,原 Token 继续使用', FINALIZE: '过渡已结束,旧 Token 已停用', SET_PERMISSIONS: '权限已更新,原 Token 继续使用' }[op.input.kind];
|
||||
$('access-result-note').hidden = !op.token; $('access-result-config-field').hidden = !op.token; $('access-result-actions').hidden = !op.token;
|
||||
$('access-result-config').value = preparedConfig;
|
||||
$('access-result-expiry').textContent = `当前 Token:${time(credential.expires_at)}。${credential.previous_valid_until ? `旧 Token 截止:${time(credential.previous_valid_until)},请在此之前完成切换。` : '无旧 Token 过渡。'}`;
|
||||
@@ -241,12 +280,12 @@
|
||||
$('access-url').value = location.origin;
|
||||
$('access-list').addEventListener('click', e => { const el = e.target.closest('[data-access-id]'); if (el) { closeToken(); select(el.dataset.accessId); } });
|
||||
$('access-credentials').addEventListener('click', e => {
|
||||
const el = e.target.closest('[data-access-help],[data-access-rotate],[data-access-expiry],[data-access-finalize],[data-access-token],[data-access-copy-config]'); if (!el || el.disabled) return;
|
||||
const id = el.dataset.accessHelp || el.dataset.accessRotate || el.dataset.accessExpiry || el.dataset.accessFinalize || el.dataset.accessToken || el.dataset.accessCopyConfig;
|
||||
const el = e.target.closest('[data-access-help],[data-access-rotate],[data-access-expiry],[data-access-finalize],[data-access-permissions],[data-access-token],[data-access-copy-config]'); if (!el || el.disabled) return;
|
||||
const id = el.dataset.accessHelp || el.dataset.accessRotate || el.dataset.accessExpiry || el.dataset.accessFinalize || el.dataset.accessPermissions || el.dataset.accessToken || el.dataset.accessCopyConfig;
|
||||
const credential = ui.detail?.credentials.find(c => c.id === id);
|
||||
if (credential && (el.dataset.accessToken || el.dataset.accessCopyConfig)) { showToken(credential, !!el.dataset.accessCopyConfig); return; }
|
||||
if (credential && el.dataset.accessHelp) { showCredentialHelp(credential); return; }
|
||||
if (credential && !credential.revoked_at) open(credential, el.dataset.accessExpiry ? 'SET_EXPIRY' : el.dataset.accessFinalize ? 'FINALIZE' : 'ROTATE');
|
||||
if (credential && !credential.revoked_at) open(credential, el.dataset.accessPermissions ? 'SET_PERMISSIONS' : el.dataset.accessExpiry ? 'SET_EXPIRY' : el.dataset.accessFinalize ? 'FINALIZE' : 'ROTATE');
|
||||
});
|
||||
$('access-help-close').addEventListener('click', () => $('access-help-dialog').close());
|
||||
$('access-help-done').addEventListener('click', () => $('access-help-dialog').close());
|
||||
|
||||
Reference in New Issue
Block a user