@plusplus7/clawclamp 0.1.0 → 0.1.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/assets/app.js +110 -44
- package/package.json +1 -1
- package/src/grants.ts +35 -6
- package/src/guard.ts +2 -2
- package/src/http.ts +11 -3
- package/src/policy-store.ts +37 -8
- package/src/policy.ts +6 -2
package/assets/app.js
CHANGED
|
@@ -44,6 +44,31 @@ async function fetchJson(path, opts = {}) {
|
|
|
44
44
|
return payload;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function showAlert(message) {
|
|
48
|
+
window.alert(message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertOperationOk(payload, fallbackMessage) {
|
|
52
|
+
if (payload && Object.prototype.hasOwnProperty.call(payload, "ok") && payload.ok === false) {
|
|
53
|
+
throw new Error(fallbackMessage);
|
|
54
|
+
}
|
|
55
|
+
return payload;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runAction(action, successMessage, failurePrefix) {
|
|
59
|
+
try {
|
|
60
|
+
const result = await action();
|
|
61
|
+
if (successMessage) {
|
|
62
|
+
showAlert(successMessage);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
showAlert(`${failurePrefix}${message}`);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
function formatTime(iso) {
|
|
48
73
|
if (!iso) return "-";
|
|
49
74
|
const date = new Date(iso);
|
|
@@ -79,8 +104,15 @@ function renderGrants(grants) {
|
|
|
79
104
|
<button class="btn ghost" data-id="${grant.id}">撤销</button>
|
|
80
105
|
`;
|
|
81
106
|
item.querySelector("button").addEventListener("click", async () => {
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
try {
|
|
108
|
+
await runAction(async () => {
|
|
109
|
+
const result = await fetchJson(`${API_BASE}/grants/${encodeURIComponent(grant.id)}`, {
|
|
110
|
+
method: "DELETE",
|
|
111
|
+
});
|
|
112
|
+
assertOperationOk(result, "撤销授权失败");
|
|
113
|
+
await refreshAll();
|
|
114
|
+
}, "已撤销短期授权。", "撤销短期授权失败:");
|
|
115
|
+
} catch {}
|
|
84
116
|
});
|
|
85
117
|
grantsEl.appendChild(item);
|
|
86
118
|
});
|
|
@@ -154,15 +186,23 @@ function renderAudit(entries) {
|
|
|
154
186
|
const allowBtn = row.querySelector("button[data-action=\"allow\"]");
|
|
155
187
|
if (allowBtn) {
|
|
156
188
|
allowBtn.addEventListener("click", async () => {
|
|
157
|
-
|
|
158
|
-
|
|
189
|
+
try {
|
|
190
|
+
await runAction(async () => {
|
|
191
|
+
await applyPolicyChange("permit", entry.toolName);
|
|
192
|
+
await refreshAll();
|
|
193
|
+
}, "已添加允许策略。", "添加允许策略失败:");
|
|
194
|
+
} catch {}
|
|
159
195
|
});
|
|
160
196
|
}
|
|
161
197
|
const denyBtn = row.querySelector("button[data-action=\"deny\"]");
|
|
162
198
|
if (denyBtn) {
|
|
163
199
|
denyBtn.addEventListener("click", async () => {
|
|
164
|
-
|
|
165
|
-
|
|
200
|
+
try {
|
|
201
|
+
await runAction(async () => {
|
|
202
|
+
await applyPolicyChange("forbid", entry.toolName);
|
|
203
|
+
await refreshAll();
|
|
204
|
+
}, "已添加拒绝策略。", "添加拒绝策略失败:");
|
|
205
|
+
} catch {}
|
|
166
206
|
});
|
|
167
207
|
}
|
|
168
208
|
auditEl.appendChild(row);
|
|
@@ -226,19 +266,27 @@ refreshBtn.addEventListener("click", () => {
|
|
|
226
266
|
});
|
|
227
267
|
|
|
228
268
|
enforceBtn.addEventListener("click", async () => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
269
|
+
try {
|
|
270
|
+
await runAction(async () => {
|
|
271
|
+
await fetchJson(`${API_BASE}/mode`, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: JSON.stringify({ mode: "enforce" }),
|
|
274
|
+
});
|
|
275
|
+
await refreshAll();
|
|
276
|
+
}, "已切换到强制模式。", "切换模式失败:");
|
|
277
|
+
} catch {}
|
|
234
278
|
});
|
|
235
279
|
|
|
236
280
|
grayBtn.addEventListener("click", async () => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
try {
|
|
282
|
+
await runAction(async () => {
|
|
283
|
+
await fetchJson(`${API_BASE}/mode`, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
body: JSON.stringify({ mode: "gray" }),
|
|
286
|
+
});
|
|
287
|
+
await refreshAll();
|
|
288
|
+
}, "已切换到灰度模式。", "切换模式失败:");
|
|
289
|
+
} catch {}
|
|
242
290
|
});
|
|
243
291
|
|
|
244
292
|
grantForm.addEventListener("submit", async (event) => {
|
|
@@ -246,14 +294,18 @@ grantForm.addEventListener("submit", async (event) => {
|
|
|
246
294
|
const toolName = grantToolInput.value.trim();
|
|
247
295
|
const ttlSeconds = grantTtlInput.value ? Number(grantTtlInput.value) : undefined;
|
|
248
296
|
const note = grantNoteInput.value.trim();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
297
|
+
try {
|
|
298
|
+
await runAction(async () => {
|
|
299
|
+
await fetchJson(`${API_BASE}/grants`, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
body: JSON.stringify({ toolName, ttlSeconds, note: note || undefined }),
|
|
302
|
+
});
|
|
303
|
+
grantToolInput.value = "";
|
|
304
|
+
grantTtlInput.value = "";
|
|
305
|
+
grantNoteInput.value = "";
|
|
306
|
+
await refreshAll();
|
|
307
|
+
}, "已创建短期授权。", "创建短期授权失败:");
|
|
308
|
+
} catch {}
|
|
257
309
|
});
|
|
258
310
|
|
|
259
311
|
auditPageSizeEl.addEventListener("change", async () => {
|
|
@@ -294,9 +346,10 @@ function policyBody(effect, toolName) {
|
|
|
294
346
|
async function removePoliciesByPrefix(prefix) {
|
|
295
347
|
const targets = policies.filter((policy) => policy.id.startsWith(prefix));
|
|
296
348
|
for (const policy of targets) {
|
|
297
|
-
await fetchJson(`${API_BASE}/policies/${encodeURIComponent(policy.id)}`, {
|
|
349
|
+
const result = await fetchJson(`${API_BASE}/policies/${encodeURIComponent(policy.id)}`, {
|
|
298
350
|
method: "DELETE",
|
|
299
351
|
});
|
|
352
|
+
assertOperationOk(result, `删除策略失败: ${policy.id}`);
|
|
300
353
|
}
|
|
301
354
|
}
|
|
302
355
|
|
|
@@ -319,12 +372,16 @@ policyCreateBtn.addEventListener("click", async () => {
|
|
|
319
372
|
return;
|
|
320
373
|
}
|
|
321
374
|
const body = { content, ...(id ? { id } : {}) };
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
375
|
+
try {
|
|
376
|
+
await runAction(async () => {
|
|
377
|
+
await fetchJson(`${API_BASE}/policies`, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
body: JSON.stringify(body),
|
|
380
|
+
});
|
|
381
|
+
policyStatusEl.textContent = "已新增策略。";
|
|
382
|
+
await refreshAll();
|
|
383
|
+
}, "已新增策略。", "新增策略失败:");
|
|
384
|
+
} catch {}
|
|
328
385
|
});
|
|
329
386
|
|
|
330
387
|
policyUpdateBtn.addEventListener("click", async () => {
|
|
@@ -338,12 +395,16 @@ policyUpdateBtn.addEventListener("click", async () => {
|
|
|
338
395
|
policyStatusEl.textContent = "请输入 Policy 内容。";
|
|
339
396
|
return;
|
|
340
397
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
398
|
+
try {
|
|
399
|
+
await runAction(async () => {
|
|
400
|
+
await fetchJson(`${API_BASE}/policies/${encodeURIComponent(id)}`, {
|
|
401
|
+
method: "PUT",
|
|
402
|
+
body: JSON.stringify({ content }),
|
|
403
|
+
});
|
|
404
|
+
policyStatusEl.textContent = "已保存策略。";
|
|
405
|
+
await refreshAll();
|
|
406
|
+
}, "已保存策略。", "保存策略失败:");
|
|
407
|
+
} catch {}
|
|
347
408
|
});
|
|
348
409
|
|
|
349
410
|
policyDeleteBtn.addEventListener("click", async () => {
|
|
@@ -352,11 +413,16 @@ policyDeleteBtn.addEventListener("click", async () => {
|
|
|
352
413
|
policyStatusEl.textContent = "请选择或填写 Policy ID。";
|
|
353
414
|
return;
|
|
354
415
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
416
|
+
try {
|
|
417
|
+
await runAction(async () => {
|
|
418
|
+
const result = await fetchJson(`${API_BASE}/policies/${encodeURIComponent(id)}`, {
|
|
419
|
+
method: "DELETE",
|
|
420
|
+
});
|
|
421
|
+
assertOperationOk(result, "删除策略失败");
|
|
422
|
+
policyStatusEl.textContent = "已删除策略。";
|
|
423
|
+
policyIdInput.value = "";
|
|
424
|
+
policyContentInput.value = "";
|
|
425
|
+
await refreshAll();
|
|
426
|
+
}, "已删除策略。", "删除策略失败:");
|
|
427
|
+
} catch {}
|
|
362
428
|
});
|
package/package.json
CHANGED
package/src/grants.ts
CHANGED
|
@@ -42,12 +42,37 @@ function decodeBase64(value: string): string {
|
|
|
42
42
|
return Buffer.from(value, "base64").toString("utf8");
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function normalizePolicyRecord(
|
|
46
|
+
id: string,
|
|
47
|
+
record: PolicyRecord,
|
|
48
|
+
cedarVersion: string,
|
|
49
|
+
fallbackDescription: string,
|
|
50
|
+
): PolicyRecord {
|
|
51
|
+
return {
|
|
52
|
+
cedar_version: record.cedar_version ?? cedarVersion,
|
|
53
|
+
name: record.name ?? id,
|
|
54
|
+
description: record.description ?? fallbackDescription,
|
|
55
|
+
policy_content: record.policy_content,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
async function readPolicyStore(stateDir: string): Promise<PolicyStoreSnapshot> {
|
|
46
60
|
const filePath = resolvePolicyPath(stateDir);
|
|
47
61
|
const { value } = await readJsonFileWithFallback<PolicyStoreSnapshot>(
|
|
48
62
|
filePath,
|
|
49
63
|
buildDefaultPolicyStore() as PolicyStoreSnapshot,
|
|
50
64
|
);
|
|
65
|
+
const policyStore = value.policy_stores?.[POLICY_STORE_ID];
|
|
66
|
+
if (policyStore?.policies) {
|
|
67
|
+
for (const [id, record] of Object.entries(policyStore.policies)) {
|
|
68
|
+
policyStore.policies[id] = normalizePolicyRecord(
|
|
69
|
+
id,
|
|
70
|
+
record,
|
|
71
|
+
value.cedar_version,
|
|
72
|
+
"Managed by Clawclamp.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
51
76
|
return value;
|
|
52
77
|
}
|
|
53
78
|
|
|
@@ -152,12 +177,16 @@ export async function createGrant(params: {
|
|
|
152
177
|
);
|
|
153
178
|
const expiresAtMs = nowMs + ttlSeconds * 1000;
|
|
154
179
|
const id = grantPolicyId(nowMs);
|
|
155
|
-
policyStore.policies![id] =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
policyStore.policies![id] = normalizePolicyRecord(
|
|
181
|
+
id,
|
|
182
|
+
{
|
|
183
|
+
name: `Grant ${params.toolName}`,
|
|
184
|
+
description: params.note?.trim() || "Temporary grant policy.",
|
|
185
|
+
policy_content: encodeBase64(buildGrantPolicy(params.toolName, expiresAtMs)),
|
|
186
|
+
},
|
|
187
|
+
store.cedar_version,
|
|
188
|
+
"Temporary grant policy.",
|
|
189
|
+
);
|
|
161
190
|
await writePolicyStore(params.stateDir, store);
|
|
162
191
|
return {
|
|
163
192
|
id,
|
package/src/guard.ts
CHANGED
|
@@ -151,7 +151,7 @@ async function evaluateCedar(params: {
|
|
|
151
151
|
const request = {
|
|
152
152
|
principals: [
|
|
153
153
|
{
|
|
154
|
-
type: "User",
|
|
154
|
+
type: "Jans::User",
|
|
155
155
|
id: params.config.principalId,
|
|
156
156
|
role: "operator",
|
|
157
157
|
},
|
|
@@ -180,7 +180,7 @@ async function evaluateCedar(params: {
|
|
|
180
180
|
`[clawclamp] cedar response ${JSON.stringify({ request, result: jsonString })}`,
|
|
181
181
|
);
|
|
182
182
|
const parsed = JSON.parse(jsonString) as unknown;
|
|
183
|
-
return parseCedarDecision(parsed, "User", params.config.principalId);
|
|
183
|
+
return parseCedarDecision(parsed, "Jans::User", params.config.principalId);
|
|
184
184
|
} catch (error) {
|
|
185
185
|
return { decision: "error", reason: error instanceof Error ? error.message : String(error) };
|
|
186
186
|
}
|
package/src/http.ts
CHANGED
|
@@ -48,6 +48,14 @@ function parseUrl(rawUrl?: string): URL | null {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function decodePathComponent(value: string): string {
|
|
52
|
+
try {
|
|
53
|
+
return decodeURIComponent(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
function getHeader(req: IncomingMessage, name: string): string | undefined {
|
|
52
60
|
const raw = req.headers[name.toLowerCase()];
|
|
53
61
|
if (typeof raw === "string") {
|
|
@@ -337,7 +345,7 @@ export function createClawClampHttpHandler(params: {
|
|
|
337
345
|
return true;
|
|
338
346
|
}
|
|
339
347
|
try {
|
|
340
|
-
const id = apiPath.slice("policies/".length);
|
|
348
|
+
const id = decodePathComponent(apiPath.slice("policies/".length));
|
|
341
349
|
if (!id) {
|
|
342
350
|
sendJson(res, 400, { error: "policy id required" });
|
|
343
351
|
return true;
|
|
@@ -365,7 +373,7 @@ export function createClawClampHttpHandler(params: {
|
|
|
365
373
|
sendJson(res, 400, { error: "policyStoreUri is read-only" });
|
|
366
374
|
return true;
|
|
367
375
|
}
|
|
368
|
-
const id = apiPath.slice("policies/".length);
|
|
376
|
+
const id = decodePathComponent(apiPath.slice("policies/".length));
|
|
369
377
|
if (!id) {
|
|
370
378
|
sendJson(res, 400, { error: "policy id required" });
|
|
371
379
|
return true;
|
|
@@ -414,7 +422,7 @@ export function createClawClampHttpHandler(params: {
|
|
|
414
422
|
}
|
|
415
423
|
|
|
416
424
|
if (apiPath.startsWith("grants/") && req.method === "DELETE") {
|
|
417
|
-
const grantId = apiPath.slice("grants/".length);
|
|
425
|
+
const grantId = decodePathComponent(apiPath.slice("grants/".length));
|
|
418
426
|
if (!grantId) {
|
|
419
427
|
sendJson(res, 400, { error: "grant id required" });
|
|
420
428
|
return true;
|
package/src/policy-store.ts
CHANGED
|
@@ -50,12 +50,37 @@ function encodeBase64(value: string): string {
|
|
|
50
50
|
return Buffer.from(value, "utf8").toString("base64");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function normalizePolicyRecord(
|
|
54
|
+
id: string,
|
|
55
|
+
record: PolicyRecord,
|
|
56
|
+
cedarVersion: string,
|
|
57
|
+
fallbackDescription: string,
|
|
58
|
+
): PolicyRecord {
|
|
59
|
+
return {
|
|
60
|
+
cedar_version: record.cedar_version ?? cedarVersion,
|
|
61
|
+
name: record.name ?? id,
|
|
62
|
+
description: record.description ?? fallbackDescription,
|
|
63
|
+
policy_content: record.policy_content,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
async function readPolicyStore(stateDir: string): Promise<PolicyStoreSnapshot> {
|
|
54
68
|
const filePath = resolvePolicyPath(stateDir);
|
|
55
69
|
const { value } = await readJsonFileWithFallback<PolicyStoreSnapshot>(
|
|
56
70
|
filePath,
|
|
57
71
|
buildDefaultPolicyStore() as PolicyStoreSnapshot,
|
|
58
72
|
);
|
|
73
|
+
const policyStore = value.policy_stores?.[POLICY_STORE_ID];
|
|
74
|
+
if (policyStore?.policies) {
|
|
75
|
+
for (const [id, record] of Object.entries(policyStore.policies)) {
|
|
76
|
+
policyStore.policies[id] = normalizePolicyRecord(
|
|
77
|
+
id,
|
|
78
|
+
record,
|
|
79
|
+
value.cedar_version,
|
|
80
|
+
"Managed by Clawclamp.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
59
84
|
return value;
|
|
60
85
|
}
|
|
61
86
|
|
|
@@ -140,10 +165,12 @@ export async function createPolicy(params: {
|
|
|
140
165
|
throw new Error("policy id already exists");
|
|
141
166
|
}
|
|
142
167
|
policyStore.policies[id] = {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
...normalizePolicyRecord(
|
|
169
|
+
id,
|
|
170
|
+
{ policy_content: encodeBase64(params.content) },
|
|
171
|
+
store.cedar_version,
|
|
172
|
+
"Created from Clawclamp UI.",
|
|
173
|
+
),
|
|
147
174
|
};
|
|
148
175
|
await writePolicyStore(params.stateDir, store);
|
|
149
176
|
return { id, content: params.content };
|
|
@@ -162,10 +189,12 @@ export async function updatePolicy(params: {
|
|
|
162
189
|
throw new Error("policy id not found");
|
|
163
190
|
}
|
|
164
191
|
policyStore.policies[params.id] = {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
...normalizePolicyRecord(
|
|
193
|
+
params.id,
|
|
194
|
+
{ policy_content: encodeBase64(params.content) },
|
|
195
|
+
store.cedar_version,
|
|
196
|
+
"Updated from Clawclamp UI.",
|
|
197
|
+
),
|
|
169
198
|
};
|
|
170
199
|
await writePolicyStore(params.stateDir, store);
|
|
171
200
|
return { id: params.id, content: params.content };
|
package/src/policy.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
const DEFAULT_SCHEMA = `
|
|
1
|
+
const DEFAULT_SCHEMA = `namespace Jans {
|
|
2
|
+
entity Role;
|
|
3
|
+
|
|
4
|
+
entity User in [Role] = {
|
|
2
5
|
role: String
|
|
3
6
|
};
|
|
7
|
+
}
|
|
4
8
|
|
|
5
9
|
entity Tool = {
|
|
6
10
|
name: String,
|
|
@@ -8,7 +12,7 @@ entity Tool = {
|
|
|
8
12
|
};
|
|
9
13
|
|
|
10
14
|
action "Invoke" appliesTo {
|
|
11
|
-
principal: [User],
|
|
15
|
+
principal: [Jans::User],
|
|
12
16
|
resource: [Tool],
|
|
13
17
|
context: {
|
|
14
18
|
now: Long,
|