cedar-mcp-server 1.0.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/.editorconfig +12 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +42 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +241 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +182 -0
- package/README.md +1635 -0
- package/SECURITY.md +37 -0
- package/dist/http-server.d.ts +61 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +194 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/policy-ast.d.ts +49 -0
- package/dist/parser/policy-ast.d.ts.map +1 -0
- package/dist/parser/policy-ast.js +311 -0
- package/dist/parser/policy-ast.js.map +1 -0
- package/dist/prompts/index.d.ts +38 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +172 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/ref-resolver.d.ts +23 -0
- package/dist/resources/ref-resolver.d.ts.map +1 -0
- package/dist/resources/ref-resolver.js +128 -0
- package/dist/resources/ref-resolver.js.map +1 -0
- package/dist/resources/store-manager.d.ts +64 -0
- package/dist/resources/store-manager.d.ts.map +1 -0
- package/dist/resources/store-manager.js +221 -0
- package/dist/resources/store-manager.js.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +539 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/advise/avp-rules.d.ts +49 -0
- package/dist/tools/advise/avp-rules.d.ts.map +1 -0
- package/dist/tools/advise/avp-rules.js +59 -0
- package/dist/tools/advise/avp-rules.js.map +1 -0
- package/dist/tools/advise/cedar-patterns.d.ts +24 -0
- package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
- package/dist/tools/advise/cedar-patterns.js +57 -0
- package/dist/tools/advise/cedar-patterns.js.map +1 -0
- package/dist/tools/advise/context-builder.d.ts +28 -0
- package/dist/tools/advise/context-builder.d.ts.map +1 -0
- package/dist/tools/advise/context-builder.js +89 -0
- package/dist/tools/advise/context-builder.js.map +1 -0
- package/dist/tools/advise/gotchas.d.ts +15 -0
- package/dist/tools/advise/gotchas.d.ts.map +1 -0
- package/dist/tools/advise/gotchas.js +83 -0
- package/dist/tools/advise/gotchas.js.map +1 -0
- package/dist/tools/advise.d.ts +96 -0
- package/dist/tools/advise.d.ts.map +1 -0
- package/dist/tools/advise.js +258 -0
- package/dist/tools/advise.js.map +1 -0
- package/dist/tools/authorize-batch.d.ts +35 -0
- package/dist/tools/authorize-batch.d.ts.map +1 -0
- package/dist/tools/authorize-batch.js +262 -0
- package/dist/tools/authorize-batch.js.map +1 -0
- package/dist/tools/authorize.d.ts +115 -0
- package/dist/tools/authorize.d.ts.map +1 -0
- package/dist/tools/authorize.js +373 -0
- package/dist/tools/authorize.js.map +1 -0
- package/dist/tools/check-change.d.ts +19 -0
- package/dist/tools/check-change.d.ts.map +1 -0
- package/dist/tools/check-change.js +91 -0
- package/dist/tools/check-change.js.map +1 -0
- package/dist/tools/diff-schema.d.ts +103 -0
- package/dist/tools/diff-schema.d.ts.map +1 -0
- package/dist/tools/diff-schema.js +379 -0
- package/dist/tools/diff-schema.js.map +1 -0
- package/dist/tools/diff-stores.d.ts +45 -0
- package/dist/tools/diff-stores.d.ts.map +1 -0
- package/dist/tools/diff-stores.js +222 -0
- package/dist/tools/diff-stores.js.map +1 -0
- package/dist/tools/explain.d.ts +80 -0
- package/dist/tools/explain.d.ts.map +1 -0
- package/dist/tools/explain.js +187 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/format.d.ts +11 -0
- package/dist/tools/format.d.ts.map +1 -0
- package/dist/tools/format.js +20 -0
- package/dist/tools/format.js.map +1 -0
- package/dist/tools/generate-sample.d.ts +28 -0
- package/dist/tools/generate-sample.d.ts.map +1 -0
- package/dist/tools/generate-sample.js +568 -0
- package/dist/tools/generate-sample.js.map +1 -0
- package/dist/tools/link-template.d.ts +17 -0
- package/dist/tools/link-template.d.ts.map +1 -0
- package/dist/tools/link-template.js +78 -0
- package/dist/tools/link-template.js.map +1 -0
- package/dist/tools/list-template-links.d.ts +16 -0
- package/dist/tools/list-template-links.d.ts.map +1 -0
- package/dist/tools/list-template-links.js +22 -0
- package/dist/tools/list-template-links.js.map +1 -0
- package/dist/tools/list-templates.d.ts +16 -0
- package/dist/tools/list-templates.d.ts.map +1 -0
- package/dist/tools/list-templates.js +36 -0
- package/dist/tools/list-templates.js.map +1 -0
- package/dist/tools/translate.d.ts +11 -0
- package/dist/tools/translate.d.ts.map +1 -0
- package/dist/tools/translate.js +53 -0
- package/dist/tools/translate.js.map +1 -0
- package/dist/tools/validate-entities.d.ts +19 -0
- package/dist/tools/validate-entities.d.ts.map +1 -0
- package/dist/tools/validate-entities.js +88 -0
- package/dist/tools/validate-entities.js.map +1 -0
- package/dist/tools/validate-schema.d.ts +22 -0
- package/dist/tools/validate-schema.d.ts.map +1 -0
- package/dist/tools/validate-schema.js +89 -0
- package/dist/tools/validate-schema.js.map +1 -0
- package/dist/tools/validate-template.d.ts +18 -0
- package/dist/tools/validate-template.d.ts.map +1 -0
- package/dist/tools/validate-template.js +59 -0
- package/dist/tools/validate-template.js.map +1 -0
- package/dist/tools/validate.d.ts +90 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +351 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/utils/format-detector.d.ts +49 -0
- package/dist/utils/format-detector.d.ts.map +1 -0
- package/dist/utils/format-detector.js +298 -0
- package/dist/utils/format-detector.js.map +1 -0
- package/examples/README.md +36 -0
- package/examples/abac-multi-tenant/README.md +150 -0
- package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
- package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
- package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
- package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
- package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
- package/examples/abac-multi-tenant/run.ts +92 -0
- package/examples/abac-multi-tenant/schema.json +60 -0
- package/examples/api-gateway-path-routing/README.md +154 -0
- package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
- package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
- package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
- package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
- package/examples/api-gateway-path-routing/run.ts +108 -0
- package/examples/api-gateway-path-routing/schema.json +54 -0
- package/examples/rbac-document-management/README.md +167 -0
- package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
- package/examples/rbac-document-management/policies/admin.cedar +6 -0
- package/examples/rbac-document-management/policies/editor.cedar +6 -0
- package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
- package/examples/rbac-document-management/policies/viewer.cedar +6 -0
- package/examples/rbac-document-management/run.ts +87 -0
- package/examples/rbac-document-management/schema.json +57 -0
- package/package.json +50 -0
- package/src/http-server.ts +239 -0
- package/src/index.ts +294 -0
- package/src/parser/policy-ast.ts +345 -0
- package/src/prompts/README.md +3 -0
- package/src/prompts/index.ts +217 -0
- package/src/resources/ref-resolver.ts +134 -0
- package/src/resources/store-manager.ts +248 -0
- package/src/server.ts +711 -0
- package/src/tools/advise/avp-rules.ts +70 -0
- package/src/tools/advise/cedar-patterns.ts +73 -0
- package/src/tools/advise/context-builder.ts +109 -0
- package/src/tools/advise/gotchas.ts +92 -0
- package/src/tools/advise.ts +366 -0
- package/src/tools/authorize-batch.ts +345 -0
- package/src/tools/authorize.ts +464 -0
- package/src/tools/check-change.ts +119 -0
- package/src/tools/diff-schema.ts +510 -0
- package/src/tools/diff-stores.ts +298 -0
- package/src/tools/explain.ts +278 -0
- package/src/tools/format.ts +33 -0
- package/src/tools/generate-sample.ts +665 -0
- package/src/tools/link-template.ts +109 -0
- package/src/tools/list-template-links.ts +41 -0
- package/src/tools/list-templates.ts +55 -0
- package/src/tools/translate.ts +66 -0
- package/src/tools/validate-entities.ts +125 -0
- package/src/tools/validate-schema.ts +128 -0
- package/src/tools/validate-template.ts +72 -0
- package/src/tools/validate.ts +459 -0
- package/src/utils/format-detector.ts +356 -0
- package/test/fixtures/docmgmt.ts +121 -0
- package/test/fixtures/multitenant.ts +163 -0
- package/test/index.test.ts +96 -0
- package/test/integration/e2e/behavior.test.ts +359 -0
- package/test/integration/e2e/edge-cases.test.ts +365 -0
- package/test/integration/e2e/failure-modes.test.ts +266 -0
- package/test/integration/e2e/protocol.test.ts +252 -0
- package/test/integration/http-smoke.test.ts +588 -0
- package/test/integration/smoke.test.ts +475 -0
- package/test/prompts/prompts.test.ts +173 -0
- package/test/property/properties.test.ts +234 -0
- package/test/resources/ref-resolver.test.ts +186 -0
- package/test/resources/store-manager.test.ts +344 -0
- package/test/setup.test.ts +7 -0
- package/test/tools/advise/avp-rules.test.ts +76 -0
- package/test/tools/advise.test.ts +339 -0
- package/test/tools/authorize-batch.test.ts +459 -0
- package/test/tools/authorize.test.ts +682 -0
- package/test/tools/check-change.test.ts +104 -0
- package/test/tools/cross-fixture.test.ts +170 -0
- package/test/tools/diff-schema.test.ts +355 -0
- package/test/tools/diff-stores.test.ts +291 -0
- package/test/tools/explain.test.ts +221 -0
- package/test/tools/format.test.ts +33 -0
- package/test/tools/generate-sample.test.ts +480 -0
- package/test/tools/link-template.test.ts +90 -0
- package/test/tools/list-templates.test.ts +151 -0
- package/test/tools/translate.test.ts +89 -0
- package/test/tools/validate-entities.test.ts +178 -0
- package/test/tools/validate-schema.test.ts +86 -0
- package/test/tools/validate-template.test.ts +89 -0
- package/test/tools/validate.test.ts +331 -0
- package/test/utils/format-detector.test.ts +518 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { isAuthorized } from "@cedar-policy/cedar-wasm/nodejs";
|
|
2
|
+
import type { StoreManager } from "../resources/store-manager.js";
|
|
3
|
+
import { handleCheckChange } from "./check-change.js";
|
|
4
|
+
import { handleDiffSchema, type SchemaDiff } from "./diff-schema.js";
|
|
5
|
+
import { normalizePrincipalRef } from "../utils/format-detector.js";
|
|
6
|
+
import type { Entities } from "@cedar-policy/cedar-wasm/nodejs";
|
|
7
|
+
|
|
8
|
+
export interface DiffStoresInput {
|
|
9
|
+
blue: string;
|
|
10
|
+
green: string;
|
|
11
|
+
behavioral_test_requests?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PolicyChangeInfo {
|
|
15
|
+
policy_id: string;
|
|
16
|
+
can_update_in_place: boolean;
|
|
17
|
+
changes: Array<{ field: string; in_place_allowed: boolean; reason: string }>;
|
|
18
|
+
recommendation: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BehavioralDriftEntry {
|
|
22
|
+
principal: string;
|
|
23
|
+
action: string;
|
|
24
|
+
resource: string;
|
|
25
|
+
blue_decision: "Allow" | "Deny" | "Error";
|
|
26
|
+
green_decision: "Allow" | "Deny" | "Error";
|
|
27
|
+
drifted: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DiffStoresResult {
|
|
32
|
+
blue: string;
|
|
33
|
+
green: string;
|
|
34
|
+
policies_added: Array<{ policy_id: string; content: string }>;
|
|
35
|
+
policies_removed: Array<{ policy_id: string; content: string }>;
|
|
36
|
+
policies_modified: PolicyChangeInfo[];
|
|
37
|
+
schema_diff: SchemaDiff;
|
|
38
|
+
behavioral_diff?: BehavioralDriftEntry[];
|
|
39
|
+
summary: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function handleDiffStores(
|
|
44
|
+
input: DiffStoresInput,
|
|
45
|
+
manager: StoreManager
|
|
46
|
+
): Promise<DiffStoresResult> {
|
|
47
|
+
// Validate stores exist
|
|
48
|
+
try {
|
|
49
|
+
manager.requireStore(input.blue);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return errorResult(input.blue, input.green, e instanceof Error ? e.message : String(e));
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
manager.requireStore(input.green);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return errorResult(input.blue, input.green, e instanceof Error ? e.message : String(e));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const bluePolicies = new Map<string, string>();
|
|
60
|
+
const greenPolicies = new Map<string, string>();
|
|
61
|
+
|
|
62
|
+
for (const id of manager.listPolicies(input.blue)) {
|
|
63
|
+
bluePolicies.set(id, manager.readPolicy(input.blue, id));
|
|
64
|
+
}
|
|
65
|
+
for (const id of manager.listPolicies(input.green)) {
|
|
66
|
+
greenPolicies.set(id, manager.readPolicy(input.green, id));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Structural diff
|
|
70
|
+
const policies_added: DiffStoresResult["policies_added"] = [];
|
|
71
|
+
const policies_removed: DiffStoresResult["policies_removed"] = [];
|
|
72
|
+
const policies_modified: PolicyChangeInfo[] = [];
|
|
73
|
+
|
|
74
|
+
for (const [id, content] of greenPolicies) {
|
|
75
|
+
if (!bluePolicies.has(id)) {
|
|
76
|
+
policies_added.push({ policy_id: id, content });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const [id, content] of bluePolicies) {
|
|
81
|
+
if (!greenPolicies.has(id)) {
|
|
82
|
+
policies_removed.push({ policy_id: id, content });
|
|
83
|
+
} else {
|
|
84
|
+
const blueContent = content;
|
|
85
|
+
const greenContent = greenPolicies.get(id)!;
|
|
86
|
+
if (blueContent.trim() !== greenContent.trim()) {
|
|
87
|
+
// Reuse check-change logic for AVP immutability classification
|
|
88
|
+
const changeResult = await handleCheckChange({
|
|
89
|
+
old_policy: blueContent,
|
|
90
|
+
new_policy: greenContent,
|
|
91
|
+
});
|
|
92
|
+
if (changeResult.error) {
|
|
93
|
+
// Parse error on one or both sides — report as modified with error context
|
|
94
|
+
policies_modified.push({
|
|
95
|
+
policy_id: id,
|
|
96
|
+
can_update_in_place: false,
|
|
97
|
+
changes: [],
|
|
98
|
+
recommendation: `Could not diff policy "${id}": ${changeResult.error}`,
|
|
99
|
+
});
|
|
100
|
+
} else if (changeResult.changes.length > 0) {
|
|
101
|
+
policies_modified.push({
|
|
102
|
+
policy_id: id,
|
|
103
|
+
can_update_in_place: changeResult.can_update_in_place,
|
|
104
|
+
changes: changeResult.changes.map((c) => ({
|
|
105
|
+
field: c.field,
|
|
106
|
+
in_place_allowed: c.in_place_allowed,
|
|
107
|
+
reason: c.reason,
|
|
108
|
+
})),
|
|
109
|
+
recommendation: changeResult.recommendation,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// If changes.length === 0 and no error: policies differ in text but not semantically
|
|
113
|
+
// (formatting change). Treat as unchanged — no entry in policies_modified.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Schema diff — structured via handleDiffSchema
|
|
119
|
+
let schema_diff: SchemaDiff;
|
|
120
|
+
try {
|
|
121
|
+
const blueSchema = manager.readSchema(input.blue);
|
|
122
|
+
const greenSchema = manager.readSchema(input.green);
|
|
123
|
+
schema_diff = await handleDiffSchema({ blue: blueSchema, green: greenSchema });
|
|
124
|
+
} catch (e) {
|
|
125
|
+
schema_diff = emptySchemaDiff(`Schema comparison failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Behavioral diff (optional)
|
|
129
|
+
let behavioral_diff: BehavioralDriftEntry[] | undefined;
|
|
130
|
+
if (input.behavioral_test_requests) {
|
|
131
|
+
behavioral_diff = await runBehavioralDiff(
|
|
132
|
+
input.blue,
|
|
133
|
+
input.green,
|
|
134
|
+
input.behavioral_test_requests,
|
|
135
|
+
manager
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Summary
|
|
140
|
+
const totalChanges =
|
|
141
|
+
policies_added.length + policies_removed.length + policies_modified.length;
|
|
142
|
+
const requiresRecreate = policies_modified.filter((p) => !p.can_update_in_place).length;
|
|
143
|
+
const driftCount = behavioral_diff?.filter((d) => d.drifted).length ?? 0;
|
|
144
|
+
const schema_changed = hasSchemaChanges(schema_diff);
|
|
145
|
+
const schemaBreaking = schema_diff.risk_level === "breaking";
|
|
146
|
+
|
|
147
|
+
let summary: string;
|
|
148
|
+
if (totalChanges === 0 && !schema_changed) {
|
|
149
|
+
summary = "No changes detected between blue and green stores.";
|
|
150
|
+
} else {
|
|
151
|
+
const parts: string[] = [];
|
|
152
|
+
if (policies_added.length) parts.push(`${policies_added.length} added`);
|
|
153
|
+
if (policies_removed.length) parts.push(`${policies_removed.length} removed`);
|
|
154
|
+
if (policies_modified.length) {
|
|
155
|
+
parts.push(`${policies_modified.length} modified`);
|
|
156
|
+
if (requiresRecreate) parts.push(`(${requiresRecreate} require delete-recreate in AVP)`);
|
|
157
|
+
}
|
|
158
|
+
if (schema_changed) {
|
|
159
|
+
parts.push(schemaBreaking ? "schema changed (BREAKING)" : "schema changed");
|
|
160
|
+
}
|
|
161
|
+
if (driftCount) parts.push(`${driftCount} authorization decision(s) would change`);
|
|
162
|
+
summary = `Policy diff: ${parts.join(", ")}.`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
blue: input.blue,
|
|
167
|
+
green: input.green,
|
|
168
|
+
policies_added,
|
|
169
|
+
policies_removed,
|
|
170
|
+
policies_modified,
|
|
171
|
+
schema_diff,
|
|
172
|
+
...(behavioral_diff !== undefined ? { behavioral_diff } : {}),
|
|
173
|
+
summary,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function hasSchemaChanges(d: SchemaDiff): boolean {
|
|
178
|
+
return (
|
|
179
|
+
d.namespaces_added.length > 0 ||
|
|
180
|
+
d.namespaces_removed.length > 0 ||
|
|
181
|
+
d.entity_types.added.length > 0 ||
|
|
182
|
+
d.entity_types.removed.length > 0 ||
|
|
183
|
+
d.entity_types.modified.length > 0 ||
|
|
184
|
+
d.actions.added.length > 0 ||
|
|
185
|
+
d.actions.removed.length > 0 ||
|
|
186
|
+
d.actions.modified.length > 0 ||
|
|
187
|
+
d.common_types.added.length > 0 ||
|
|
188
|
+
d.common_types.removed.length > 0 ||
|
|
189
|
+
d.common_types.modified.length > 0
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function emptySchemaDiff(error?: string): SchemaDiff {
|
|
194
|
+
return {
|
|
195
|
+
namespaces_added: [],
|
|
196
|
+
namespaces_removed: [],
|
|
197
|
+
entity_types: { added: [], removed: [], modified: [] },
|
|
198
|
+
actions: { added: [], removed: [], modified: [] },
|
|
199
|
+
common_types: { added: [], removed: [], modified: [] },
|
|
200
|
+
summary: "",
|
|
201
|
+
risk_level: "safe",
|
|
202
|
+
...(error ? { error } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runBehavioralDiff(
|
|
207
|
+
blue: string,
|
|
208
|
+
green: string,
|
|
209
|
+
requestsJson: string,
|
|
210
|
+
manager: StoreManager
|
|
211
|
+
): Promise<BehavioralDriftEntry[]> {
|
|
212
|
+
let requests: Array<{
|
|
213
|
+
principal: string | object;
|
|
214
|
+
action: string | object;
|
|
215
|
+
resource: string | object;
|
|
216
|
+
entities: string;
|
|
217
|
+
context?: string;
|
|
218
|
+
}>;
|
|
219
|
+
try {
|
|
220
|
+
requests = JSON.parse(requestsJson);
|
|
221
|
+
if (!Array.isArray(requests)) return [{ principal: "", action: "", resource: "", blue_decision: "Deny", green_decision: "Deny", drifted: false }];
|
|
222
|
+
} catch {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const bluePolicies = manager.readAllPolicies(blue);
|
|
227
|
+
const greenPolicies = manager.readAllPolicies(green);
|
|
228
|
+
|
|
229
|
+
const entries: BehavioralDriftEntry[] = [];
|
|
230
|
+
|
|
231
|
+
for (const req of requests) {
|
|
232
|
+
const principalRef = normalizePrincipalRef(req.principal);
|
|
233
|
+
const actionRef = normalizePrincipalRef(req.action);
|
|
234
|
+
const resourceRef = normalizePrincipalRef(req.resource);
|
|
235
|
+
|
|
236
|
+
const refError =
|
|
237
|
+
("error" in principalRef ? principalRef.error : null) ??
|
|
238
|
+
("error" in actionRef ? actionRef.error : null) ??
|
|
239
|
+
("error" in resourceRef ? resourceRef.error : null);
|
|
240
|
+
|
|
241
|
+
const principalStr = typeof req.principal === "string" ? req.principal : JSON.stringify(req.principal);
|
|
242
|
+
const actionStr = typeof req.action === "string" ? req.action : JSON.stringify(req.action);
|
|
243
|
+
const resourceStr = typeof req.resource === "string" ? req.resource : JSON.stringify(req.resource);
|
|
244
|
+
|
|
245
|
+
if (refError) {
|
|
246
|
+
entries.push({ principal: principalStr, action: actionStr, resource: resourceStr, blue_decision: "Error", green_decision: "Error", drifted: false, error: refError });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let entities: Entities;
|
|
251
|
+
try {
|
|
252
|
+
entities = JSON.parse(req.entities);
|
|
253
|
+
} catch {
|
|
254
|
+
entries.push({ principal: principalStr, action: actionStr, resource: resourceStr, blue_decision: "Error", green_decision: "Error", drifted: false, error: "Invalid entities JSON" });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// After the refError guard above, refs are guaranteed to be NormalizedRef (no error field)
|
|
259
|
+
const safeP = principalRef as { type: string; id: string };
|
|
260
|
+
const safeA = actionRef as { type: string; id: string };
|
|
261
|
+
const safeR = resourceRef as { type: string; id: string };
|
|
262
|
+
const context = {};
|
|
263
|
+
const callBase = { principal: safeP, action: safeA, resource: safeR, context, entities };
|
|
264
|
+
|
|
265
|
+
const blueAnswer = isAuthorized({ ...callBase, policies: { staticPolicies: bluePolicies } });
|
|
266
|
+
const greenAnswer = isAuthorized({ ...callBase, policies: { staticPolicies: greenPolicies } });
|
|
267
|
+
|
|
268
|
+
const blueDecision: "Allow" | "Deny" =
|
|
269
|
+
blueAnswer.type === "success" && blueAnswer.response.decision === "allow" ? "Allow" : "Deny";
|
|
270
|
+
const greenDecision: "Allow" | "Deny" =
|
|
271
|
+
greenAnswer.type === "success" && greenAnswer.response.decision === "allow" ? "Allow" : "Deny";
|
|
272
|
+
|
|
273
|
+
entries.push({
|
|
274
|
+
principal: principalStr,
|
|
275
|
+
action: actionStr,
|
|
276
|
+
resource: resourceStr,
|
|
277
|
+
blue_decision: blueDecision,
|
|
278
|
+
green_decision: greenDecision,
|
|
279
|
+
drifted: blueDecision !== greenDecision,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return entries;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function errorResult(blue: string, green: string, error: string): DiffStoresResult {
|
|
287
|
+
return {
|
|
288
|
+
blue,
|
|
289
|
+
green,
|
|
290
|
+
policies_added: [],
|
|
291
|
+
policies_removed: [],
|
|
292
|
+
policies_modified: [],
|
|
293
|
+
schema_diff: emptySchemaDiff(),
|
|
294
|
+
summary: "",
|
|
295
|
+
error,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { policyToJson, templateToJson, policySetTextToParts } from "@cedar-policy/cedar-wasm/nodejs";
|
|
2
|
+
import {
|
|
3
|
+
describePrincipal,
|
|
4
|
+
describeAction,
|
|
5
|
+
describeResource,
|
|
6
|
+
describeCondition,
|
|
7
|
+
detectPatterns,
|
|
8
|
+
} from "../parser/policy-ast.js";
|
|
9
|
+
import type { PolicyJson } from "@cedar-policy/cedar-wasm/nodejs";
|
|
10
|
+
import { storeManager } from "../resources/store-manager.js";
|
|
11
|
+
|
|
12
|
+
export interface ExplainInput {
|
|
13
|
+
policy: string;
|
|
14
|
+
schema?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ScopeDescription {
|
|
18
|
+
scope: string;
|
|
19
|
+
description: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ConditionDescription {
|
|
23
|
+
kind: "when" | "unless";
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExplainResult {
|
|
28
|
+
effect: "permit" | "forbid";
|
|
29
|
+
principal: ScopeDescription;
|
|
30
|
+
action: ScopeDescription;
|
|
31
|
+
resource: ScopeDescription;
|
|
32
|
+
conditions: ConditionDescription[];
|
|
33
|
+
summary: string;
|
|
34
|
+
patterns_detected: string[];
|
|
35
|
+
error?: string;
|
|
36
|
+
/**
|
|
37
|
+
* 10d workspace auto-discovery: populated by the server.ts MCP handler when
|
|
38
|
+
* the schema was resolved from a loaded MCP root rather than supplied inline.
|
|
39
|
+
* On ExplainManyResult the field appears on the top-level result so a single
|
|
40
|
+
* auto-discovery decision applies to the whole policy set.
|
|
41
|
+
*/
|
|
42
|
+
auto_discovered?: {
|
|
43
|
+
schema_from?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parsePolicyJson(policyText: string): PolicyJson {
|
|
48
|
+
const result = policyToJson(policyText);
|
|
49
|
+
if (result.type === "success") return result.json;
|
|
50
|
+
|
|
51
|
+
const errors = result.errors.map((e) => e.message).join("; ");
|
|
52
|
+
|
|
53
|
+
// Fall back to templateToJson if the error is about template slots
|
|
54
|
+
if (errors.includes("template") || errors.includes("slot")) {
|
|
55
|
+
const templateResult = templateToJson(policyText);
|
|
56
|
+
if (templateResult.type === "success") return templateResult.json as unknown as PolicyJson;
|
|
57
|
+
throw new Error(templateResult.errors.map((e) => e.message).join("; "));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(errors);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildSummary(
|
|
64
|
+
json: PolicyJson,
|
|
65
|
+
principalDesc: string,
|
|
66
|
+
actionDesc: string,
|
|
67
|
+
resourceDesc: string,
|
|
68
|
+
conditions: ConditionDescription[],
|
|
69
|
+
isTemplate: boolean
|
|
70
|
+
): string {
|
|
71
|
+
const effect = json.effect === "permit" ? "PERMITS" : "FORBIDS";
|
|
72
|
+
const base = `${effect} ${principalDesc} to perform ${actionDesc} on ${resourceDesc}`;
|
|
73
|
+
|
|
74
|
+
if (isTemplate) {
|
|
75
|
+
const slots = [
|
|
76
|
+
json.principal.op === "==" && "slot" in json.principal ? `?principal` : null,
|
|
77
|
+
json.resource.op === "==" && "slot" in json.resource ? `?resource` : null,
|
|
78
|
+
].filter(Boolean);
|
|
79
|
+
return `TEMPLATE POLICY: ${base}. Template slots: ${slots.join(", ")}.`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (conditions.length === 0) return `${base}.`;
|
|
83
|
+
|
|
84
|
+
const whenClauses = conditions
|
|
85
|
+
.filter((c) => c.kind === "when")
|
|
86
|
+
.map((c) => c.text.replace(/^WHEN /, ""));
|
|
87
|
+
const unlessClauses = conditions
|
|
88
|
+
.filter((c) => c.kind === "unless")
|
|
89
|
+
.map((c) => c.text.replace(/^UNLESS /, ""));
|
|
90
|
+
|
|
91
|
+
let summary = base;
|
|
92
|
+
if (whenClauses.length > 0) summary += `, when: ${whenClauses.join("; ")}`;
|
|
93
|
+
if (unlessClauses.length > 0) summary += `, unless: ${unlessClauses.join("; ")}`;
|
|
94
|
+
return summary + ".";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function handleExplain(input: ExplainInput): Promise<ExplainResult> {
|
|
98
|
+
let json: PolicyJson;
|
|
99
|
+
let isTemplate = false;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const raw = policyToJson(input.policy);
|
|
103
|
+
if (raw.type === "failure") {
|
|
104
|
+
const errors = raw.errors.map((e) => e.message).join("; ");
|
|
105
|
+
if (errors.includes("template") || errors.includes("slot")) {
|
|
106
|
+
const templateResult = templateToJson(input.policy);
|
|
107
|
+
if (templateResult.type === "failure") {
|
|
108
|
+
return {
|
|
109
|
+
effect: "permit",
|
|
110
|
+
principal: { scope: "unknown", description: "unknown" },
|
|
111
|
+
action: { scope: "unknown", description: "unknown" },
|
|
112
|
+
resource: { scope: "unknown", description: "unknown" },
|
|
113
|
+
conditions: [],
|
|
114
|
+
summary: "Failed to parse policy.",
|
|
115
|
+
patterns_detected: [],
|
|
116
|
+
error: templateResult.errors.map((e) => e.message).join("; "),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
json = templateResult.json as unknown as PolicyJson;
|
|
120
|
+
isTemplate = true;
|
|
121
|
+
} else {
|
|
122
|
+
return {
|
|
123
|
+
effect: "permit",
|
|
124
|
+
principal: { scope: "unknown", description: "unknown" },
|
|
125
|
+
action: { scope: "unknown", description: "unknown" },
|
|
126
|
+
resource: { scope: "unknown", description: "unknown" },
|
|
127
|
+
conditions: [],
|
|
128
|
+
summary: "Failed to parse policy.",
|
|
129
|
+
patterns_detected: [],
|
|
130
|
+
error: errors,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
json = raw.json;
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
138
|
+
return {
|
|
139
|
+
effect: "permit",
|
|
140
|
+
principal: { scope: "unknown", description: "unknown" },
|
|
141
|
+
action: { scope: "unknown", description: "unknown" },
|
|
142
|
+
resource: { scope: "unknown", description: "unknown" },
|
|
143
|
+
conditions: [],
|
|
144
|
+
summary: "Failed to parse policy.",
|
|
145
|
+
patterns_detected: [],
|
|
146
|
+
error: msg,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const principalDesc = describePrincipal(json.principal);
|
|
151
|
+
const actionDesc = describeAction(json.action);
|
|
152
|
+
const resourceDesc = describeResource(json.resource);
|
|
153
|
+
|
|
154
|
+
const conditions: ConditionDescription[] = json.conditions.map((c) => ({
|
|
155
|
+
kind: c.kind,
|
|
156
|
+
text: describeCondition(c),
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
const patterns = detectPatterns(json);
|
|
160
|
+
if (isTemplate && !patterns.includes("template_policy")) patterns.unshift("template_policy");
|
|
161
|
+
|
|
162
|
+
const summary = buildSummary(json, principalDesc, actionDesc, resourceDesc, conditions, isTemplate);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
effect: json.effect,
|
|
166
|
+
principal: { scope: json.principal.op, description: principalDesc },
|
|
167
|
+
action: { scope: json.action.op, description: actionDesc },
|
|
168
|
+
resource: { scope: json.resource.op, description: resourceDesc },
|
|
169
|
+
conditions,
|
|
170
|
+
summary,
|
|
171
|
+
patterns_detected: patterns,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Multi-policy entry point ──────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
export interface ExplainManyResult {
|
|
178
|
+
policy_count: number;
|
|
179
|
+
policies: Array<ExplainResult & { index: number }>;
|
|
180
|
+
/**
|
|
181
|
+
* 10d workspace auto-discovery: see ExplainResult.auto_discovered. On the
|
|
182
|
+
* many-result the field lives at the top level so the auto-discovered schema
|
|
183
|
+
* is reported once rather than duplicated on every policy entry.
|
|
184
|
+
*/
|
|
185
|
+
auto_discovered?: {
|
|
186
|
+
schema_from?: string;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Explains a Cedar policy set (one or more policies).
|
|
192
|
+
* Uses policySetTextToParts to split, then explains each individually.
|
|
193
|
+
* Falls back to single-policy handling when there is exactly one policy.
|
|
194
|
+
*/
|
|
195
|
+
export async function handleExplainMany(input: ExplainInput): Promise<ExplainManyResult | ExplainResult> {
|
|
196
|
+
const parts = policySetTextToParts(input.policy);
|
|
197
|
+
|
|
198
|
+
// Single policy or unparseable — fall through to single-policy handler
|
|
199
|
+
if (parts.type === "failure" || (parts.policies.length + parts.policy_templates.length) <= 1) {
|
|
200
|
+
return handleExplain(input);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const allPolicies = [...parts.policies, ...parts.policy_templates];
|
|
204
|
+
const results = await Promise.all(
|
|
205
|
+
allPolicies.map(async (policyText, i) => {
|
|
206
|
+
const result = await handleExplain({ policy: policyText, schema: input.schema });
|
|
207
|
+
return { ...result, index: i };
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
policy_count: allPolicies.length,
|
|
213
|
+
policies: results,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── 10d workspace auto-discovery wrapper ────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Inputs accepted by the MCP-level explain entry point. Wider than
|
|
221
|
+
* `ExplainInput` because it also accepts the `_ref` shape the MCP layer
|
|
222
|
+
* resolves before reaching `handleExplainMany`.
|
|
223
|
+
*/
|
|
224
|
+
export interface ExplainMcpInput {
|
|
225
|
+
policy: string;
|
|
226
|
+
schema?: string;
|
|
227
|
+
schema_ref?: string;
|
|
228
|
+
store?: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 10d workspace auto-discovery wrapper for `cedar_explain`. Resolves the
|
|
233
|
+
* schema from a loaded MCP root when neither `schema` nor `schema_ref` was
|
|
234
|
+
* supplied. The schema is optional for explain, so single-store deployments
|
|
235
|
+
* with no schema file just delegate to the parser without one. Multi-store
|
|
236
|
+
* deployments with no explicit `store` parameter return an ambiguity error.
|
|
237
|
+
*/
|
|
238
|
+
export async function handleExplainMcp(
|
|
239
|
+
input: ExplainMcpInput,
|
|
240
|
+
resolveRef: (uri: string) => { content: string } | { error: string },
|
|
241
|
+
): Promise<{ result: ExplainResult | ExplainManyResult } | { error: string }> {
|
|
242
|
+
let schema = input.schema;
|
|
243
|
+
if (!schema && input.schema_ref) {
|
|
244
|
+
const resolved = resolveRef(input.schema_ref);
|
|
245
|
+
if ("error" in resolved) return { error: resolved.error };
|
|
246
|
+
schema = resolved.content;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let autoSchemaFrom: string | undefined;
|
|
250
|
+
if (!schema && !input.schema_ref) {
|
|
251
|
+
if (input.store) {
|
|
252
|
+
try {
|
|
253
|
+
schema = storeManager.readSchema(input.store);
|
|
254
|
+
autoSchemaFrom = input.store;
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const def = storeManager.getDefaultStore();
|
|
260
|
+
if (def.kind === "single") {
|
|
261
|
+
try {
|
|
262
|
+
schema = storeManager.readSchema(def.store.name);
|
|
263
|
+
autoSchemaFrom = def.store.name;
|
|
264
|
+
} catch {
|
|
265
|
+
// Store has no schema file; explain runs without a schema.
|
|
266
|
+
}
|
|
267
|
+
} else if (def.kind === "ambiguous") {
|
|
268
|
+
return { error: `Multiple stores are loaded (${def.names.join(", ")}). Pass store: "<name>" to choose.` };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = await handleExplainMany({ policy: input.policy, schema });
|
|
274
|
+
if (autoSchemaFrom) {
|
|
275
|
+
result.auto_discovered = { schema_from: autoSchemaFrom };
|
|
276
|
+
}
|
|
277
|
+
return { result };
|
|
278
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { formatPolicies } from "@cedar-policy/cedar-wasm/nodejs";
|
|
2
|
+
|
|
3
|
+
export interface FormatInput {
|
|
4
|
+
policies: string;
|
|
5
|
+
line_width?: number;
|
|
6
|
+
indent_width?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FormatResult {
|
|
10
|
+
formatted: string | null;
|
|
11
|
+
error: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function handleFormat(input: FormatInput): Promise<FormatResult> {
|
|
15
|
+
// per spike-report-wasm-api.md §3: formatPolicies takes FormattingCall object, not raw string
|
|
16
|
+
const answer = formatPolicies({
|
|
17
|
+
policyText: input.policies,
|
|
18
|
+
...(input.line_width !== undefined ? { lineWidth: input.line_width } : {}),
|
|
19
|
+
...(input.indent_width !== undefined ? { indentWidth: input.indent_width } : {}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (answer.type === "failure") {
|
|
23
|
+
return {
|
|
24
|
+
formatted: null,
|
|
25
|
+
error: answer.errors.map((e) => e.message).join("; "),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
formatted: answer.formatted_policy,
|
|
31
|
+
error: null,
|
|
32
|
+
};
|
|
33
|
+
}
|