@aalis/plugin-authority 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authority-manager.d.ts +14 -19
- package/dist/authority-manager.d.ts.map +1 -1
- package/dist/authority-manager.js +64 -124
- package/dist/authority-manager.js.map +1 -1
- package/dist/authority-model.d.ts +51 -0
- package/dist/authority-model.d.ts.map +1 -0
- package/dist/authority-model.js +85 -0
- package/dist/authority-model.js.map +1 -0
- package/dist/capability-model.d.ts +0 -8
- package/dist/capability-model.d.ts.map +1 -1
- package/dist/capability-model.js +2 -15
- package/dist/capability-model.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +188 -346
- package/dist/index.js.map +1 -1
- package/dist/tier-model.d.ts +42 -0
- package/dist/tier-model.d.ts.map +1 -0
- package/dist/tier-model.js +83 -0
- package/dist/tier-model.js.map +1 -0
- package/dist/user-store.d.ts +5 -31
- package/dist/user-store.d.ts.map +1 -1
- package/dist/user-store.js +24 -156
- package/dist/user-store.js.map +1 -1
- package/package.json +20 -9
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@ import { useCommandService } from '@aalis/plugin-commands-api';
|
|
|
2
2
|
import { getPlatformNames } from '@aalis/plugin-platform-api';
|
|
3
3
|
import { createStorageGateway } from '@aalis/plugin-storage-api';
|
|
4
4
|
import { useWebuiService } from '@aalis/plugin-webui-api';
|
|
5
|
+
import { setNetworkPolicy } from '@aalis/util-network-guard';
|
|
5
6
|
import { AuthorityManager } from './authority-manager.js';
|
|
7
|
+
import { autoConfirmActive, DEFAULT_AUTHORITY, shouldSkipConfirm } from './authority-model.js';
|
|
6
8
|
export { AuthorityManager } from './authority-manager.js';
|
|
7
9
|
// ===== 插件元数据 =====
|
|
8
10
|
export const name = '@aalis/plugin-authority';
|
|
@@ -12,36 +14,9 @@ export const provides = ['authority'];
|
|
|
12
14
|
export const inject = {
|
|
13
15
|
optional: ['commands', 'tools'],
|
|
14
16
|
};
|
|
15
|
-
// 权限管理页(自定义 renderer 在 webui-client
|
|
16
|
-
// 复用通用 cytoscape 渲染器):能力委托模型下"上层分发下层"天然是一张图,比扁平列表直观。
|
|
17
|
+
// 权限管理页(自定义 renderer 在 webui-client)。单 owner 终态无委托树,故无委托关系图。
|
|
17
18
|
const webuiPages = [
|
|
18
19
|
{ key: 'authority', label: '权限管理', icon: 'authority', order: 50, renderer: 'authority' },
|
|
19
|
-
{
|
|
20
|
-
key: 'authority-graph',
|
|
21
|
-
label: '委托关系图',
|
|
22
|
-
icon: 'authority',
|
|
23
|
-
order: 51,
|
|
24
|
-
content: [
|
|
25
|
-
{
|
|
26
|
-
type: 'graph',
|
|
27
|
-
label: '委托关系图:owner → 子 → 孙 委托链 + 授予/拒绝能力 + 跨平台绑定(点节点看详情)',
|
|
28
|
-
source: 'getDelegationGraph',
|
|
29
|
-
detailSource: 'getDelegationNode',
|
|
30
|
-
defaultMaxDepth: 2,
|
|
31
|
-
nodeKinds: [
|
|
32
|
-
{ kind: 'owner', label: 'Owner(*)', shape: 'diamond', color: '#fbbf24' },
|
|
33
|
-
{ kind: 'user', label: '用户', shape: 'circle', color: '#60a5fa' },
|
|
34
|
-
{ kind: 'cap', label: '能力', shape: 'round-rect', color: '#9ca3af' },
|
|
35
|
-
],
|
|
36
|
-
edgeKinds: [
|
|
37
|
-
{ kind: 'delegate', label: '委托', color: '#34d399' },
|
|
38
|
-
{ kind: 'grant', label: '授予', color: '#60a5fa' },
|
|
39
|
-
{ kind: 'deny', label: '拒绝', color: '#ef4444', dashed: true },
|
|
40
|
-
{ kind: 'bind', label: '绑定', color: '#a78bfa', dashed: true },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
20
|
];
|
|
46
21
|
// ===== 插件入口 =====
|
|
47
22
|
export async function apply(ctx, _config) {
|
|
@@ -51,32 +26,67 @@ export async function apply(ctx, _config) {
|
|
|
51
26
|
const cmds = useCommandService(ctx);
|
|
52
27
|
const storage = createStorageGateway(ctx);
|
|
53
28
|
const authority = new AuthorityManager(ctx.config, ctx.logger, storage);
|
|
54
|
-
await authority.init();
|
|
55
29
|
ctx.provide('authority', authority);
|
|
56
|
-
//
|
|
30
|
+
// 用户等级存于 data:/users.json,读取依赖 storage 服务。storage provider 可能晚于本插件
|
|
31
|
+
// 上线(曾因此在 init 阶段 readFile 失败被静默吞 → 重启后等级不回载);改为 storage 就绪时再
|
|
32
|
+
// load,规避初始化时序竞态。whenService 对「已在线」的服务也会立即触发,故任意加载序都成立。
|
|
33
|
+
ctx.whenService('storage', () => {
|
|
34
|
+
void authority.init().then(() => ctx.logger.debug('授权用户等级已加载'), err => ctx.logger.warn(`授权用户等级加载失败: ${err}`));
|
|
35
|
+
});
|
|
36
|
+
// 网络出口闸(SSRF):把 core 配置 network 注入进程级 safeFetch 策略(启动一次)。
|
|
37
|
+
// 安全归属在权限域;本地固定服务走裸 fetch、不过 safeFetch,故不受影响。
|
|
38
|
+
setNetworkPolicy(ctx.config.get('network') ?? {});
|
|
39
|
+
// ===== 执行守卫:两轴正交闸 —— 轴 A 授权(authorize)+ 轴 B 确认(confirm,owner 也吃)=====
|
|
57
40
|
const guard = async (g) => {
|
|
58
41
|
const capability = `${g.type}:${g.name}`;
|
|
59
|
-
|
|
60
|
-
const
|
|
42
|
+
// 确认覆盖:'off' 强制关确认;否则覆盖值优先,回退插件声明。
|
|
43
|
+
const confOv = (ctx.config.get('confirmOverrides') ?? {});
|
|
44
|
+
const cOv = confOv[capability];
|
|
45
|
+
const confirm = cOv === 'off' ? undefined : (cOv ?? g.confirm);
|
|
61
46
|
const identity = { platform: g.platform, userId: g.userId };
|
|
62
|
-
|
|
63
|
-
const denied = authority.authorize(identity, { capability, visibility, resourceCapabilities: g.permissions });
|
|
64
|
-
if (!denied)
|
|
65
|
-
return null;
|
|
66
|
-
// 受限被拒:系统/受信源无人确认,直接返回拒绝;否则走交互确认(白名单/会话授予/回调)
|
|
67
|
-
if (g.skipConfirm)
|
|
68
|
-
return denied;
|
|
69
|
-
const granted = await authority.requestAccess({
|
|
47
|
+
const accessBase = {
|
|
70
48
|
name: g.name,
|
|
71
49
|
type: g.type,
|
|
72
50
|
capability,
|
|
73
|
-
resourceCapabilities: g.permissions,
|
|
74
51
|
args: g.args,
|
|
75
52
|
sessionId: g.sessionId,
|
|
76
53
|
platform: g.platform,
|
|
77
54
|
userId: g.userId,
|
|
55
|
+
};
|
|
56
|
+
// ── 轴 A · 授权:数字等级裁决(minLevel 由 risk/visibility/authorityOverrides 在 manager 内派生)——系统源也评估,防绕过提权 ──
|
|
57
|
+
const denied = authority.authorize(identity, {
|
|
58
|
+
capability,
|
|
59
|
+
visibility: g.visibility,
|
|
60
|
+
risk: g.risk,
|
|
78
61
|
});
|
|
79
|
-
|
|
62
|
+
if (denied) {
|
|
63
|
+
// 未授权(等级不够 / 硬禁 / 资源受限):**绝不**让发起者本人弹确认自我提权。
|
|
64
|
+
// 仅 owner 预先配置的放行(restrictedPolicy 白名单 / 该用户在本会话已有的授予)可救;否则硬拒。
|
|
65
|
+
// 注意:这里**不**调 requestAccess(那会询问发起者)——只查 isPreApproved(不问人)。
|
|
66
|
+
if (g.skipConfirm)
|
|
67
|
+
return denied;
|
|
68
|
+
return authority.isPreApproved(accessBase) ? null : denied;
|
|
69
|
+
}
|
|
70
|
+
// ── 轴 B · 确认:授权已过(含 owner / public / 已授予),但操作声明了 confirm 仍需「意图确认」 ──
|
|
71
|
+
// 仅对**已授权**操作做意图确认(owner 也吃,防注入借权);不再是提权入口。
|
|
72
|
+
// 跳过判定见 shouldSkipConfirm:always 永不跳(cron 无人确认即拒);非 always 可被
|
|
73
|
+
// skipConfirm(系统/受信源) 或 owner 本人 auto 模式 跳过。修 #6:旧实现用 `!g.skipConfirm`
|
|
74
|
+
// 门控整块 → skipConfirm 会连 always 一起绕过。
|
|
75
|
+
if (confirm) {
|
|
76
|
+
const skip = shouldSkipConfirm({
|
|
77
|
+
confirm,
|
|
78
|
+
skipConfirm: !!g.skipConfirm,
|
|
79
|
+
isOwner: authority.isOwner(g.platform, g.userId),
|
|
80
|
+
autoConfirmUntil: ctx.config.get('autoConfirmUntil') ?? 0,
|
|
81
|
+
now: Date.now(),
|
|
82
|
+
});
|
|
83
|
+
if (!skip) {
|
|
84
|
+
const ok = await authority.requestAccess({ ...accessBase, confirm });
|
|
85
|
+
if (!ok)
|
|
86
|
+
return `操作已取消:${capability} 需确认后执行`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
80
90
|
};
|
|
81
91
|
// 注入到 commands / tools(whenService 在 provider 上线/重启时各调一次)
|
|
82
92
|
ctx.whenService('commands', svc => {
|
|
@@ -93,28 +103,17 @@ export async function apply(ctx, _config) {
|
|
|
93
103
|
});
|
|
94
104
|
ctx.on('app:stopping', () => authority.save());
|
|
95
105
|
// ===== 权限指令 =====
|
|
96
|
-
// /authority [target] —
|
|
97
|
-
cmds.command('authority [target:string]', '
|
|
106
|
+
// /authority [target] — 查看自己或指定用户的权限等级
|
|
107
|
+
cmds.command('authority [target:string]', '查看自己或指定用户的权限等级').action(async (argv, target) => {
|
|
98
108
|
const describe = (platform, userId, self) => {
|
|
99
109
|
const isOwner = authority.isOwner(platform, userId);
|
|
100
110
|
const who = self ? '您' : `${platform}:${userId}`;
|
|
101
|
-
|
|
111
|
+
if (isOwner)
|
|
112
|
+
return `${who}(owner,等级 ∞,拥有全部权限)`;
|
|
102
113
|
const entry = userId
|
|
103
114
|
? authority.listUsers().find(u => u.platform === platform && u.userId === userId)
|
|
104
115
|
: undefined;
|
|
105
|
-
|
|
106
|
-
lines.push(`已绑定到主账户 ${entry.linkedTo}(能力以账户为准)`);
|
|
107
|
-
if (entry?.links?.length)
|
|
108
|
-
lines.push(`已绑定身份: ${entry.links.join(', ')}`);
|
|
109
|
-
if (entry?.grant?.length)
|
|
110
|
-
lines.push(`授予能力: ${entry.grant.join(', ')}`);
|
|
111
|
-
if (entry?.deny?.length)
|
|
112
|
-
lines.push(`禁用能力: ${entry.deny.join(', ')}`);
|
|
113
|
-
if (entry?.grantedBy)
|
|
114
|
-
lines.push(`委托自: ${entry.grantedBy}`);
|
|
115
|
-
if (!isOwner && !entry?.grant?.length)
|
|
116
|
-
lines.push('(默认拥有全部 public 能力)');
|
|
117
|
-
return lines.join('\n');
|
|
116
|
+
return `${who} 等级: ${entry?.level ?? DEFAULT_AUTHORITY}`;
|
|
118
117
|
};
|
|
119
118
|
const t = target;
|
|
120
119
|
if (t) {
|
|
@@ -125,65 +124,65 @@ export async function apply(ctx, _config) {
|
|
|
125
124
|
}
|
|
126
125
|
return describe(argv.session.platform, argv.session.userId, true);
|
|
127
126
|
});
|
|
128
|
-
// /
|
|
127
|
+
// /level <target> <整数> — owner 给外部身份设等级(越大越高,0=默认,负数=封禁)。权限管理仅 owner 可达(防自授)。
|
|
129
128
|
cmds
|
|
130
|
-
.command('
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
.action(async (argv, target, capability) => editCaps(argv, target, capability, 'deny'));
|
|
138
|
-
/** /grant、/deny 共用:往目标用户的 grant/deny 集追加一条能力(委托子集校验在 setUserCapabilities) */
|
|
139
|
-
function editCaps(argv, target, capability, field) {
|
|
129
|
+
.command('level <target:string> <level:number>', '设置用户权限等级(整数,越大越高;0 默认,负数封禁)', {
|
|
130
|
+
visibility: 'restricted',
|
|
131
|
+
})
|
|
132
|
+
.example('/level onebot:12345 5')
|
|
133
|
+
.action(async (argv, target, level) => {
|
|
134
|
+
if (!authority.isOwner(argv.session.platform, argv.session.userId))
|
|
135
|
+
return '只有 owner 可管理权限';
|
|
140
136
|
const t = String(target);
|
|
141
|
-
const cap = String(capability).trim();
|
|
142
137
|
const sep = t.indexOf(':');
|
|
143
138
|
if (sep < 1)
|
|
144
139
|
return '目标格式: <platform:userId>';
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
grant: field === 'grant' ? next : cur?.grant,
|
|
154
|
-
deny: field === 'deny' ? next : cur?.deny,
|
|
155
|
-
});
|
|
156
|
-
authority.save();
|
|
157
|
-
return `已${field === 'grant' ? '授予' : '禁用'} ${t}: ${cap}`;
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
return err instanceof Error ? err.message : String(err);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// /bind <code> — 把当前平台账号绑定到 WebUI 主账户(码在 WebUI 权限页生成)。
|
|
164
|
-
// 仅限私聊:群聊发码会暴露给旁观者。
|
|
140
|
+
const lv = Number(level);
|
|
141
|
+
if (!Number.isInteger(lv))
|
|
142
|
+
return '等级必须是整数';
|
|
143
|
+
authority.setUserLevel({ platform: t.slice(0, sep), userId: t.slice(sep + 1) }, lv);
|
|
144
|
+
authority.save();
|
|
145
|
+
return `已设 ${t} 等级: ${lv}`;
|
|
146
|
+
});
|
|
147
|
+
// /auto [分钟|off|on] — owner 临时免 dangerous 二次确认(批处理便利)。on=一直, off=关, 数字=分钟。
|
|
165
148
|
cmds
|
|
166
|
-
.command('
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (platform
|
|
173
|
-
return '
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
149
|
+
.command('auto [arg:string]', '自动确认模式:临时免 dangerous 二次确认(仅 owner 本人)', {
|
|
150
|
+
visibility: 'restricted',
|
|
151
|
+
})
|
|
152
|
+
.example('/auto 30')
|
|
153
|
+
.example('/auto off')
|
|
154
|
+
.action(async (argv, arg) => {
|
|
155
|
+
if (!authority.isOwner(argv.session.platform, argv.session.userId))
|
|
156
|
+
return '只有 owner 可管理权限';
|
|
157
|
+
const a = arg === undefined ? undefined : String(arg).trim().toLowerCase();
|
|
158
|
+
const setUntil = (u) => {
|
|
159
|
+
ctx.config.set('autoConfirmUntil', u);
|
|
160
|
+
ctx.getService('app')?.saveConfig();
|
|
161
|
+
};
|
|
162
|
+
if (a === undefined) {
|
|
163
|
+
const u = ctx.config.get('autoConfirmUntil') ?? 0;
|
|
164
|
+
if (u === -1)
|
|
165
|
+
return '自动确认:一直开启';
|
|
166
|
+
if (autoConfirmActive(u, Date.now()))
|
|
167
|
+
return `自动确认:开启中,剩 ${Math.ceil((u - Date.now()) / 60000)} 分钟`;
|
|
168
|
+
return '自动确认:关闭';
|
|
169
|
+
}
|
|
170
|
+
if (a === 'off' || a === '0') {
|
|
171
|
+
setUntil(0);
|
|
172
|
+
return '已关闭自动确认';
|
|
180
173
|
}
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
if (a === 'on') {
|
|
175
|
+
setUntil(-1);
|
|
176
|
+
return '已开启自动确认(一直,直到手动关闭)';
|
|
183
177
|
}
|
|
178
|
+
const m = Number(a);
|
|
179
|
+
if (!Number.isInteger(m) || m <= 0)
|
|
180
|
+
return '用法:/auto <分钟> | off | on';
|
|
181
|
+
setUntil(Date.now() + m * 60000);
|
|
182
|
+
return `已开启自动确认 ${m} 分钟`;
|
|
184
183
|
});
|
|
185
184
|
}
|
|
186
|
-
// ===== WebUI
|
|
185
|
+
// ===== WebUI 操作处理器(数字等级单轴:用户等级 + 操作门槛 + owner 列表 + 高级)=====
|
|
187
186
|
function asStringList(v, label) {
|
|
188
187
|
if (v === undefined || v === null)
|
|
189
188
|
return undefined;
|
|
@@ -192,7 +191,7 @@ function asStringList(v, label) {
|
|
|
192
191
|
return v;
|
|
193
192
|
}
|
|
194
193
|
export const actions = {
|
|
195
|
-
/**
|
|
194
|
+
/** 权限概览:用户等级 + owner + 操作门槛/确认 + 临时放行 + 受限/禁用清单 */
|
|
196
195
|
async getOverview(ctx) {
|
|
197
196
|
const auth = ctx.getService('authority');
|
|
198
197
|
const users = auth?.listUsers() ?? [];
|
|
@@ -212,242 +211,52 @@ export const actions = {
|
|
|
212
211
|
users,
|
|
213
212
|
owners,
|
|
214
213
|
platforms,
|
|
215
|
-
restrictedCapabilities: ctx.config.get('restrictedCapabilities') ?? [],
|
|
216
214
|
deniedCapabilities: ctx.config.get('deniedCapabilities') ?? [],
|
|
217
|
-
|
|
215
|
+
authorityOverrides: ctx.config.get('authorityOverrides') ?? {},
|
|
216
|
+
defaultAuthority: DEFAULT_AUTHORITY,
|
|
217
|
+
confirmOverrides: ctx.config.get('confirmOverrides') ?? {},
|
|
218
|
+
autoConfirmUntil: ctx.config.get('autoConfirmUntil') ?? 0,
|
|
218
219
|
restrictedPolicy: ctx.config.get('restrictedPolicy') ?? {},
|
|
219
220
|
temporaryGrants: auth?.listTemporaryGrants() ?? [],
|
|
220
221
|
commandPrefix,
|
|
222
|
+
// 操作清单:指令 + 工具统一带 pluginName/type/confirm,供前端「操作」视图按插件分组、显示两轴默认。
|
|
221
223
|
commands: cmdNodes.map(n => ({
|
|
222
224
|
key: n.name,
|
|
223
225
|
name: n.name,
|
|
226
|
+
type: 'command',
|
|
224
227
|
displayName: `${commandPrefix}${n.name.split('.').join(' ')}`,
|
|
228
|
+
pluginName: n.pluginName,
|
|
225
229
|
visibility: n.visibility ?? 'public',
|
|
230
|
+
confirm: n.confirm,
|
|
231
|
+
risk: n.risk,
|
|
232
|
+
})),
|
|
233
|
+
tools: tools.map(t => ({
|
|
234
|
+
key: t.name,
|
|
235
|
+
name: t.name,
|
|
236
|
+
type: 'tool',
|
|
237
|
+
displayName: t.name,
|
|
238
|
+
pluginName: t.pluginName,
|
|
239
|
+
visibility: t.visibility ?? 'public',
|
|
240
|
+
confirm: t.confirm,
|
|
241
|
+
risk: t.risk,
|
|
226
242
|
})),
|
|
227
|
-
tools: tools.map(t => ({ key: t.name, name: t.name, visibility: t.visibility ?? 'public' })),
|
|
228
|
-
};
|
|
229
|
-
},
|
|
230
|
-
/**
|
|
231
|
-
* 委托关系图数据(喂通用 cytoscape graph 组件,协议与 user-relation getRelationGraph 对齐):
|
|
232
|
-
* 用户节点(owner/user)+ 能力节点,边 = 委托(父→子) / 授予 / 拒绝 / 绑定(被绑身份→主账户)。
|
|
233
|
-
* 保证每条边两端节点都存在;支持焦点子图导航(args.focusId 为节点或边 id + maxDepth/maxBreadth),
|
|
234
|
-
* 点边时回 focusEdge(详情卡片用)。无 focusId 返回全图。
|
|
235
|
-
*/
|
|
236
|
-
async getDelegationGraph(ctx, args) {
|
|
237
|
-
const auth = ctx.getService('authority');
|
|
238
|
-
const users = auth?.listUsers() ?? [];
|
|
239
|
-
const owners = ctx.config.get('owners') ?? [];
|
|
240
|
-
const nodes = new Map();
|
|
241
|
-
const edges = [];
|
|
242
|
-
const ensureUser = (key) => {
|
|
243
|
-
const id = `user:${key}`;
|
|
244
|
-
if (nodes.has(id))
|
|
245
|
-
return id;
|
|
246
|
-
const i = key.indexOf(':');
|
|
247
|
-
const isOwner = i > 0 ? (auth?.isOwner(key.slice(0, i), key.slice(i + 1)) ?? false) : false;
|
|
248
|
-
nodes.set(id, { data: { id, label: key, kind: isOwner ? 'owner' : 'user', pageRankScale: isOwner ? 0.7 : 0.5 } });
|
|
249
|
-
return id;
|
|
250
|
-
};
|
|
251
|
-
const ensureCap = (pat) => {
|
|
252
|
-
const id = `cap:${pat}`;
|
|
253
|
-
if (!nodes.has(id))
|
|
254
|
-
nodes.set(id, { data: { id, label: pat, kind: 'cap', pageRankScale: 0.3 } });
|
|
255
|
-
return id;
|
|
256
|
-
};
|
|
257
|
-
for (const o of owners)
|
|
258
|
-
ensureUser(`${o.platform}:${o.userId}`);
|
|
259
|
-
for (const u of users) {
|
|
260
|
-
const key = `${u.platform}:${u.userId}`;
|
|
261
|
-
const src = ensureUser(key);
|
|
262
|
-
for (const g of u.grant ?? [])
|
|
263
|
-
edges.push({
|
|
264
|
-
data: { id: `grant:${key}:${g}`, source: src, target: ensureCap(g), label: '授予', kind: 'grant' },
|
|
265
|
-
});
|
|
266
|
-
for (const d of u.deny ?? [])
|
|
267
|
-
edges.push({
|
|
268
|
-
data: { id: `deny:${key}:${d}`, source: src, target: ensureCap(d), label: '拒绝', kind: 'deny' },
|
|
269
|
-
});
|
|
270
|
-
if (u.grantedBy)
|
|
271
|
-
edges.push({
|
|
272
|
-
data: {
|
|
273
|
-
id: `delegate:${key}`,
|
|
274
|
-
source: ensureUser(u.grantedBy),
|
|
275
|
-
target: src,
|
|
276
|
-
label: '委托',
|
|
277
|
-
kind: 'delegate',
|
|
278
|
-
directed: true,
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
if (u.linkedTo)
|
|
282
|
-
edges.push({
|
|
283
|
-
data: {
|
|
284
|
-
id: `bind:${key}`,
|
|
285
|
-
source: src,
|
|
286
|
-
target: ensureUser(u.linkedTo),
|
|
287
|
-
label: '绑定',
|
|
288
|
-
kind: 'bind',
|
|
289
|
-
directed: true,
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
// owner → 「* 全部能力」:owner 持有 `*`,不逐条 grant,否则会是孤立节点。连一个
|
|
294
|
-
// `*` 能力节点直观表达"拥有一切",也让焦点/邻域有内容可展开。
|
|
295
|
-
const ownerIds = [...nodes.values()].filter(n => n.data.kind === 'owner').map(n => String(n.data.id));
|
|
296
|
-
if (ownerIds.length > 0) {
|
|
297
|
-
const allCap = 'cap:*';
|
|
298
|
-
if (!nodes.has(allCap))
|
|
299
|
-
nodes.set(allCap, { data: { id: allCap, label: '★ 全部能力 (*)', kind: 'cap', pageRankScale: 0.6 } });
|
|
300
|
-
for (const id of ownerIds)
|
|
301
|
-
edges.push({
|
|
302
|
-
data: { id: `own:${id}`, source: id, target: allCap, label: '拥有全部', kind: 'grant', directed: true },
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
const stats = {
|
|
306
|
-
用户: users.length,
|
|
307
|
-
owner: owners.length,
|
|
308
|
-
能力节点: [...nodes.keys()].filter(k => k.startsWith('cap:')).length,
|
|
309
|
-
};
|
|
310
|
-
// 焦点子图导航:无 focusId → 全图;有则从焦点(节点或边两端)BFS maxDepth/maxBreadth。
|
|
311
|
-
const focusId = typeof args?.focusId === 'string' && args.focusId.trim() ? args.focusId.trim() : undefined;
|
|
312
|
-
if (!focusId)
|
|
313
|
-
return { nodes: [...nodes.values()], edges, stats };
|
|
314
|
-
const maxDepth = Number.isFinite(Number(args?.maxDepth)) ? Number(args?.maxDepth) : 2;
|
|
315
|
-
const maxBreadth = Number.isFinite(Number(args?.maxBreadth)) ? Number(args?.maxBreadth) : 10;
|
|
316
|
-
const edgeMatch = edges.find(e => e.data.id === focusId);
|
|
317
|
-
const starts = edgeMatch
|
|
318
|
-
? [String(edgeMatch.data.source), String(edgeMatch.data.target)]
|
|
319
|
-
: nodes.has(focusId)
|
|
320
|
-
? [focusId]
|
|
321
|
-
: [];
|
|
322
|
-
const focusEdge = edgeMatch
|
|
323
|
-
? {
|
|
324
|
-
id: String(edgeMatch.data.id),
|
|
325
|
-
kind: String(edgeMatch.data.kind ?? ''),
|
|
326
|
-
description: String(edgeMatch.data.label ?? ''),
|
|
327
|
-
endpoints: [String(edgeMatch.data.source), String(edgeMatch.data.target)],
|
|
328
|
-
directed: edgeMatch.data.directed === true,
|
|
329
|
-
}
|
|
330
|
-
: undefined;
|
|
331
|
-
// 无向邻接:节点 id → [{edgeId, other}]
|
|
332
|
-
const adj = new Map();
|
|
333
|
-
for (const e of edges) {
|
|
334
|
-
const s = String(e.data.source);
|
|
335
|
-
const t = String(e.data.target);
|
|
336
|
-
const id = String(e.data.id);
|
|
337
|
-
(adj.get(s) ?? adj.set(s, []).get(s))?.push({ edgeId: id, other: t });
|
|
338
|
-
(adj.get(t) ?? adj.set(t, []).get(t))?.push({ edgeId: id, other: s });
|
|
339
|
-
}
|
|
340
|
-
const keptNodes = new Set(starts.filter(id => nodes.has(id)));
|
|
341
|
-
const keptEdges = new Set(edgeMatch ? [focusId] : []);
|
|
342
|
-
let frontier = [...keptNodes];
|
|
343
|
-
for (let d = 0; d < maxDepth && frontier.length > 0; d++) {
|
|
344
|
-
const next = [];
|
|
345
|
-
for (const id of frontier) {
|
|
346
|
-
for (const { edgeId, other } of (adj.get(id) ?? []).slice(0, maxBreadth)) {
|
|
347
|
-
keptEdges.add(edgeId);
|
|
348
|
-
if (!keptNodes.has(other)) {
|
|
349
|
-
keptNodes.add(other);
|
|
350
|
-
next.push(other);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
frontier = next;
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
focusId,
|
|
358
|
-
focusEdge,
|
|
359
|
-
nodes: [...nodes.values()].filter(n => keptNodes.has(String(n.data.id))),
|
|
360
|
-
edges: edges.filter(e => keptEdges.has(String(e.data.id)) &&
|
|
361
|
-
keptNodes.has(String(e.data.source)) &&
|
|
362
|
-
keptNodes.has(String(e.data.target))),
|
|
363
|
-
stats,
|
|
364
243
|
};
|
|
365
244
|
},
|
|
366
|
-
/**
|
|
367
|
-
async
|
|
368
|
-
const
|
|
369
|
-
const auth = ctx.getService('authority');
|
|
370
|
-
const users = auth?.listUsers() ?? [];
|
|
371
|
-
if (nodeId.startsWith('user:')) {
|
|
372
|
-
const key = nodeId.slice(5);
|
|
373
|
-
const i = key.indexOf(':');
|
|
374
|
-
const isOwner = i > 0 ? (auth?.isOwner(key.slice(0, i), key.slice(i + 1)) ?? false) : false;
|
|
375
|
-
const u = users.find(x => `${x.platform}:${x.userId}` === key);
|
|
376
|
-
return {
|
|
377
|
-
身份: key,
|
|
378
|
-
类型: isOwner ? 'owner(拥有一切能力)' : '用户',
|
|
379
|
-
授予: u?.grant?.join('、') || '(无)',
|
|
380
|
-
拒绝: u?.deny?.join('、') || '(无)',
|
|
381
|
-
委托自: u?.grantedBy || '(顶层 / owner 直接)',
|
|
382
|
-
可登录账户: u?.hasPassword ? '是' : '否',
|
|
383
|
-
绑定: u?.links?.join('、') || (u?.linkedTo ? `→ ${u.linkedTo}` : '(无)'),
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
if (nodeId.startsWith('cap:')) {
|
|
387
|
-
const pat = nodeId.slice(4);
|
|
388
|
-
const granters = users.filter(u => (u.grant ?? []).some(g => g === pat)).map(u => `${u.platform}:${u.userId}`);
|
|
389
|
-
const deniers = users.filter(u => (u.deny ?? []).some(d => d === pat)).map(u => `${u.platform}:${u.userId}`);
|
|
390
|
-
return { 能力: pat, 授予给: granters.join('、') || '(无)', 拒绝于: deniers.join('、') || '(无)' };
|
|
391
|
-
}
|
|
392
|
-
return { id: nodeId };
|
|
393
|
-
},
|
|
394
|
-
/** 委托:设置用户能力 grant/deny(caller 为授予方,非 owner 时子集校验在 manager 内) */
|
|
395
|
-
async setUserCapabilities(ctx, args, caller) {
|
|
396
|
-
const { platform, userId, grant, deny } = args;
|
|
245
|
+
/** 设置外部身份等级(覆盖式整数)。权限管理仅 owner 可达(防自我提权)。 */
|
|
246
|
+
async setUserLevel(ctx, args, caller) {
|
|
247
|
+
const { platform, userId, level } = args;
|
|
397
248
|
if (!platform || !userId)
|
|
398
249
|
throw new Error('platform, userId 必填');
|
|
250
|
+
if (typeof level !== 'number' || !Number.isInteger(level))
|
|
251
|
+
throw new Error('level 必须是整数');
|
|
399
252
|
const auth = ctx.getService('authority');
|
|
400
253
|
if (!auth)
|
|
401
254
|
throw new Error('Authority 服务不可用');
|
|
402
|
-
auth.
|
|
255
|
+
if (caller && !auth.isOwner(caller.platform, caller.userId))
|
|
256
|
+
throw new Error('只有 owner 可管理权限');
|
|
257
|
+
auth.setUserLevel({ platform: platform, userId: userId }, level);
|
|
403
258
|
auth.save();
|
|
404
|
-
return { message: `${platform}:${userId}
|
|
405
|
-
},
|
|
406
|
-
/** 设置/重置账户密码(owner 或本人) */
|
|
407
|
-
async setPassword(ctx, args, caller) {
|
|
408
|
-
const { platform, userId, password } = args;
|
|
409
|
-
if (!platform || !userId || typeof password !== 'string')
|
|
410
|
-
throw new Error('platform, userId, password 必填');
|
|
411
|
-
if (password.length < 6)
|
|
412
|
-
throw new Error('密码长度至少 6 位');
|
|
413
|
-
const auth = ctx.getService('authority');
|
|
414
|
-
if (!auth)
|
|
415
|
-
throw new Error('Authority 服务不可用');
|
|
416
|
-
const isSelf = caller && caller.platform === platform && caller.userId === userId;
|
|
417
|
-
if (caller && !isSelf && !auth.isOwner(caller.platform, caller.userId)) {
|
|
418
|
-
throw new Error('只有 owner 或本人可设置密码');
|
|
419
|
-
}
|
|
420
|
-
await auth.setPassword(platform, userId, password);
|
|
421
|
-
auth.save();
|
|
422
|
-
return { message: `${platform}:${userId} 密码已更新` };
|
|
423
|
-
},
|
|
424
|
-
async createBindCode(ctx, _args, caller) {
|
|
425
|
-
if (!caller)
|
|
426
|
-
throw new Error('无法识别调用者身份');
|
|
427
|
-
const auth = ctx.getService('authority');
|
|
428
|
-
if (!auth)
|
|
429
|
-
throw new Error('Authority 服务不可用');
|
|
430
|
-
const { code, expiresAt } = auth.createBindCode(caller.platform, caller.userId);
|
|
431
|
-
const prefix = ctx.getService('commands')?.prefix ?? '/';
|
|
432
|
-
return { code, expiresAt, hint: `请在 5 分钟内用要绑定的平台账号私聊机器人发送:${prefix}bind ${code}` };
|
|
433
|
-
},
|
|
434
|
-
async unlinkIdentity(ctx, args, caller) {
|
|
435
|
-
const { platform, userId } = args;
|
|
436
|
-
if (!platform || !userId)
|
|
437
|
-
throw new Error('platform, userId 必填');
|
|
438
|
-
const auth = ctx.getService('authority');
|
|
439
|
-
if (!auth)
|
|
440
|
-
throw new Error('Authority 服务不可用');
|
|
441
|
-
if (caller) {
|
|
442
|
-
const owning = auth.listUsers().find(u => u.links?.includes(`${platform}:${userId}`));
|
|
443
|
-
const isSelf = owning && owning.platform === caller.platform && owning.userId === caller.userId;
|
|
444
|
-
if (!isSelf && !auth.isOwner(caller.platform, caller.userId)) {
|
|
445
|
-
throw new Error('只有绑定所属账户本人或 owner 可以解绑');
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const ok = auth.unlinkIdentity(platform, userId);
|
|
449
|
-
auth.save();
|
|
450
|
-
return { ok, message: ok ? `${platform}:${userId} 已解绑` : '该身份没有绑定记录' };
|
|
259
|
+
return { message: `${platform}:${userId} 等级已更新为 ${level}` };
|
|
451
260
|
},
|
|
452
261
|
/** 删除用户记录 */
|
|
453
262
|
async deleteUser(ctx, args) {
|
|
@@ -459,11 +268,14 @@ export const actions = {
|
|
|
459
268
|
auth?.save();
|
|
460
269
|
return { message: `${platform}:${userId} 记录已删除` };
|
|
461
270
|
},
|
|
462
|
-
/** 更新 owner
|
|
463
|
-
async setOwners(ctx, args) {
|
|
271
|
+
/** 更新 owner 列表(仅 owner 可达:防非 owner 把自己加成 owner 提权) */
|
|
272
|
+
async setOwners(ctx, args, caller) {
|
|
464
273
|
const owners = args.owners;
|
|
465
274
|
if (!Array.isArray(owners))
|
|
466
275
|
throw new Error('owners 必须是数组');
|
|
276
|
+
const auth = ctx.getService('authority');
|
|
277
|
+
if (caller && !auth?.isOwner(caller.platform, caller.userId))
|
|
278
|
+
throw new Error('只有 owner 可管理 owner 列表');
|
|
467
279
|
const app = ctx.getService('app');
|
|
468
280
|
if (!app)
|
|
469
281
|
throw new Error('App 不可用');
|
|
@@ -497,42 +309,72 @@ export const actions = {
|
|
|
497
309
|
const ok = auth.revokeTemporaryGrant(id);
|
|
498
310
|
return { ok, message: ok ? '临时委托已撤销' : '不存在或已过期' };
|
|
499
311
|
},
|
|
500
|
-
/** owner
|
|
501
|
-
async
|
|
502
|
-
const { name,
|
|
312
|
+
/** owner 覆盖单条操作的最低等级(任意整数),无需改插件声明。key=能力键 `type:name`;传非整数则清除该条(回退默认派生)。 */
|
|
313
|
+
async setAuthorityOverride(ctx, args, caller) {
|
|
314
|
+
const { name, level } = args;
|
|
503
315
|
if (!name || typeof name !== 'string')
|
|
504
316
|
throw new Error('name 必填');
|
|
317
|
+
const auth = ctx.getService('authority');
|
|
318
|
+
if (caller && !auth?.isOwner(caller.platform, caller.userId))
|
|
319
|
+
throw new Error('只有 owner 可管理权限');
|
|
320
|
+
const app = ctx.getService('app');
|
|
321
|
+
if (!app)
|
|
322
|
+
throw new Error('App 不可用');
|
|
323
|
+
const overrides = { ...(ctx.config.get('authorityOverrides') ?? {}) };
|
|
324
|
+
if (typeof level === 'number' && Number.isInteger(level))
|
|
325
|
+
overrides[name] = level;
|
|
326
|
+
else
|
|
327
|
+
delete overrides[name];
|
|
328
|
+
ctx.config.set('authorityOverrides', overrides);
|
|
329
|
+
app.saveConfig();
|
|
330
|
+
return { message: `操作 ${name} 最低等级已更新` };
|
|
331
|
+
},
|
|
332
|
+
/** owner 覆盖单条操作的确认要求(session/always/off)。key=能力键 `type:name`;非法值清除该条。 */
|
|
333
|
+
async setConfirmOverride(ctx, args, caller) {
|
|
334
|
+
const { name, confirm } = args;
|
|
335
|
+
if (!name || typeof name !== 'string')
|
|
336
|
+
throw new Error('name 必填');
|
|
337
|
+
const auth = ctx.getService('authority');
|
|
338
|
+
if (caller && !auth?.isOwner(caller.platform, caller.userId))
|
|
339
|
+
throw new Error('只有 owner 可管理权限');
|
|
505
340
|
const app = ctx.getService('app');
|
|
506
341
|
if (!app)
|
|
507
342
|
throw new Error('App 不可用');
|
|
508
|
-
const overrides = { ...(ctx.config.get('
|
|
509
|
-
if (
|
|
510
|
-
overrides[name] =
|
|
343
|
+
const overrides = { ...(ctx.config.get('confirmOverrides') ?? {}) };
|
|
344
|
+
if (confirm === 'session' || confirm === 'always' || confirm === 'off')
|
|
345
|
+
overrides[name] = confirm;
|
|
511
346
|
else
|
|
512
347
|
delete overrides[name];
|
|
513
|
-
ctx.config.set('
|
|
348
|
+
ctx.config.set('confirmOverrides', overrides);
|
|
514
349
|
app.saveConfig();
|
|
515
|
-
return { message: `操作 ${name}
|
|
350
|
+
return { message: `操作 ${name} 确认要求已更新` };
|
|
516
351
|
},
|
|
517
|
-
/**
|
|
352
|
+
/** owner 切换 auto 确认模式。minutes: -1=一直 / 0=关 / N=N 分钟。仅 owner 可达。 */
|
|
353
|
+
async setAutoConfirm(ctx, args, caller) {
|
|
354
|
+
const auth = ctx.getService('authority');
|
|
355
|
+
if (caller && !auth?.isOwner(caller.platform, caller.userId))
|
|
356
|
+
throw new Error('只有 owner 可管理权限');
|
|
357
|
+
const app = ctx.getService('app');
|
|
358
|
+
if (!app)
|
|
359
|
+
throw new Error('App 不可用');
|
|
360
|
+
const m = args.minutes;
|
|
361
|
+
if (typeof m !== 'number' || !Number.isInteger(m))
|
|
362
|
+
throw new Error('minutes 必须是整数(-1 一直 / 0 关 / N 分钟)');
|
|
363
|
+
const until = m === -1 ? -1 : m <= 0 ? 0 : Date.now() + m * 60000;
|
|
364
|
+
ctx.config.set('autoConfirmUntil', until);
|
|
365
|
+
app.saveConfig();
|
|
366
|
+
return { message: until === -1 ? '自动确认:一直' : until === 0 ? '自动确认:关' : `自动确认:${m} 分钟`, until };
|
|
367
|
+
},
|
|
368
|
+
/** 更新禁用能力清单 */
|
|
518
369
|
async setConfig(ctx, args) {
|
|
519
370
|
const app = ctx.getService('app');
|
|
520
371
|
if (!app)
|
|
521
372
|
throw new Error('App 不可用');
|
|
522
|
-
const restricted = asStringList(args.restrictedCapabilities, 'restrictedCapabilities');
|
|
523
373
|
const denied = asStringList(args.deniedCapabilities, 'deniedCapabilities');
|
|
524
|
-
if (restricted)
|
|
525
|
-
ctx.config.set('restrictedCapabilities', restricted);
|
|
526
374
|
if (denied)
|
|
527
375
|
ctx.config.set('deniedCapabilities', denied);
|
|
528
376
|
app.saveConfig();
|
|
529
377
|
return { message: '权限配置已更新' };
|
|
530
378
|
},
|
|
531
379
|
};
|
|
532
|
-
// createBindCode / unlinkIdentity 对任何登录账户开放(绑码只能绑自己;解绑有 handler 内本人/owner 检查);
|
|
533
|
-
// 其余 action 不声明 → 默认 restricted(仅 owner / 被委托)。
|
|
534
|
-
export const actionsMeta = {
|
|
535
|
-
createBindCode: { visibility: 'public' },
|
|
536
|
-
unlinkIdentity: { visibility: 'public' },
|
|
537
|
-
};
|
|
538
380
|
//# sourceMappingURL=index.js.map
|