mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-21 06:51:25 +02:00
fix(rbac): protect built-in administrator from losing access
This commit is contained in:
1 parent
e6121e83eb
commit
be2c8c04e5
12 files changed
+319
-3
No files matched your search
@@ -4793,6 +4793,8 @@
|
||||
"deleteRoleConfirm": "Delete this platform role?",
|
||||
"scopeHint": "Resource scope and operation permissions jointly determine effective access.",
|
||||
"builtinAccount": "Built-in account",
|
||||
"protectBuiltinAdminRole": "The built-in administrator must retain the admin role",
|
||||
"protectBuiltinAdminEnabled": "The built-in administrator cannot be disabled",
|
||||
"statusEnabled": "Enabled",
|
||||
"statusDisabled": "Disabled",
|
||||
"noRoleAssigned": "No role assigned",
|
||||
|
||||
@@ -4806,6 +4806,8 @@
|
||||
"deleteRoleConfirm": "Удалить эту роль платформы?",
|
||||
"scopeHint": "Область ресурсов и права на операции вместе определяют фактический доступ.",
|
||||
"builtinAccount": "Встроенная учётная запись",
|
||||
"protectBuiltinAdminRole": "Встроенный администратор должен сохранять роль admin",
|
||||
"protectBuiltinAdminEnabled": "Встроенного администратора нельзя отключить",
|
||||
"statusEnabled": "Включена",
|
||||
"statusDisabled": "Отключена",
|
||||
"noRoleAssigned": "Роль не назначена",
|
||||
|
||||
@@ -4781,6 +4781,8 @@
|
||||
"deleteRoleConfirm": "确认删除该平台角色?",
|
||||
"scopeHint": "资源范围与操作权限共同决定最终访问能力。",
|
||||
"builtinAccount": "内置账号",
|
||||
"protectBuiltinAdminRole": "内置管理员必须保留管理员角色",
|
||||
"protectBuiltinAdminEnabled": "内置管理员不能停用",
|
||||
"statusEnabled": "启用",
|
||||
"statusDisabled": "停用",
|
||||
"noRoleAssigned": "未分配角色",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function setup() {
|
||||
const elements = new Map([
|
||||
'rbac-user-id', 'rbac-username', 'rbac-display-name', 'rbac-password',
|
||||
'rbac-user-enabled', 'rbac-user-roles-create-section', 'rbac-user-roles-edit-section',
|
||||
'rbac-user-roles-readonly', 'rbac-user-role-checkboxes', 'rbac-roles-list',
|
||||
].map(id => [id, { value: '', checked: false, disabled: false, dataset: {}, innerHTML: '' }]));
|
||||
const context = vm.createContext({
|
||||
window: {}, console,
|
||||
document: { getElementById: id => elements.get(id) || null, querySelector: () => null, addEventListener() {} },
|
||||
openAppModal() {},
|
||||
escapeHtml: text => String(text),
|
||||
});
|
||||
vm.runInContext(fs.readFileSync(path.join(__dirname, 'rbac.js'), 'utf8') + '\nglobalThis.state = rbacState;', context);
|
||||
context.state.users = [
|
||||
{ id: 'admin', username: 'admin', enabled: true, is_builtin: true, roles: ['admin'] },
|
||||
{ id: 'member', username: 'member', enabled: true, roles: ['admin', 'operator'] },
|
||||
];
|
||||
context.state.roles = [
|
||||
{ id: 'admin', name: 'Administrator', scope: 'all', is_system: true },
|
||||
{ id: 'operator', name: 'Operator', scope: 'assigned', is_system: true },
|
||||
];
|
||||
return { context, elements };
|
||||
}
|
||||
|
||||
function adminCheckbox(html) {
|
||||
return html.match(/<input[^>]+onchange="toggleSelectedUserRole\('admin'[^>]+>/)[0];
|
||||
}
|
||||
|
||||
test('built-in enabled control is locked, then resets for member editing and creation', () => {
|
||||
const { context, elements } = setup();
|
||||
const input = elements.get('rbac-user-enabled');
|
||||
context.openRbacUserModal('admin');
|
||||
assert.equal(input.checked, true);
|
||||
assert.equal(input.disabled, true);
|
||||
assert.equal(input.dataset.stateDisabled, 'true');
|
||||
assert.ok(input.title);
|
||||
context.openRbacUserModal('member');
|
||||
assert.equal(input.disabled, false);
|
||||
assert.equal(input.dataset.stateDisabled, 'false');
|
||||
assert.equal(input.title, '');
|
||||
context.openRbacUserModal('admin');
|
||||
context.openRbacUserModal();
|
||||
assert.equal(input.disabled, false);
|
||||
assert.equal(input.checked, true);
|
||||
assert.equal(elements.get('rbac-user-id').value, '');
|
||||
});
|
||||
|
||||
test('built-in admin role cannot be unchecked; another admin account can be demoted', () => {
|
||||
const { context, elements } = setup();
|
||||
context.state.selectedUserId = 'admin';
|
||||
context.renderRbacRoles();
|
||||
let checkbox = adminCheckbox(elements.get('rbac-roles-list').innerHTML);
|
||||
assert.match(checkbox, /\bchecked\b/);
|
||||
assert.match(checkbox, /\bdisabled\b/);
|
||||
context.toggleSelectedUserRole('admin', false);
|
||||
assert.equal(context.state.pendingUserRoles.has('admin'), true);
|
||||
context.state.selectedUserId = 'member';
|
||||
context.renderRbacRoles();
|
||||
checkbox = adminCheckbox(elements.get('rbac-roles-list').innerHTML);
|
||||
assert.doesNotMatch(checkbox, /\bdisabled\b/);
|
||||
context.toggleSelectedUserRole('admin', false);
|
||||
assert.equal(context.state.pendingUserRoles.has('admin'), false);
|
||||
assert.equal(context.state.pendingUserRoles.has('operator'), true);
|
||||
});
|
||||
|
||||
test('protection still permits explicitly restoring a historically damaged built-in account', () => {
|
||||
const { context, elements } = setup();
|
||||
const admin = context.state.users[0];
|
||||
admin.enabled = false;
|
||||
admin.roles = [];
|
||||
context.openRbacUserModal('admin');
|
||||
assert.equal(elements.get('rbac-user-enabled').disabled, false);
|
||||
context.state.selectedUserId = 'admin';
|
||||
context.renderRbacRoles();
|
||||
assert.doesNotMatch(adminCheckbox(elements.get('rbac-roles-list').innerHTML), /\bdisabled\b/);
|
||||
context.toggleSelectedUserRole('admin', true);
|
||||
assert.match(adminCheckbox(elements.get('rbac-roles-list').innerHTML), /\bdisabled\b/);
|
||||
});
|
||||
@@ -714,6 +714,7 @@ function renderRbacRoles() {
|
||||
const userRoles = user ? rbacState.pendingUserRoles : new Set();
|
||||
box.innerHTML = rbacState.roles.map(role => {
|
||||
const checked = userRoles.has(role.id);
|
||||
const protectedRole = rbacUserIsBuiltin(user) && role.id === 'admin' && checked;
|
||||
const permissionCount = (role.permissions || []).length;
|
||||
const assignedUsers = rbacState.users.filter(u => rbacUserRoles(u).includes(role.id)).length;
|
||||
const scope = rbacScopeInfo(role.scope);
|
||||
@@ -721,7 +722,7 @@ function renderRbacRoles() {
|
||||
<article class="rbac-role-card${checked ? ' is-selected' : ''}">
|
||||
<div class="rbac-role-card-head">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" class="modern-checkbox" ${checked ? 'checked' : ''} ${!user ? 'disabled' : ''} onchange="toggleSelectedUserRole('${rbacEscape(role.id)}', this.checked)">
|
||||
<input type="checkbox" class="modern-checkbox" ${checked ? 'checked' : ''} ${!user || protectedRole ? 'disabled' : ''} title="${protectedRole ? rbacEscape(rbacT('rbac.protectBuiltinAdminRole', '内置管理员必须保留管理员角色')) : ''}" onchange="toggleSelectedUserRole('${rbacEscape(role.id)}', this.checked)">
|
||||
<span class="checkbox-custom"></span>
|
||||
<span class="checkbox-text"><strong>${rbacEscape(rbacRoleName(role))}</strong></span>
|
||||
</label>
|
||||
@@ -751,7 +752,8 @@ function ensurePendingUserRoles(user) {
|
||||
}
|
||||
|
||||
function toggleSelectedUserRole(roleId, enabled) {
|
||||
if (!selectedRbacUser()) return;
|
||||
const user = selectedRbacUser();
|
||||
if (!user || (rbacUserIsBuiltin(user) && roleId === 'admin' && !enabled)) return;
|
||||
if (enabled) rbacState.pendingUserRoles.add(roleId);
|
||||
else rbacState.pendingUserRoles.delete(roleId);
|
||||
renderRbacRoles();
|
||||
@@ -1232,7 +1234,11 @@ function openRbacUserModal(userId = '') {
|
||||
document.getElementById('rbac-username').disabled = !!user;
|
||||
document.getElementById('rbac-display-name').value = user ? (user.displayName || user.display_name || '') : '';
|
||||
document.getElementById('rbac-password').value = '';
|
||||
document.getElementById('rbac-user-enabled').checked = user ? !!user.enabled : true;
|
||||
const enabledInput = document.getElementById('rbac-user-enabled');
|
||||
enabledInput.checked = user ? !!user.enabled : true;
|
||||
enabledInput.disabled = rbacUserIsBuiltin(user) && enabledInput.checked;
|
||||
enabledInput.dataset.stateDisabled = String(enabledInput.disabled);
|
||||
enabledInput.title = enabledInput.disabled ? rbacT('rbac.protectBuiltinAdminEnabled', '内置管理员不能停用') : '';
|
||||
const createRoles = document.getElementById('rbac-user-roles-create-section');
|
||||
const editRoles = document.getElementById('rbac-user-roles-edit-section');
|
||||
if (createRoles) createRoles.hidden = !!user;
|
||||
|
||||
Reference in new issue
Block a user