@actiondock/core 2.19.0 → 2.20.0
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/client/config.d.ts +17 -14
- package/dist/client/config.js +112 -14
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +2 -0
- package/dist/execution/service.js +8 -5
- package/dist/execution/types.d.ts +3 -3
- package/dist/host/host.d.ts +9 -1
- package/dist/host/host.js +183 -38
- package/dist/host/types.d.ts +3 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/input/config-override.d.ts +5 -0
- package/dist/input/config-override.js +23 -0
- package/dist/input/input-resolver.d.ts +24 -7
- package/dist/input/input-resolver.js +73 -5
- package/dist/invocation/bindings.d.ts +53 -0
- package/dist/invocation/bindings.js +248 -0
- package/dist/invocation/policy.d.ts +5 -1
- package/dist/invocation/policy.js +16 -9
- package/dist/invocation/remote.d.ts +40 -0
- package/dist/invocation/remote.js +489 -0
- package/dist/invocation/types.d.ts +2 -0
- package/dist/invocation/types.js +1 -0
- package/dist/package/index.d.ts +1 -1
- package/dist/package/runtime.js +31 -12
- package/dist/platform/types.d.ts +48 -1
- package/dist/project/config-declarations.d.ts +61 -0
- package/dist/project/config-declarations.js +297 -0
- package/dist/project/index.d.ts +5 -2
- package/dist/project/index.js +5 -2
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +4 -0
- package/dist/project/manifest.js +10 -0
- package/dist/project/types.d.ts +30 -0
- package/dist/runtime/context.d.ts +6 -3
- package/dist/runtime/context.js +60 -13
- package/dist/runtime/env.js +15 -1
- package/dist/runtime/standalone-commands.d.ts +2 -2
- package/dist/runtime/standalone-commands.js +113 -47
- package/dist/runtime/standalone-help.d.ts +1 -0
- package/dist/runtime/standalone-help.js +15 -9
- package/dist/runtime/standalone.js +2 -2
- package/dist/server/policy.d.ts +24 -107
- package/dist/server/policy.js +134 -291
- package/dist/server/routes/actions.js +10 -8
- package/dist/server/routes/common.d.ts +5 -20
- package/dist/server/routes/common.js +22 -58
- package/dist/server/routes/config.js +65 -7
- package/dist/server/routes/doctor.d.ts +1 -0
- package/dist/server/routes/doctor.js +4 -3
- package/dist/server/routes/info.js +100 -13
- package/dist/server/routes/playbooks.js +1 -3
- package/dist/server/routes/runs.js +96 -18
- package/dist/server/server.js +25 -53
- package/dist/server/types.d.ts +6 -6
- package/dist/service/factory.js +1 -0
- package/dist/service/remote.js +14 -29
- package/dist/service/types.d.ts +20 -5
- package/dist/service/types.js +8 -0
- package/dist/storage/mask.d.ts +33 -6
- package/dist/storage/mask.js +298 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/server/policy.js
CHANGED
|
@@ -1,11 +1,48 @@
|
|
|
1
1
|
import { snapshotRunOrigin } from "../runtime/run-origin.js";
|
|
2
2
|
import { parseActionRef } from "../catalog/resolve-action.js";
|
|
3
|
-
import { ActionDockError, ACTION_FORBIDDEN, PACKAGE_NOT_ALLOWED } from "../errors.js";
|
|
3
|
+
import { ActionDockError, ACTION_FORBIDDEN, INVALID_ARGUMENT, PACKAGE_NOT_ALLOWED } from "../errors.js";
|
|
4
|
+
import { assertValidPackageId } from "../utils/index.js";
|
|
4
5
|
import { safeEqual } from "./security.js";
|
|
5
6
|
export { extractBearerToken } from "./security.js";
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/** 校验两类白名单授权;省略或空数组不增加授权,包列表中的独立 * 显式授予全部权限。 */
|
|
8
|
+
export function assertServerPolicyValid(item, scope = "server options") {
|
|
9
|
+
for (const field of ["packageAllowlist", "actionAllowlist"]) {
|
|
10
|
+
const list = item[field];
|
|
11
|
+
if (list === undefined)
|
|
12
|
+
continue;
|
|
13
|
+
if (!Array.isArray(list)) {
|
|
14
|
+
throw new ActionDockError(INVALID_ARGUMENT, `Invalid ${scope}.${field}: expected an array`);
|
|
15
|
+
}
|
|
16
|
+
for (const entry of list) {
|
|
17
|
+
if (typeof entry !== "string" || !entry.trim()) {
|
|
18
|
+
throw new ActionDockError(INVALID_ARGUMENT, `Invalid ${scope}.${field}: entries must be non-empty strings`);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
if (field === "packageAllowlist") {
|
|
22
|
+
if (entry !== "*")
|
|
23
|
+
assertValidPackageId(entry);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
if (entry.includes("*") || entry.includes("?")) {
|
|
27
|
+
throw new Error("wildcards are not supported in action references");
|
|
28
|
+
}
|
|
29
|
+
const ref = parseActionRef(entry);
|
|
30
|
+
if (!ref.packageId) {
|
|
31
|
+
throw new Error("action references must be fully qualified packageId/actionId references");
|
|
32
|
+
}
|
|
33
|
+
assertValidPackageId(ref.packageId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new ActionDockError(INVALID_ARGUMENT, `Invalid ${scope}.${field}: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 是否显式授予全部包与动作;不替代独立的管理能力门禁。 */
|
|
43
|
+
export function isFullAccessGranted(policy) {
|
|
44
|
+
return policy?.packageAllowlist?.includes("*") === true;
|
|
45
|
+
}
|
|
9
46
|
export class ActionForbiddenError extends ActionDockError {
|
|
10
47
|
status = 403;
|
|
11
48
|
statusCode = 403;
|
|
@@ -15,9 +52,6 @@ export class ActionForbiddenError extends ActionDockError {
|
|
|
15
52
|
Object.setPrototypeOf(this, ActionForbiddenError.prototype);
|
|
16
53
|
}
|
|
17
54
|
}
|
|
18
|
-
/**
|
|
19
|
-
* 包未列入白名单拒绝访问异常。
|
|
20
|
-
*/
|
|
21
55
|
export class PackageNotAllowedError extends ActionDockError {
|
|
22
56
|
status = 403;
|
|
23
57
|
statusCode = 403;
|
|
@@ -27,41 +61,43 @@ export class PackageNotAllowedError extends ActionDockError {
|
|
|
27
61
|
Object.setPrototypeOf(this, PackageNotAllowedError.prototype);
|
|
28
62
|
}
|
|
29
63
|
}
|
|
30
|
-
/**
|
|
31
|
-
* 根据客户端携带的 Token 智能匹配已注册的视图。
|
|
32
|
-
* 使用 safeEqual 进行恒定时间比较以防范时序攻击。
|
|
33
|
-
*
|
|
34
|
-
* @param token 客户端提供的 Bearer Token
|
|
35
|
-
* @param views 可遍历的视图集合
|
|
36
|
-
* @returns 匹配成功的视图对象,未命中返回 undefined
|
|
37
|
-
*/
|
|
64
|
+
/** 遍历全部视图并以常数时间比较令牌,避免提前退出泄露匹配位置。 */
|
|
38
65
|
export function matchViewByToken(token, views) {
|
|
39
66
|
const trimmed = token.trim();
|
|
40
|
-
if (!trimmed)
|
|
67
|
+
if (!trimmed)
|
|
41
68
|
return undefined;
|
|
42
|
-
|
|
43
|
-
let matched = undefined;
|
|
69
|
+
let matched;
|
|
44
70
|
for (const view of views) {
|
|
45
71
|
const candidateToken = view.policy.token?.trim();
|
|
46
72
|
if (candidateToken) {
|
|
47
73
|
const isMatch = safeEqual(trimmed, candidateToken);
|
|
48
|
-
if (isMatch && !matched)
|
|
74
|
+
if (isMatch && !matched)
|
|
49
75
|
matched = view;
|
|
50
|
-
}
|
|
51
76
|
}
|
|
52
77
|
}
|
|
53
78
|
return matched;
|
|
54
79
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
function normalizeView(item, name) {
|
|
81
|
+
assertServerPolicyValid(item, `view '${name}'`);
|
|
82
|
+
return {
|
|
83
|
+
name,
|
|
84
|
+
policy: {
|
|
85
|
+
viewName: name,
|
|
86
|
+
token: item.token,
|
|
87
|
+
packageAllowlist: item.packageAllowlist?.slice(),
|
|
88
|
+
actionAllowlist: item.actionAllowlist?.map((entry) => {
|
|
89
|
+
const ref = parseActionRef(entry);
|
|
90
|
+
return `${ref.packageId}/${ref.actionId}`;
|
|
91
|
+
}),
|
|
92
|
+
enableManagement: item.enableManagement,
|
|
93
|
+
},
|
|
94
|
+
enableMcp: item.enableMcp !== false,
|
|
95
|
+
rawOptions: item,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** 全局选项仅配置默认视图;命名视图独立授权,缺少授权的视图默认拒绝全部。 */
|
|
62
99
|
export function normalizeServerViews(options) {
|
|
63
|
-
|
|
64
|
-
// 1. 初始化基于全局配置的默认视图
|
|
100
|
+
assertServerPolicyValid(options);
|
|
65
101
|
const defaultRawOptions = {
|
|
66
102
|
name: "default",
|
|
67
103
|
token: options.token,
|
|
@@ -72,307 +108,114 @@ export function normalizeServerViews(options) {
|
|
|
72
108
|
mcpHandler: options.mcpHandler,
|
|
73
109
|
};
|
|
74
110
|
const customViews = new Map();
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (name === "default") {
|
|
87
|
-
Object.assign(defaultRawOptions, item);
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
customViews.set(name, {
|
|
91
|
-
name,
|
|
92
|
-
policy: {
|
|
93
|
-
viewName: name,
|
|
94
|
-
token: item.token,
|
|
95
|
-
packageAllowlist: item.packageAllowlist,
|
|
96
|
-
actionAllowlist: item.actionAllowlist,
|
|
97
|
-
enableManagement: item.enableManagement,
|
|
98
|
-
},
|
|
99
|
-
enableMcp: item.enableMcp !== false,
|
|
100
|
-
rawOptions: item,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
111
|
+
const entries = Array.isArray(options.views)
|
|
112
|
+
? options.views.map((item, index) => [`view_${index}`, item])
|
|
113
|
+
: Object.entries(options.views || {});
|
|
114
|
+
for (const [key, item] of entries) {
|
|
115
|
+
if (!item || typeof item !== "object")
|
|
116
|
+
continue;
|
|
117
|
+
const name = item.name?.trim() || key.trim();
|
|
118
|
+
if (!name || name === "." || name === "..")
|
|
119
|
+
continue;
|
|
120
|
+
if (name === "default") {
|
|
121
|
+
Object.assign(defaultRawOptions, item);
|
|
104
122
|
}
|
|
105
|
-
else
|
|
106
|
-
|
|
107
|
-
if (!item || typeof item !== "object")
|
|
108
|
-
continue;
|
|
109
|
-
const name = item.name?.trim() || key.trim();
|
|
110
|
-
if (!name || name === "." || name === "..") {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (name === "default") {
|
|
114
|
-
Object.assign(defaultRawOptions, item);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
customViews.set(name, {
|
|
118
|
-
name,
|
|
119
|
-
policy: {
|
|
120
|
-
viewName: name,
|
|
121
|
-
token: item.token,
|
|
122
|
-
packageAllowlist: item.packageAllowlist,
|
|
123
|
-
actionAllowlist: item.actionAllowlist,
|
|
124
|
-
enableManagement: item.enableManagement,
|
|
125
|
-
},
|
|
126
|
-
enableMcp: item.enableMcp !== false,
|
|
127
|
-
rawOptions: item,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
123
|
+
else {
|
|
124
|
+
customViews.set(name, normalizeView(item, name));
|
|
131
125
|
}
|
|
132
126
|
}
|
|
133
|
-
const defaultView =
|
|
134
|
-
|
|
135
|
-
policy: {
|
|
136
|
-
viewName: "default",
|
|
137
|
-
token: defaultRawOptions.token,
|
|
138
|
-
packageAllowlist: defaultRawOptions.packageAllowlist,
|
|
139
|
-
actionAllowlist: defaultRawOptions.actionAllowlist,
|
|
140
|
-
enableManagement: defaultRawOptions.enableManagement,
|
|
141
|
-
},
|
|
142
|
-
enableMcp: defaultRawOptions.enableMcp !== false,
|
|
143
|
-
rawOptions: defaultRawOptions,
|
|
144
|
-
};
|
|
145
|
-
// 默认视图置于首位,随后装载自定义视图
|
|
146
|
-
views.set("default", defaultView);
|
|
147
|
-
for (const [k, v] of customViews) {
|
|
148
|
-
views.set(k, v);
|
|
149
|
-
}
|
|
127
|
+
const defaultView = normalizeView(defaultRawOptions, "default");
|
|
128
|
+
const views = new Map([["default", defaultView], ...customViews]);
|
|
150
129
|
return { defaultView, views };
|
|
151
130
|
}
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
* - 白名单项若不含 '/'(如 act),则只需 actionId 匹配(不限包)。
|
|
157
|
-
*
|
|
158
|
-
* @param action 目标动作对象或引用字符串
|
|
159
|
-
* @param allowlist 允许动作白名单列表
|
|
160
|
-
* @returns 是否允许访问
|
|
161
|
-
*/
|
|
162
|
-
export function isActionAllowed(action, allowlist) {
|
|
163
|
-
if (!allowlist || !Array.isArray(allowlist) || allowlist.length === 0) {
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
if (!action) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
let target;
|
|
131
|
+
/** 目标引用格式错误或包身份不一致时返回未命中,不能猜测其所属包。 */
|
|
132
|
+
function parsePolicyAction(action) {
|
|
133
|
+
if (!action)
|
|
134
|
+
return undefined;
|
|
170
135
|
try {
|
|
171
|
-
if (typeof action === "string")
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
target = parseActionRef(rawId);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
target = parseActionRef({
|
|
184
|
-
packageId: action.packageId,
|
|
185
|
-
actionId: rawId,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
target = parseActionRef(rawId);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
136
|
+
if (typeof action === "string")
|
|
137
|
+
return parseActionRef(action);
|
|
138
|
+
const rawId = action.actionId || action.id;
|
|
139
|
+
if (!rawId)
|
|
140
|
+
return undefined;
|
|
141
|
+
const ref = parseActionRef(rawId);
|
|
142
|
+
if (action.packageId && ref.packageId && action.packageId !== ref.packageId)
|
|
143
|
+
return undefined;
|
|
144
|
+
return { packageId: action.packageId || ref.packageId, actionId: ref.actionId };
|
|
193
145
|
}
|
|
194
146
|
catch {
|
|
195
|
-
return
|
|
196
|
-
}
|
|
197
|
-
for (const item of allowlist) {
|
|
198
|
-
if (typeof item !== "string")
|
|
199
|
-
continue;
|
|
200
|
-
const trimmed = item.trim();
|
|
201
|
-
if (!trimmed)
|
|
202
|
-
continue;
|
|
203
|
-
try {
|
|
204
|
-
const rule = parseActionRef(trimmed);
|
|
205
|
-
if (rule.packageId) {
|
|
206
|
-
if (rule.packageId === target.packageId && rule.actionId === target.actionId) {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
if (rule.actionId === target.actionId) {
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
147
|
+
return undefined;
|
|
219
148
|
}
|
|
220
|
-
return false;
|
|
221
149
|
}
|
|
222
|
-
/**
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
150
|
+
/** 动作列表仅按全限定引用匹配;省略或空列表不会授予任何动作。 */
|
|
151
|
+
export function isActionAllowed(action, allowlist) {
|
|
152
|
+
const target = parsePolicyAction(action);
|
|
153
|
+
if (!target?.packageId)
|
|
154
|
+
return false;
|
|
155
|
+
return (allowlist || []).some((entry) => {
|
|
156
|
+
const rule = parsePolicyAction(entry);
|
|
157
|
+
return Boolean(rule?.packageId && rule.packageId === target.packageId && rule.actionId === target.actionId);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** 整包授权与全限定单动作授权取并集;仅缺少策略对象的内部调用不执行过滤。 */
|
|
230
161
|
export function isActionAllowedByPolicy(action, policy) {
|
|
231
|
-
if (!policy)
|
|
162
|
+
if (!policy)
|
|
232
163
|
return true;
|
|
233
|
-
|
|
234
|
-
if (!
|
|
164
|
+
const target = parsePolicyAction(action);
|
|
165
|
+
if (!target)
|
|
235
166
|
return false;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
if (typeof action === "string") {
|
|
241
|
-
const parsed = parseActionRef(action);
|
|
242
|
-
pkgId = parsed.packageId;
|
|
243
|
-
actId = parsed.actionId;
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
pkgId = action.packageId;
|
|
247
|
-
const rawId = action.actionId || action.id;
|
|
248
|
-
if (!rawId)
|
|
249
|
-
return false;
|
|
250
|
-
if (rawId.includes("/")) {
|
|
251
|
-
const parsed = parseActionRef(rawId);
|
|
252
|
-
pkgId = parsed.packageId || pkgId;
|
|
253
|
-
actId = parsed.actionId;
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
actId = rawId;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
167
|
+
if (isFullAccessGranted(policy))
|
|
168
|
+
return true;
|
|
169
|
+
if (!target.packageId)
|
|
261
170
|
return false;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (policy.packageAllowlist && policy.packageAllowlist.length > 0) {
|
|
265
|
-
if (pkgId) {
|
|
266
|
-
if (!policy.packageAllowlist.includes(pkgId)) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
// 未指明所属包时,仅当其动作标识明确列入 actionAllowlist 时才允许放行
|
|
272
|
-
if (!policy.actionAllowlist ||
|
|
273
|
-
policy.actionAllowlist.length === 0 ||
|
|
274
|
-
!isActionAllowed({ actionId: actId }, policy.actionAllowlist)) {
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
// 2. 若配置了 actionAllowlist,校验动作白名单
|
|
280
|
-
if (policy.actionAllowlist && policy.actionAllowlist.length > 0) {
|
|
281
|
-
if (!isActionAllowed({ packageId: pkgId, actionId: actId }, policy.actionAllowlist)) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return true;
|
|
171
|
+
return Boolean(policy.packageAllowlist?.includes(target.packageId) ||
|
|
172
|
+
isActionAllowed(target, policy.actionAllowlist));
|
|
286
173
|
}
|
|
287
|
-
/**
|
|
288
|
-
* 根据生效策略统一判定目标包标识是否允许访问。
|
|
289
|
-
*
|
|
290
|
-
* @param packageId 目标包 ID
|
|
291
|
-
* @param policy 当前生效的服务端策略
|
|
292
|
-
* @returns 是否允许访问
|
|
293
|
-
*/
|
|
174
|
+
/** 整包能力授权,不从某个动作的授权推导配置、状态、规程或诊断权限。 */
|
|
294
175
|
export function isPackageAllowedByPolicy(packageId, policy) {
|
|
295
|
-
if (!policy
|
|
176
|
+
if (!policy || isFullAccessGranted(policy))
|
|
296
177
|
return true;
|
|
297
|
-
|
|
298
|
-
|
|
178
|
+
return Boolean(packageId && policy.packageAllowlist?.includes(packageId));
|
|
179
|
+
}
|
|
180
|
+
/** 单动作授权的包可发现必要身份信息,但不因此获得整包能力。 */
|
|
181
|
+
export function isPackageVisibleByPolicy(packageId, policy) {
|
|
182
|
+
if (isPackageAllowedByPolicy(packageId, policy))
|
|
183
|
+
return true;
|
|
184
|
+
if (!packageId)
|
|
299
185
|
return false;
|
|
300
|
-
|
|
301
|
-
|
|
186
|
+
return (policy?.actionAllowlist || []).some((entry) => parsePolicyAction(entry)?.packageId === packageId);
|
|
187
|
+
}
|
|
188
|
+
export function requiresMinimalPackageProjection(packageId, policy) {
|
|
189
|
+
return isPackageVisibleByPolicy(packageId, policy) && !isPackageAllowedByPolicy(packageId, policy);
|
|
302
190
|
}
|
|
303
|
-
/**
|
|
304
|
-
* 根据生效策略统一判定配置与状态管理路由是否开启。
|
|
305
|
-
*
|
|
306
|
-
* @param policy 当前生效的服务端策略
|
|
307
|
-
* @returns 是否允许管理操作
|
|
308
|
-
*/
|
|
309
191
|
export function isManagementAllowedByPolicy(policy) {
|
|
310
192
|
return policy?.enableManagement === true;
|
|
311
193
|
}
|
|
312
|
-
/**
|
|
313
|
-
* 根据生效策略对动作列表进行统一过滤。
|
|
314
|
-
*
|
|
315
|
-
* @param actions 待过滤的动作列表
|
|
316
|
-
* @param policy 当前生效的服务端策略
|
|
317
|
-
* @returns 过滤后允许访问的动作列表
|
|
318
|
-
*/
|
|
319
194
|
export function filterActionsByPolicy(actions, policy) {
|
|
320
|
-
if (!policy)
|
|
321
|
-
return actions;
|
|
322
|
-
}
|
|
323
|
-
const hasPkg = policy.packageAllowlist && policy.packageAllowlist.length > 0;
|
|
324
|
-
const hasAct = policy.actionAllowlist && policy.actionAllowlist.length > 0;
|
|
325
|
-
if (!hasPkg && !hasAct) {
|
|
195
|
+
if (!policy)
|
|
326
196
|
return actions;
|
|
327
|
-
|
|
328
|
-
return actions.filter((a) => isActionAllowedByPolicy({ packageId: a.packageId, actionId: a.actionId || a.id }, policy));
|
|
197
|
+
return actions.filter((action) => isActionAllowedByPolicy(action, policy));
|
|
329
198
|
}
|
|
330
|
-
/**
|
|
331
|
-
* 根据生效策略对包列表进行统一过滤。
|
|
332
|
-
*
|
|
333
|
-
* @param packages 待过滤的包列表
|
|
334
|
-
* @param policy 当前生效的服务端策略
|
|
335
|
-
* @returns 过滤后允许访问的包列表
|
|
336
|
-
*/
|
|
337
199
|
export function filterPackagesByPolicy(packages, policy) {
|
|
338
|
-
if (!policy
|
|
200
|
+
if (!policy)
|
|
339
201
|
return packages;
|
|
340
|
-
|
|
341
|
-
return packages.filter((p) => p.id && policy.packageAllowlist.includes(p.id));
|
|
202
|
+
return packages.filter((pkg) => isPackageVisibleByPolicy(pkg.id, policy));
|
|
342
203
|
}
|
|
343
|
-
/**
|
|
344
|
-
* 校验目标动作是否属于允许白名单,不符合则抛出 403 异常。
|
|
345
|
-
*
|
|
346
|
-
* @param action 目标动作对象或引用
|
|
347
|
-
* @param policy 当前生效的服务端策略
|
|
348
|
-
*/
|
|
349
204
|
export function assertActionAllowedByPolicy(action, policy) {
|
|
350
205
|
if (!isActionAllowedByPolicy(action, policy)) {
|
|
351
|
-
const
|
|
206
|
+
const ref = typeof action === "string"
|
|
352
207
|
? action
|
|
353
208
|
: action.packageId
|
|
354
209
|
? `${action.packageId}/${action.actionId || action.id}`
|
|
355
210
|
: action.actionId || action.id || "unknown";
|
|
356
|
-
throw new ActionForbiddenError(
|
|
211
|
+
throw new ActionForbiddenError(ref);
|
|
357
212
|
}
|
|
358
213
|
}
|
|
359
|
-
/**
|
|
360
|
-
* 校验目标包是否属于允许白名单,不符合则抛出 403 异常。
|
|
361
|
-
*
|
|
362
|
-
* @param packageId 目标包 ID
|
|
363
|
-
* @param policy 当前生效的服务端策略
|
|
364
|
-
*/
|
|
365
214
|
export function assertPackageAllowedByPolicy(packageId, policy) {
|
|
366
|
-
if (!isPackageAllowedByPolicy(packageId, policy))
|
|
215
|
+
if (!isPackageAllowedByPolicy(packageId, policy))
|
|
367
216
|
throw new PackageNotAllowedError(packageId);
|
|
368
|
-
}
|
|
369
217
|
}
|
|
370
|
-
/**
|
|
371
|
-
* 构造受信任的服务端执行来源快照。
|
|
372
|
-
*
|
|
373
|
-
* 仅挑选 channel、viewName(默认 "default")与 authenticated(视图配置非空有效令牌)。
|
|
374
|
-
* 严禁暴露令牌原文、片段或摘要。
|
|
375
|
-
*/
|
|
218
|
+
/** 只记录受信任的执行来源,不能包含令牌原文、片段或摘要。 */
|
|
376
219
|
export function createServerRunOrigin(policy, channel) {
|
|
377
220
|
return snapshotRunOrigin({
|
|
378
221
|
channel,
|
|
@@ -3,7 +3,7 @@ import { filterByIntent } from "../../utils/intent.js";
|
|
|
3
3
|
import { ACTION_EXECUTION_ERROR, ACTION_FORBIDDEN, ACTION_NOT_FOUND, ACTION_START_FAILED, ACTION_TIMEOUT, ACTIONS_LIST_ERROR, IDEMPOTENCY_CONFLICT, INPUT_VALIDATION_FAILED, INVALID_JSON, PACKAGE_FORBIDDEN, PACKAGE_NOT_FOUND, REQUEST_TOO_LARGE, } from "../../errors.js";
|
|
4
4
|
import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "../body.js";
|
|
5
5
|
import { createServerRunOrigin } from "../policy.js";
|
|
6
|
-
import { filterActionsByPolicy, getSubPath, isActionAllowedByPolicy,
|
|
6
|
+
import { filterActionsByPolicy, getSubPath, isActionAllowedByPolicy, isPackageVisibleByPolicy, jsonResponse, } from "./common.js";
|
|
7
7
|
/**
|
|
8
8
|
* 处理 Action 相关的 HTTP 路由(列表、规范查询、同步执行与异步启动)。
|
|
9
9
|
*/
|
|
@@ -44,7 +44,9 @@ export async function handleActionsRoutes(ctx) {
|
|
|
44
44
|
if (pkgActionShowMatch && req.method === "GET") {
|
|
45
45
|
const packageId = decodeURIComponent(pkgActionShowMatch[1]);
|
|
46
46
|
const actionId = decodeURIComponent(pkgActionShowMatch[2]);
|
|
47
|
-
|
|
47
|
+
// 先按包可见性拦截完全无关包,再按实际完整引用判定动作授权,
|
|
48
|
+
// 保证仅持有单动作授权(如 B/read)时 B/read 可见可用
|
|
49
|
+
if (!isPackageVisibleByPolicy(packageId, policy)) {
|
|
48
50
|
return jsonResponse({
|
|
49
51
|
ok: false,
|
|
50
52
|
error: {
|
|
@@ -84,7 +86,7 @@ export async function handleActionsRoutes(ctx) {
|
|
|
84
86
|
try {
|
|
85
87
|
const parsed = parseActionRef(actionId);
|
|
86
88
|
if (parsed.packageId) {
|
|
87
|
-
if (!
|
|
89
|
+
if (!isPackageVisibleByPolicy(parsed.packageId, policy)) {
|
|
88
90
|
return jsonResponse({
|
|
89
91
|
ok: false,
|
|
90
92
|
error: {
|
|
@@ -107,7 +109,7 @@ export async function handleActionsRoutes(ctx) {
|
|
|
107
109
|
catch { }
|
|
108
110
|
try {
|
|
109
111
|
const spec = await service.discovery.describeAction(actionId);
|
|
110
|
-
if (!
|
|
112
|
+
if (!isPackageVisibleByPolicy(spec.packageId, policy)) {
|
|
111
113
|
return jsonResponse({
|
|
112
114
|
ok: false,
|
|
113
115
|
error: {
|
|
@@ -166,8 +168,8 @@ export async function handleActionsRoutes(ctx) {
|
|
|
166
168
|
}
|
|
167
169
|
catch { }
|
|
168
170
|
}
|
|
169
|
-
//
|
|
170
|
-
if (
|
|
171
|
+
// 查询实际包与动作身份,短名请求不能绕过全限定授权判断。
|
|
172
|
+
if (!pkgId || !actId) {
|
|
171
173
|
try {
|
|
172
174
|
const spec = await service.discovery.describeAction(actionRef);
|
|
173
175
|
pkgId = pkgId || spec?.packageId;
|
|
@@ -185,8 +187,8 @@ export async function handleActionsRoutes(ctx) {
|
|
|
185
187
|
actId = actionRef;
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
|
-
//
|
|
189
|
-
if (!
|
|
190
|
+
// 单动作授权只要求其所属包可见,不能用整包能力门禁将其拦截。
|
|
191
|
+
if (!isPackageVisibleByPolicy(pkgId, policy)) {
|
|
190
192
|
return jsonResponse({
|
|
191
193
|
ok: false,
|
|
192
194
|
runId: "",
|
|
@@ -1,32 +1,17 @@
|
|
|
1
1
|
import type { PackageInfo } from "../../package/types.js";
|
|
2
2
|
import type { EffectiveServerPolicy, RouteContext, ServerOptions } from "../types.js";
|
|
3
|
-
import { ActionForbiddenError, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, isActionAllowed, isActionAllowedByPolicy, isManagementAllowedByPolicy, isPackageAllowedByPolicy, PackageNotAllowedError } from "../policy.js";
|
|
3
|
+
import { ActionForbiddenError, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, isActionAllowed, isActionAllowedByPolicy, isFullAccessGranted, isManagementAllowedByPolicy, isPackageAllowedByPolicy, isPackageVisibleByPolicy, PackageNotAllowedError, requiresMinimalPackageProjection } from "../policy.js";
|
|
4
4
|
export type { RouteContext };
|
|
5
|
-
export { ActionForbiddenError, PackageNotAllowedError, isActionAllowed, isActionAllowedByPolicy, isPackageAllowedByPolicy, isManagementAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, };
|
|
6
|
-
/**
|
|
7
|
-
* 构造带 CORS 头的标准 JSON HTTP 响应。
|
|
8
|
-
*/
|
|
5
|
+
export { ActionForbiddenError, PackageNotAllowedError, isActionAllowed, isActionAllowedByPolicy, isFullAccessGranted, isPackageAllowedByPolicy, isPackageVisibleByPolicy, isManagementAllowedByPolicy, requiresMinimalPackageProjection, filterActionsByPolicy, filterPackagesByPolicy, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, };
|
|
9
6
|
export declare function jsonResponse(data: unknown, status?: number, corsHeaders?: Record<string, string>): Response;
|
|
10
|
-
/**
|
|
11
|
-
* 剥离标准版本前缀 (/api/v2/),获取相对路由子路径。
|
|
12
|
-
*/
|
|
13
7
|
export declare function getSubPath(pathname: string): string;
|
|
14
|
-
/**
|
|
15
|
-
* 校验目标 action 是否属于允许动作白名单。
|
|
16
|
-
* 当 options 或 policy 中的 actionAllowlist 存在且目标 action 不在白名单中时抛出 403 ACTION_FORBIDDEN 异常。
|
|
17
|
-
*/
|
|
18
8
|
export declare function assertActionAllowed(action: {
|
|
19
9
|
actionId?: string;
|
|
20
10
|
id?: string;
|
|
21
11
|
packageId?: string;
|
|
22
12
|
} | string, options?: ServerOptions | EffectiveServerPolicy): void;
|
|
23
|
-
/**
|
|
24
|
-
* 校验目标 packageId 是否属于允许包白名单。
|
|
25
|
-
* 当 options 或 policy 中的 packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
|
|
26
|
-
*/
|
|
27
13
|
export declare function assertPackageAllowed(packageId: string, options?: ServerOptions | EffectiveServerPolicy): void;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*/
|
|
14
|
+
/** 包发现可见性与整包能力分开判断,不能因单动作授权开放包级管理。 */
|
|
15
|
+
export declare function assertPackageVisible(packageId: string, options?: ServerOptions | EffectiveServerPolicy): void;
|
|
16
|
+
/** 管理接口只能选取整包授权目标;全局管理必须显式拥有全部授权。 */
|
|
32
17
|
export declare function resolveTargetPackageId(packages: PackageInfo[], requestedPackageId?: string, options?: ServerOptions | EffectiveServerPolicy): string;
|