${escapeHtml(item.title)}
-
`;
}).join('');
updateProjectsDetailVisibility();
+ if (typeof applyRBACToUI === 'function') applyRBACToUI(el);
}
function clampProjectDescription(text) {
@@ -1029,6 +1051,7 @@ function toggleProjectFactGraphConnectMode() {
async function handleGraphConnectNodePick(factKey) {
if (!factKey || String(factKey).startsWith('vuln:')) return;
+ if (!requireProjectWrite()) return;
if (!_graphConnectSource) {
_graphConnectSource = factKey;
if (typeof showNotification === 'function') {
@@ -1052,10 +1075,7 @@ async function handleGraphConnectNodePick(factKey) {
}),
});
_graphConnectSource = null;
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- return alert(err.error || tp('projects.graphConnectFailed'));
- }
+ if (!(await notifyProjectApiFailure(res, 'projects.graphConnectFailed', '创建边失败'))) return;
if (typeof showNotification === 'function') showNotification(tp('projects.graphConnectSuccess'), 'success');
loadProjectFactGraph();
loadProjectFacts();
@@ -1270,16 +1290,14 @@ function focusProjectFactGraphEdge(edgeId) {
async function deleteProjectFactEdge(edgeId) {
if (!edgeId || !currentProjectId) return;
+ if (!requireProjectWrite()) return;
const edge = (_currentGraphData?.edges || []).find((e) => e.id === edgeId);
if (isSyntheticGraphEdge(edge)) return;
if (!confirm(tp('projects.confirmDeleteGraphEdge'))) return;
const res = await apiFetch(`/api/projects/${currentProjectId}/fact-edges/${encodeURIComponent(edgeId)}`, {
method: 'DELETE',
});
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- return alert(err.error || tp('projects.graphEdgeDeleteFailed'));
- }
+ if (!(await notifyProjectApiFailure(res, 'projects.graphEdgeDeleteFailed', '删除边失败'))) return;
if (typeof showNotification === 'function') showNotification(tp('projects.graphEdgeDeleteSuccess'), 'success');
const keepKey = _selectedGraphFactKey;
await loadProjectFactGraph();
@@ -1439,15 +1457,13 @@ function openProjectConversation(conversationId) {
async function promoteConversationAttackChain(conversationId) {
if (!currentProjectId || !conversationId) return;
+ if (!requireProjectWrite()) return;
if (!confirm(tp('projects.confirmPromoteAttackChain'))) return;
const res = await apiFetch(
`/api/projects/${currentProjectId}/promote-attack-chain/${encodeURIComponent(conversationId)}`,
{ method: 'POST' },
);
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- return alert(err.error || tp('projects.promoteAttackChainFailed'));
- }
+ if (!(await notifyProjectApiFailure(res, 'projects.promoteAttackChainFailed', '沉淀失败'))) return;
const data = await res.json();
if (typeof showNotification === 'function') {
showNotification(
@@ -1581,6 +1597,7 @@ async function linkFactToExistingVulnerability() {
async function createVulnerabilityFromCurrentFact() {
const f = _factDetailFact;
if (!f || !currentProjectId) return;
+ if (typeof requirePermission === 'function' && !requirePermission('vulnerability:write')) return;
let convId =
(f.source_conversation_id || '').trim() ||
(typeof window.currentConversationId === 'string' ? window.currentConversationId.trim() : '');
@@ -1611,12 +1628,9 @@ async function createVulnerabilityFromCurrentFact() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- return alert(err.error || tp('projects.createVulnerabilityFailed'));
- }
+ if (!(await notifyProjectApiFailure(res, 'projects.createVulnerabilityFailed', '创建漏洞失败'))) return;
const vuln = await res.json();
- await apiFetch(`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}`, {
+ const upd = await apiFetch(`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -1628,6 +1642,7 @@ async function createVulnerabilityFromCurrentFact() {
related_vulnerability_id: vuln.id,
}),
});
+ if (!(await notifyProjectApiFailure(upd, 'projects.linkFailed', '关联失败'))) return;
const createdVulnLabel = vuln.title || vuln.id;
const successMsg = tp('projects.createVulnerabilityAndLinkSuccess', {
value: createdVulnLabel,
@@ -1648,6 +1663,7 @@ function inferSeverityFromFact(f) {
}
async function deprecateProjectFactByKey(factKey) {
+ if (!requireProjectWrite()) return;
if (!confirm(
tp('projects.confirmDeprecateFact', {
factKey,
@@ -1659,11 +1675,12 @@ async function deprecateProjectFactByKey(factKey) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fact_key: factKey }),
});
- if (!res.ok) return alert(tp('projects.operationFailed'));
+ if (!(await notifyProjectApiFailure(res, 'projects.operationFailed', '操作失败'))) return;
loadProjectFacts();
}
async function restoreProjectFactByKey(factKey) {
+ if (!requireProjectWrite()) return;
if (!confirm(
tp('projects.confirmRestoreFact', {
factKey,
@@ -1675,10 +1692,7 @@ async function restoreProjectFactByKey(factKey) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fact_key: factKey, confidence: 'tentative' }),
});
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- return alert(err.error || tp('projects.operationFailed'));
- }
+ if (!(await notifyProjectApiFailure(res, 'projects.operationFailed', '操作失败'))) return;
loadProjectFacts();
}
@@ -1793,6 +1807,7 @@ function closeProjectsOverlay(id) {
}
function showNewProjectModal() {
+ if (!requireProjectWrite()) return;
document.getElementById('project-modal-title').textContent = tp('projects.modalNewTitle');
const sub = document.getElementById('project-modal-subtitle');
if (sub) sub.textContent = tp('projects.modalNewSubtitle');
@@ -1806,6 +1821,7 @@ function showNewProjectModal() {
async function showEditProjectModal(projectId) {
if (!projectId) return;
+ if (!requireProjectWrite()) return;
window._projectModalFromChat = false;
window._projectModalEditId = projectId;
document.getElementById('project-modal-title').textContent = tp('projects.modalEditTitle');
@@ -1848,6 +1864,7 @@ function showNewProjectModalFromChat() {
}
async function saveProjectModal() {
+ if (!requireProjectWrite()) return;
const name = document.getElementById('project-modal-name').value.trim().slice(0, PROJECT_NAME_MAX_LENGTH);
if (!name) return alert(tp('projects.enterProjectName'));
const body = {
@@ -1855,31 +1872,39 @@ async function saveProjectModal() {
description: clampProjectDescription(document.getElementById('project-modal-description').value),
};
const editId = window._projectModalEditId;
- const res = editId
- ? await apiFetch(`/api/projects/${editId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
- : await apiFetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- alert(err.error || tp('projects.saveFailed'));
- return;
- }
- const fromChat = !!window._projectModalFromChat;
- const fromWebshellConnId = window._projectModalFromWebshellConnId || '';
- window._projectModalFromChat = false;
- window._projectModalFromWebshellConnId = '';
- closeProjectModal();
- const saved = await res.json();
- await loadProjectsList();
- if (saved.id) {
- if (fromWebshellConnId && !editId) {
- if (typeof applyWebshellAiProjectSelection === 'function') {
- await applyWebshellAiProjectSelection(saved.id);
+ const submitBtn = document.getElementById('project-modal-submit-btn');
+ if (submitBtn) submitBtn.disabled = true;
+ try {
+ const res = editId
+ ? await apiFetch(`/api/projects/${editId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
+ : await apiFetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ if (!(await notifyProjectApiFailure(res, 'projects.saveFailed', '保存失败'))) return;
+ const fromChat = !!window._projectModalFromChat;
+ const fromWebshellConnId = window._projectModalFromWebshellConnId || '';
+ window._projectModalFromChat = false;
+ window._projectModalFromWebshellConnId = '';
+ closeProjectModal();
+ const saved = await res.json();
+ await loadProjectsList();
+ if (saved.id) {
+ if (fromWebshellConnId && !editId) {
+ if (typeof applyWebshellAiProjectSelection === 'function') {
+ await applyWebshellAiProjectSelection(saved.id);
+ }
+ } else if (fromChat && !editId) {
+ await applyChatProjectSelection(saved.id);
+ } else {
+ await selectProject(saved.id);
}
- } else if (fromChat && !editId) {
- await applyChatProjectSelection(saved.id);
- } else {
- await selectProject(saved.id);
}
+ } catch (error) {
+ if (typeof notifyApiError === 'function') {
+ notifyApiError(error?.message || tpFmt('projects.saveFailed', '保存失败'));
+ } else {
+ alert(error?.message || tpFmt('projects.saveFailed', '保存失败'));
+ }
+ } finally {
+ if (submitBtn) submitBtn.disabled = false;
}
}
@@ -1914,7 +1939,7 @@ function insertProjectScopeExample() {
}
async function saveProjectSettings() {
- if (!currentProjectId) return;
+ if (!currentProjectId || !requireProjectWrite()) return;
const scopeRaw = document.getElementById('project-edit-scope').value.trim();
if (scopeRaw) {
try {
@@ -1936,10 +1961,14 @@ async function saveProjectSettings() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
- if (!res.ok) return alert(tp('projects.saveFailed'));
+ if (!(await notifyProjectApiFailure(res, 'projects.saveFailed', '保存失败'))) return;
await loadProjectsList();
await selectProject(currentProjectId);
- alert(tp('projects.saved'));
+ if (typeof notifyApiError === 'function') {
+ notifyApiError(tp('projects.saved'), 'success');
+ } else {
+ alert(tp('projects.saved'));
+ }
}
function findProjectById(projectId) {
@@ -2002,6 +2031,7 @@ function showProjectListActionMenu(event, projectId) {
}
if (deleteText) deleteText.textContent = tp('projects.deleteProject');
positionProjectListActionMenu(event);
+ if (typeof applyRBACToUI === 'function') applyRBACToUI(menu);
}
function initProjectListActionMenu() {
@@ -2020,6 +2050,7 @@ function initProjectListActionMenu() {
}
async function toggleProjectArchiveById(projectId) {
+ if (!requireProjectWrite()) return;
const p = findProjectById(projectId);
if (!p) return;
const cur = p.status || 'active';
@@ -2030,7 +2061,7 @@ async function toggleProjectArchiveById(projectId) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: next }),
});
- if (!res.ok) return alert(tp('projects.operationFailed'));
+ if (!(await notifyProjectApiFailure(res, 'projects.operationFailed', '操作失败'))) return;
await loadProjectsList();
if (currentProjectId === projectId && projectsCache.some((item) => item.id === projectId)) {
await selectProject(projectId);
@@ -2042,10 +2073,11 @@ async function toggleProjectArchiveById(projectId) {
}
async function deleteProjectById(projectId) {
+ if (!requireProjectDelete()) return;
if (!projectId || !confirm(tp('projects.confirmDeleteProject'))) return;
const deletedIndex = projectsCache.findIndex((p) => p.id === projectId);
const res = await apiFetch(`/api/projects/${projectId}`, { method: 'DELETE' });
- if (!res.ok) return alert(tp('projects.deleteFailed'));
+ if (!(await notifyProjectApiFailure(res, 'projects.deleteFailed', '删除失败'))) return;
if (getActiveProjectId() === projectId) setActiveProjectId('');
if (currentProjectId === projectId) currentProjectId = null;
await loadProjectsList();
@@ -2147,12 +2179,14 @@ function fillFactModalForm(f) {
function showAddFactModal() {
if (!currentProjectId) return alert(tp('projects.selectProjectFirst'));
+ if (!requireProjectWrite()) return;
resetFactModalForm();
openProjectsOverlay('fact-modal');
}
async function showEditFactModal(factKey) {
if (!currentProjectId) return alert(tp('projects.selectProjectFirst'));
+ if (!requireProjectWrite()) return;
resetFactModalForm();
openProjectsOverlay('fact-modal', { focus: false });
const res = await apiFetch(
@@ -2175,6 +2209,7 @@ function closeFactModal() {
}
async function saveFactModal() {
+ if (!requireProjectWrite()) return;
const fact_key = document.getElementById('fact-modal-key').value.trim();
const summary = document.getElementById('fact-modal-summary').value.trim();
const category = document.getElementById('fact-modal-category').value.trim() || 'note';
@@ -2208,18 +2243,17 @@ async function saveFactModal() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- return alert(err.error || tp('projects.saveFailed'));
- }
+ if (!(await notifyProjectApiFailure(res, 'projects.saveFailed', '保存失败'))) return;
closeFactModal();
loadProjectFacts();
if (currentProjectTab === 'graph') loadProjectFactGraph();
}
async function deleteProjectFact(id) {
+ if (!requireProjectWrite()) return;
if (!confirm(tp('projects.confirmDeleteFact'))) return;
- await apiFetch(`/api/projects/${currentProjectId}/facts/${id}`, { method: 'DELETE' });
+ const res = await apiFetch(`/api/projects/${currentProjectId}/facts/${id}`, { method: 'DELETE' });
+ if (!(await notifyProjectApiFailure(res, 'projects.operationFailed', '操作失败'))) return;
loadProjectFacts();
if (currentProjectTab === 'graph') loadProjectFactGraph();
}
@@ -2511,6 +2545,8 @@ async function renderChatProjectPanel() {
});
clearProjectPickerPanelSearch('chat', 'chat-project-search');
await loadChatProjectPanelList();
+ const panel = document.getElementById('chat-project-panel');
+ if (panel && typeof applyRBACToUI === 'function') applyRBACToUI(panel);
requestAnimationFrame(() => document.getElementById('chat-project-search')?.focus());
}
diff --git a/web/static/js/rbac-guards.js b/web/static/js/rbac-guards.js
new file mode 100644
index 00000000..c6cf98b0
--- /dev/null
+++ b/web/static/js/rbac-guards.js
@@ -0,0 +1,278 @@
+/**
+ * 全局写操作权限守卫:为各页面 onclick 绑定的 window/C2 方法统一包一层 requirePermission。
+ * 在全部业务脚本加载完成后执行(见 index.html 引用顺序)。
+ */
+(function () {
+ 'use strict';
+
+ const GLOBAL_WRITE_HANDLER_PERMISSIONS = {
+ // 对话 / 分组
+ sendMessage: 'chat:write',
+ startNewConversation: 'chat:write',
+ deleteConversation: 'chat:delete',
+ deleteConversationTurnFromUI: 'chat:delete',
+ deleteConversationFromContext: 'chat:delete',
+ showBatchManageModal: 'chat:delete',
+ deleteSelectedConversations: 'chat:delete',
+ renameConversation: 'chat:write',
+ pinConversation: 'chat:write',
+ showCreateGroupModal: 'group:write',
+ createGroup: 'group:write',
+ editGroup: 'group:write',
+ deleteGroup: 'group:delete',
+ deleteGroupFromContext: 'group:delete',
+ pinGroupFromContext: 'group:write',
+ renameGroupFromContext: 'group:write',
+ applyBatchGroupChange: 'group:write',
+ applyCustomIcon: 'group:write',
+
+ // 人机协同
+ applyHitlSidebarConfig: 'hitl:write',
+ saveHitlPageWhitelist: 'hitl:write',
+ saveHitlAuditStrategy: 'hitl:write',
+ saveHitlConversationConfig: 'hitl:write',
+ submitHitlDecision: 'hitl:write',
+ submitHitlDecisionWithPayload: 'hitl:write',
+ submitWorkflowHitlDecisionFromPage: 'hitl:write',
+ submitWorkflowHitlDecision: 'hitl:write',
+ dismissHitlItem: 'hitl:write',
+ batchDeleteHitlLogs: 'hitl:write',
+ clearHitlLogs: 'hitl:write',
+
+ // 项目
+ showNewProjectModal: 'project:write',
+ showNewProjectModalFromChat: 'project:write',
+ showNewProjectModalFromWebshellAi: 'project:write',
+ showEditProjectModal: 'project:write',
+ saveProjectModal: 'project:write',
+ saveProjectSettings: 'project:write',
+ archiveCurrentProject: 'project:write',
+ deleteCurrentProject: 'project:delete',
+ deleteProjectFromListMenu: 'project:delete',
+ toggleProjectFactGraphConnectMode: 'project:write',
+ editProjectFromListMenu: 'project:write',
+ toggleProjectArchiveFromListMenu: 'project:write',
+ showAddFactModal: 'project:write',
+ showEditFactModal: 'project:write',
+ editSelectedGraphFact: 'project:write',
+ editFactFromDetail: 'project:write',
+ saveFactModal: 'project:write',
+ deleteProjectFactEdge: 'project:delete',
+ deprecateProjectFactByKey: 'project:write',
+ restoreProjectFactByKey: 'project:write',
+ promoteConversationAttackChain: 'attackchain:write',
+ linkFactToExistingVulnerability: 'project:write',
+ createVulnerabilityFromCurrentFact: 'vulnerability:write',
+ unbindConversationFromProject: 'project:write',
+
+ // 漏洞
+ showAddVulnerabilityModal: 'vulnerability:write',
+ saveVulnerability: 'vulnerability:write',
+ deleteVulnerability: 'vulnerability:delete',
+ batchDeleteVulnerabilityReports: 'vulnerability:delete',
+ exportVulnerabilityReports: 'vulnerability:read',
+ changeVulnerabilityStatus: 'vulnerability:write',
+ bindVulnerabilityProject: 'vulnerability:write',
+
+ // 角色 / Skills / Agents
+ showAddRoleModal: 'roles:write',
+ saveRole: 'roles:write',
+ deleteRole: 'roles:delete',
+ showAddSkillModal: 'skills:write',
+ saveSkill: 'skills:write',
+ deleteSkill: 'skills:delete',
+ showAddMarkdownAgentModal: 'agents:write',
+ saveMarkdownAgent: 'agents:write',
+ deleteMarkdownAgent: 'agents:delete',
+
+ // 知识库
+ buildKnowledgeIndex: 'knowledge:write',
+ rebuildKnowledgeIndexFull: 'knowledge:write',
+ showAddKnowledgeItemModal: 'knowledge:write',
+ saveKnowledgeItem: 'knowledge:write',
+ editKnowledgeItem: 'knowledge:write',
+ deleteKnowledgeItem: 'knowledge:delete',
+ deleteRetrievalLog: 'knowledge:delete',
+
+ // 设置 / MCP
+ applySettings: 'config:write',
+ saveToolsConfig: 'config:write',
+ saveExternalMCP: 'mcp:write',
+ showAddExternalMCPModal: 'mcp:write',
+ deleteExternalMCP: 'mcp:write',
+ toggleExternalMCP: 'mcp:write',
+ changePassword: 'auth:self',
+ testOpenAIConnection: 'config:write',
+ testVisionConnection: 'config:write',
+ testHitlAuditModelConnection: 'config:write',
+ submitMcpToolAbortModal: 'monitor:write',
+ cancelMCPToolExecution: 'monitor:write',
+
+ // FOFA / 信息收集
+ submitFofaSearch: 'fofa:execute',
+ scanFofaRow: 'fofa:execute',
+ batchScanSelectedFofaRows: 'fofa:execute',
+ exportFofaResults: 'fofa:execute',
+
+ // 任务队列
+ showBatchImportModal: 'tasks:write',
+ deleteBatchQueue: 'tasks:delete',
+ deleteBatchQueueFromList: 'tasks:delete',
+ createBatchQueue: 'tasks:write',
+ saveAddBatchTask: 'tasks:write',
+ saveInlineTask: 'tasks:write',
+ deleteBatchTask: 'tasks:delete',
+ deleteBatchTaskFromElement: 'tasks:delete',
+ saveInlineTitle: 'tasks:write',
+ saveInlineRole: 'tasks:write',
+ saveInlineAgentMode: 'tasks:write',
+ saveInlineConcurrency: 'tasks:write',
+ saveInlineSchedule: 'tasks:write',
+ startBatchQueue: 'tasks:write',
+ pauseBatchQueue: 'tasks:write',
+ rerunBatchQueue: 'tasks:write',
+ runSingleBatchTask: 'tasks:write',
+ editBatchTaskFromElement: 'tasks:write',
+ batchCancelTasks: 'tasks:write',
+ cancelTask: 'tasks:write',
+ cancelActiveTask: 'tasks:write',
+ cancelProgressTask: 'tasks:write',
+
+ // 工作流
+ saveWorkflowDraft: 'workflow:write',
+ applyWorkflowMetaModal: 'workflow:write',
+ deleteCurrentWorkflow: 'workflow:delete',
+ deleteWorkflowSelection: 'workflow:delete',
+ dryRunWorkflowDraft: 'workflow:execute',
+ toggleWorkflowEnabled: 'workflow:write',
+ addWorkflowNodeFromPalette: 'workflow:write',
+ toggleWorkflowConnectMode: 'workflow:write',
+ addWorkflowCustomField: 'workflow:write',
+
+ // 文件管理
+ saveChatFilesEdit: 'files:write',
+ deleteChatFile: 'files:delete',
+ deleteChatFileIdx: 'files:delete',
+ deleteChatFolderFromBrowse: 'files:delete',
+ submitChatFilesRename: 'files:write',
+ submitChatFilesMkdir: 'files:write',
+ chatFilesOpenUploadPicker: 'files:write',
+ chatFilesUploadFiles: 'files:write',
+ onChatFilesUploadPick: 'files:write',
+ chatFilesUploadToFolderClick: 'files:write',
+ chatFilesDeleteFolderFromBtn: 'files:delete',
+
+ // 监控
+ deleteExecution: 'monitor:delete',
+ batchDeleteExecutions: 'monitor:delete',
+
+ // 攻击链
+ regenerateAttackChain: 'attackchain:write',
+ exportAttackChain: 'attackchain:read',
+
+ // 通知
+ markAllNotificationsSeen: 'notification:write',
+
+ // RBAC
+ saveRbacUser: 'rbac:write',
+ deleteSelectedRbacUser: 'rbac:write',
+ saveRbacRole: 'rbac:write',
+ deleteRbacRole: 'rbac:write',
+ createRbacAssignment: 'rbac:write',
+ deleteRbacAssignment: 'rbac:write',
+ saveSelectedUserRoles: 'rbac:write',
+
+ // WebShell
+ showAddWebshellModal: 'webshell:write',
+ showEditWebshellModal: 'webshell:write',
+ saveWebshellConnection: 'webshell:write',
+ deleteWebshell: 'webshell:delete',
+ testWebshellConnection: 'webshell:write',
+
+ // 机器人
+ openRobotCreateModal: 'robot:write',
+ openRobotEditor: 'robot:write',
+ startWechatRobotBind: 'robot:write',
+ submitWechatVerifyCode: 'robot:write',
+
+ // 审计
+ exportAuditLogs: 'audit:read',
+ exportAuditLogsCsv: 'audit:read',
+ runAuditExport: 'audit:read',
+
+ // Agent 中断
+ submitUserInterruptContinue: 'agent:execute',
+ submitUserInterruptHardCancel: 'agent:execute',
+
+ // 终端(多会话)
+ addTerminalTab: 'terminal:execute',
+ removeTerminalTab: 'terminal:execute',
+ };
+
+ const NAMESPACE_WRITE_HANDLER_PERMISSIONS = {
+ C2: {
+ showCreateListenerModal: 'c2:write',
+ createListener: 'c2:write',
+ saveListener: 'c2:write',
+ editListener: 'c2:write',
+ startListener: 'c2:write',
+ stopListener: 'c2:write',
+ deleteListener: 'c2:delete',
+ deleteSessionRecord: 'c2:delete',
+ deleteSelectedSessions: 'c2:delete',
+ deleteFilteredSessions: 'c2:delete',
+ killSession: 'c2:write',
+ setSessionSleep: 'c2:write',
+ submitSessionSleep: 'c2:write',
+ uploadFileToImplant: 'c2:write',
+ onC2FileUploadPick: 'c2:write',
+ openFileUploadPicker: 'c2:write',
+ deleteTaskById: 'c2:delete',
+ deleteSelectedTasks: 'c2:delete',
+ cancelTask: 'c2:write',
+ buildBeacon: 'c2:write',
+ generateOneliner: 'c2:write',
+ createProfile: 'c2:write',
+ showCreateProfileModal: 'c2:write',
+ deleteProfile: 'c2:delete',
+ deleteEventById: 'c2:delete',
+ deleteSelectedEvents: 'c2:delete',
+ runTerminalCommand: 'terminal:execute',
+ executeInTerminal: 'terminal:execute',
+ },
+ };
+
+ function wrapHandlerWithPermission(fn, permission) {
+ if (typeof fn !== 'function') return fn;
+ if (fn.__rbacGuarded) return fn;
+ const wrapped = function rbacGuardedHandler(...args) {
+ if (typeof requirePermission === 'function' && !requirePermission(permission)) {
+ return undefined;
+ }
+ return fn.apply(this, args);
+ };
+ wrapped.__rbacGuarded = true;
+ wrapped.__rbacOriginal = fn;
+ return wrapped;
+ }
+
+ function installWriteHandlerGuards() {
+ Object.entries(GLOBAL_WRITE_HANDLER_PERMISSIONS).forEach(([name, permission]) => {
+ if (typeof window[name] === 'function') {
+ window[name] = wrapHandlerWithPermission(window[name], permission);
+ }
+ });
+ Object.entries(NAMESPACE_WRITE_HANDLER_PERMISSIONS).forEach(([ns, methods]) => {
+ const root = window[ns];
+ if (!root || typeof root !== 'object') return;
+ Object.entries(methods).forEach(([method, permission]) => {
+ if (typeof root[method] === 'function') {
+ root[method] = wrapHandlerWithPermission(root[method], permission);
+ }
+ });
+ });
+ }
+
+ window.installWriteHandlerGuards = installWriteHandlerGuards;
+ installWriteHandlerGuards();
+})();
diff --git a/web/static/js/rbac.js b/web/static/js/rbac.js
index 6336dff7..cefe0931 100644
--- a/web/static/js/rbac.js
+++ b/web/static/js/rbac.js
@@ -209,6 +209,10 @@ function renderRbacPendingSelection() {
}
function rbacNotify(message, type = 'info') {
+ if (typeof notifyApiError === 'function') {
+ notifyApiError(message, type);
+ return;
+ }
if (typeof showNotification === 'function') showNotification(message, type);
else if (type === 'error') alert(message);
}
diff --git a/web/static/js/roles.js b/web/static/js/roles.js
index ccfa075c..79f7af16 100644
--- a/web/static/js/roles.js
+++ b/web/static/js/roles.js
@@ -1279,6 +1279,7 @@ function setSelectedRoleTools(selectedToolKeys) {
// 显示添加角色模态框
async function showAddRoleModal() {
+ if (typeof requirePermission === 'function' && !requirePermission('roles:write')) return;
const modal = document.getElementById('role-modal');
if (!modal) return;
@@ -1627,6 +1628,7 @@ async function loadAllToolsToStateMap() {
// 保存角色
async function saveRole() {
+ if (typeof requirePermission === 'function' && !requirePermission('roles:write')) return;
const name = document.getElementById('role-name').value.trim();
if (!name) {
showNotification(_t('roleModal.roleNameRequired'), 'error');
diff --git a/web/static/js/router.js b/web/static/js/router.js
index fa9248b4..4b77dbcd 100644
--- a/web/static/js/router.js
+++ b/web/static/js/router.js
@@ -120,6 +120,10 @@ function switchPage(pageId) {
// 页面特定的初始化
initPage(pageId);
+
+ if (typeof applyRBACToUI === 'function') {
+ applyRBACToUI(targetPage);
+ }
}
}
window.switchPage = switchPage;
@@ -414,9 +418,11 @@ async function initPage(pageId) {
startExternalMcpPoll();
}
};
- // 先拉取配置(含 tool_search 常驻列表),再加载工具与外部 MCP
- if (typeof loadConfig === 'function') {
- loadConfig(false)
+ // 先拉取配置(含 tool_search 常驻列表),再加载工具与外部 MCP。
+ // 仅有 mcp:read 的用户无需拉全量 /api/config(需 config:read)。
+ const canLoadFullConfig = typeof hasPermission !== 'function' || hasPermission('config:read');
+ if (typeof loadConfig === 'function' && canLoadFullConfig) {
+ loadConfig(false, { silent: true })
.catch(err => {
console.warn('加载配置失败(将继续加载 MCP 列表):', err);
})
diff --git a/web/static/js/settings.js b/web/static/js/settings.js
index 7b2b8e0d..3e74d922 100644
--- a/web/static/js/settings.js
+++ b/web/static/js/settings.js
@@ -584,10 +584,14 @@ window.onclick = function(event) {
}
// 加载配置
-async function loadConfig(loadTools = true) {
+async function loadConfig(loadTools = true, options = {}) {
+ const silent = options && options.silent === true;
try {
const response = await apiFetch('/api/config');
if (!response.ok) {
+ if (typeof readApiError === 'function') {
+ throw new Error(await readApiError(response, '获取配置失败'));
+ }
throw new Error('获取配置失败');
}
@@ -993,10 +997,17 @@ async function loadConfig(loadTools = true) {
}
} catch (error) {
console.error('加载配置失败:', error);
- const baseMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
- ? window.t('settings.apply.loadFailed')
- : '加载配置失败';
- alert(baseMsg + ': ' + error.message);
+ if (!silent) {
+ const baseMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
+ ? window.t('settings.apply.loadFailed')
+ : '加载配置失败';
+ if (typeof notifyApiError === 'function') {
+ notifyApiError(baseMsg + ': ' + error.message);
+ } else {
+ alert(baseMsg + ': ' + error.message);
+ }
+ }
+ throw error;
}
}
@@ -1050,6 +1061,9 @@ async function loadToolsList(page = 1, searchKeyword = '', options = {}) {
clearTimeout(timeoutId);
if (!response.ok) {
+ if (typeof readApiError === 'function') {
+ throw new Error(await readApiError(response, '获取工具列表失败'));
+ }
throw new Error('获取工具列表失败');
}
@@ -2790,6 +2804,7 @@ async function testOpenAIConnection() {
// 保存工具配置(独立函数,用于MCP管理页面)
async function saveToolsConfig() {
+ if (typeof requirePermission === 'function' && !requirePermission('config:write')) return;
try {
// 先保存当前页的状态到全局映射
saveCurrentPageToolStates();
@@ -3004,7 +3019,12 @@ let currentEditingMCPName = null;
// 拉取外部MCP列表数据(供轮询使用,返回 { servers, stats })
async function fetchExternalMCPs() {
const response = await apiFetch('/api/external-mcp');
- if (!response.ok) throw new Error('获取外部MCP列表失败');
+ if (!response.ok) {
+ if (typeof readApiError === 'function') {
+ throw new Error(await readApiError(response, '获取外部MCP列表失败'));
+ }
+ throw new Error('获取外部MCP列表失败');
+ }
return response.json();
}
@@ -3207,6 +3227,7 @@ function renderExternalMCPStats(stats) {
// 显示添加外部MCP模态框
function showAddExternalMCPModal() {
+ if (typeof requirePermission === 'function' && !requirePermission('mcp:write')) return;
currentEditingMCPName = null;
document.getElementById('external-mcp-modal-title').textContent = (typeof window.t === 'function' ? window.t('mcp.addExternalMCP') : '添加外部MCP');
document.getElementById('external-mcp-json').value = '';
@@ -3316,6 +3337,7 @@ function loadExternalMCPExample() {
// 保存外部MCP
async function saveExternalMCP() {
+ if (typeof requirePermission === 'function' && !requirePermission('mcp:write')) return;
const jsonTextarea = document.getElementById('external-mcp-json');
const jsonStr = jsonTextarea.value.trim();
const errorDiv = document.getElementById('external-mcp-json-error');
diff --git a/web/static/js/skills.js b/web/static/js/skills.js
index ef35b615..e49048c8 100644
--- a/web/static/js/skills.js
+++ b/web/static/js/skills.js
@@ -472,6 +472,7 @@ function wireSkillModalOnce() {
}
function showAddSkillModal() {
+ if (typeof requirePermission === 'function' && !requirePermission('skills:write')) return;
wireSkillModalOnce();
const modal = document.getElementById('skill-modal');
if (!modal) return;
@@ -739,6 +740,7 @@ function closeSkillModal() {
// 保存skill
async function saveSkill() {
+ if (typeof requirePermission === 'function' && !requirePermission('skills:write')) return;
if (isSavingSkill) return;
const name = document.getElementById('skill-name').value.trim();
diff --git a/web/static/js/tasks.js b/web/static/js/tasks.js
index 57b4ea89..5bf480cd 100644
--- a/web/static/js/tasks.js
+++ b/web/static/js/tasks.js
@@ -988,6 +988,7 @@ document.addEventListener('DOMContentLoaded', function() {
// 创建批量任务队列
async function createBatchQueue() {
+ if (typeof requirePermission === 'function' && !requirePermission('tasks:write')) return;
const input = document.getElementById('batch-tasks-input');
const titleInput = document.getElementById('batch-queue-title');
const roleSelect = document.getElementById('batch-queue-role');
@@ -2250,6 +2251,7 @@ async function saveInlineTask(queueId, taskId) {
// 显示添加批量任务模态框
function showAddBatchTaskModal() {
+ if (typeof requirePermission === 'function' && !requirePermission('tasks:write')) return;
const queueId = batchQueuesState.currentQueueId;
if (!queueId) {
alert(_t('tasks.queueInfoMissing'));
diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js
index 3b74d3b4..ca90dec2 100644
--- a/web/static/js/vulnerability.js
+++ b/web/static/js/vulnerability.js
@@ -1422,7 +1422,7 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
-
+
@@ -1462,6 +1462,7 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(listContainer);
}
+ if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(listContainer);
restoreExpandedVulnerabilityDetails(opts.expandedIds);
@@ -1596,6 +1597,7 @@ async function populateVulnerabilityModalProjectSelect(selectedId) {
// 显示添加漏洞模态框
async function showAddVulnerabilityModal() {
+ if (typeof requirePermission === 'function' && !requirePermission('vulnerability:write')) return;
currentVulnerabilityId = null;
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln');
@@ -1659,6 +1661,7 @@ async function editVulnerability(id) {
// 保存漏洞
async function saveVulnerability() {
+ if (typeof requirePermission === 'function' && !requirePermission('vulnerability:write')) return;
const conversationId = document.getElementById('vulnerability-conversation-id').value.trim();
const title = document.getElementById('vulnerability-title').value.trim();
const description = document.getElementById('vulnerability-description').value.trim();
diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js
index 8992529f..9dd0028f 100644
--- a/web/static/js/webshell.js
+++ b/web/static/js/webshell.js
@@ -2240,10 +2240,13 @@ function selectWebshell(id, stateReady) {
' ' +
'