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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the stdio entry-point helpers in src/index.ts. Tests that
|
|
3
|
+
* need a real spawned process live in test/integration/smoke.test.ts
|
|
4
|
+
* (S6, S6b, S6c). These exercise the helpers directly.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { basename, join } from "node:path";
|
|
10
|
+
import { populateCwdFallback } from "../src/index.js";
|
|
11
|
+
import { storeManager } from "../src/resources/store-manager.js";
|
|
12
|
+
|
|
13
|
+
describe("populateCwdFallback — kickoff-12 sync cwd-fallback (Round 5 Scenario E fix)", () => {
|
|
14
|
+
const tempDirs: string[] = [];
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset the singleton so a polluted state from a prior test or a
|
|
18
|
+
// file-parallel sibling cannot mask a real bug here. 12b audit
|
|
19
|
+
// Finding 3 (test fragility).
|
|
20
|
+
storeManager.loadFromRoots([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
storeManager.loadFromRoots([]);
|
|
25
|
+
while (tempDirs.length > 0) {
|
|
26
|
+
const dir = tempDirs.pop()!;
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns a root descriptor and loads the store when cwd has schema.cedarschema", () => {
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), "cedar-sync-schema-"));
|
|
33
|
+
tempDirs.push(dir);
|
|
34
|
+
writeFileSync(join(dir, "schema.cedarschema"), `namespace DocMgmt {}`);
|
|
35
|
+
|
|
36
|
+
const loaded = populateCwdFallback(dir);
|
|
37
|
+
|
|
38
|
+
expect(loaded).toEqual({ uri: `file://${dir}`, name: basename(dir) });
|
|
39
|
+
expect(storeManager.listStoreNames()).toEqual([basename(dir)]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns a root descriptor and loads the store when cwd has schema.json", () => {
|
|
43
|
+
const dir = mkdtempSync(join(tmpdir(), "cedar-sync-json-"));
|
|
44
|
+
tempDirs.push(dir);
|
|
45
|
+
writeFileSync(join(dir, "schema.json"), `{"DocMgmt":{}}`);
|
|
46
|
+
|
|
47
|
+
expect(populateCwdFallback(dir)).not.toBeNull();
|
|
48
|
+
expect(storeManager.listStoreNames()).toEqual([basename(dir)]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns a root descriptor and loads the store when cwd has only a policies/ directory (no schema)", () => {
|
|
52
|
+
// This matches the 11d-audit Probe B scenario. populateCwdFallback fires;
|
|
53
|
+
// cedar_advise's downstream auto-resolve then handles the schemaless case
|
|
54
|
+
// via the `not_provided` degrade path landed in commit 0f35740.
|
|
55
|
+
const dir = mkdtempSync(join(tmpdir(), "cedar-sync-policiesonly-"));
|
|
56
|
+
tempDirs.push(dir);
|
|
57
|
+
mkdirSync(join(dir, "policies"));
|
|
58
|
+
|
|
59
|
+
expect(populateCwdFallback(dir)).not.toBeNull();
|
|
60
|
+
expect(storeManager.listStoreNames()).toEqual([basename(dir)]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns null and leaves StoreManager untouched when cwd does not look like a Cedar workspace", () => {
|
|
64
|
+
const dir = mkdtempSync(join(tmpdir(), "cedar-sync-empty-"));
|
|
65
|
+
tempDirs.push(dir);
|
|
66
|
+
// Empty directory — no schema.cedarschema, no schema.json, no policies/.
|
|
67
|
+
|
|
68
|
+
expect(populateCwdFallback(dir)).toBeNull();
|
|
69
|
+
expect(storeManager.listStoreNames()).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("populates StoreManager synchronously (caller can observe state immediately on the next line)", () => {
|
|
73
|
+
// The whole point of 12a: the call returns AFTER state is mutated, with
|
|
74
|
+
// no async gap. A caller reading listStoreNames() on the next statement
|
|
75
|
+
// must see the populated store. If populateCwdFallback ever becomes
|
|
76
|
+
// async, every caller that relies on this property breaks.
|
|
77
|
+
const dir = mkdtempSync(join(tmpdir(), "cedar-sync-immediate-"));
|
|
78
|
+
tempDirs.push(dir);
|
|
79
|
+
writeFileSync(join(dir, "schema.cedarschema"), `namespace DocMgmt {}`);
|
|
80
|
+
|
|
81
|
+
populateCwdFallback(dir);
|
|
82
|
+
// Immediate read — no awaits, no microtask boundary.
|
|
83
|
+
const names = storeManager.listStoreNames();
|
|
84
|
+
|
|
85
|
+
expect(names).toEqual([basename(dir)]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns null when cwd is the filesystem root (12b audit Finding 2 — sandbox-bypass guard)", () => {
|
|
89
|
+
// cwd === "/" → store.path would normalize to "" → isPathAllowed would
|
|
90
|
+
// return true for every filesystem path (because `"<anything>".startsWith("")`
|
|
91
|
+
// is true). populateCwdFallback rejects this case explicitly; StoreManager
|
|
92
|
+
// also rejects empty-rawPath roots as a second defense layer.
|
|
93
|
+
expect(populateCwdFallback("/")).toBeNull();
|
|
94
|
+
expect(storeManager.listStoreNames()).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E layer 2: cross-tool behavior invariants.
|
|
3
|
+
*
|
|
4
|
+
* Invariants that span multiple tools end-to-end through the MCP protocol.
|
|
5
|
+
* Each test asserts a property the tool surface MUST satisfy, not a single
|
|
6
|
+
* happy-path output. If two tools disagree on the same input, the invariant
|
|
7
|
+
* surfaces it.
|
|
8
|
+
*
|
|
9
|
+
* Transport: stdio (same rationale as protocol.test.ts).
|
|
10
|
+
*
|
|
11
|
+
* Run: npx vitest run test/integration/e2e/behavior
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
16
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
17
|
+
|
|
18
|
+
const repoRoot = join(import.meta.dirname, "../../..");
|
|
19
|
+
|
|
20
|
+
function makeStdioClient(): { client: Client; transport: StdioClientTransport } {
|
|
21
|
+
const transport = new StdioClientTransport({
|
|
22
|
+
command: "npx",
|
|
23
|
+
args: ["tsx", "src/index.ts"],
|
|
24
|
+
cwd: repoRoot,
|
|
25
|
+
stderr: "pipe",
|
|
26
|
+
});
|
|
27
|
+
const client = new Client(
|
|
28
|
+
{ name: "e2e-behavior", version: "1.0.0" },
|
|
29
|
+
{ capabilities: {} }
|
|
30
|
+
);
|
|
31
|
+
return { client, transport };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseToolResult(result: unknown): unknown {
|
|
35
|
+
const r = result as { content?: Array<{ type: string; text?: string }> };
|
|
36
|
+
const textBlock = r.content?.find((b) => b.type === "text");
|
|
37
|
+
if (!textBlock?.text) throw new Error("No text content in tool result");
|
|
38
|
+
return JSON.parse(textBlock.text);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SCHEMA_JSON = JSON.stringify({
|
|
42
|
+
DocMgmt: {
|
|
43
|
+
entityTypes: {
|
|
44
|
+
User: {
|
|
45
|
+
memberOfTypes: ["Role"],
|
|
46
|
+
shape: {
|
|
47
|
+
type: "Record",
|
|
48
|
+
attributes: {
|
|
49
|
+
name: { type: "String", required: true },
|
|
50
|
+
email: { type: "String", required: true },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
Role: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
|
|
55
|
+
Document: {
|
|
56
|
+
memberOfTypes: ["Folder"],
|
|
57
|
+
shape: {
|
|
58
|
+
type: "Record",
|
|
59
|
+
attributes: {
|
|
60
|
+
owner: { type: "String", required: true },
|
|
61
|
+
classification: { type: "String", required: true },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
Folder: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
|
|
66
|
+
},
|
|
67
|
+
actions: {
|
|
68
|
+
read: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"], context: { type: "Record", attributes: {} } } },
|
|
69
|
+
write: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"], context: { type: "Record", attributes: {} } } },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const ADMIN_POLICY = `permit (
|
|
75
|
+
principal in DocMgmt::Role::"admin",
|
|
76
|
+
action,
|
|
77
|
+
resource
|
|
78
|
+
);`;
|
|
79
|
+
|
|
80
|
+
const VIEWER_POLICY = `permit (
|
|
81
|
+
principal in DocMgmt::Role::"viewer",
|
|
82
|
+
action == DocMgmt::Action::"read",
|
|
83
|
+
resource
|
|
84
|
+
)
|
|
85
|
+
when {
|
|
86
|
+
resource.classification == "public"
|
|
87
|
+
};`;
|
|
88
|
+
|
|
89
|
+
describe("e2e behavior", () => {
|
|
90
|
+
let client: Client | undefined;
|
|
91
|
+
let transport: StdioClientTransport | undefined;
|
|
92
|
+
|
|
93
|
+
beforeEach(async () => {
|
|
94
|
+
const conn = makeStdioClient();
|
|
95
|
+
client = conn.client;
|
|
96
|
+
transport = conn.transport;
|
|
97
|
+
await client.connect(transport);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(async () => {
|
|
101
|
+
try { await client?.close(); } catch { /* ignore */ }
|
|
102
|
+
try { await transport?.close(); } catch { /* ignore */ }
|
|
103
|
+
client = undefined;
|
|
104
|
+
transport = undefined;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("B1 — translate round-trip preserves validation: text→json→text validates the same", async () => {
|
|
108
|
+
// Invariant: if a policy validates against a schema, translating it to JSON and
|
|
109
|
+
// back to text should produce a policy that ALSO validates against the same schema.
|
|
110
|
+
// Failure case: AST round-trip drops a condition clause, validation passes "before"
|
|
111
|
+
// but fails "after" (or vice versa). Catches lossy translations.
|
|
112
|
+
|
|
113
|
+
const original = ADMIN_POLICY;
|
|
114
|
+
|
|
115
|
+
const toJsonRaw = await client!.callTool({
|
|
116
|
+
name: "cedar_translate",
|
|
117
|
+
arguments: { input: original, type: "policy", direction: "to_json" },
|
|
118
|
+
});
|
|
119
|
+
const toJson = parseToolResult(toJsonRaw) as { output: string; error: null };
|
|
120
|
+
expect(toJson.error).toBeNull();
|
|
121
|
+
|
|
122
|
+
const toCedarRaw = await client!.callTool({
|
|
123
|
+
name: "cedar_translate",
|
|
124
|
+
arguments: { input: toJson.output, type: "policy", direction: "to_cedar" },
|
|
125
|
+
});
|
|
126
|
+
const toCedar = parseToolResult(toCedarRaw) as { output: string; error: null };
|
|
127
|
+
expect(toCedar.error).toBeNull();
|
|
128
|
+
|
|
129
|
+
const validateOriginal = parseToolResult(
|
|
130
|
+
await client!.callTool({ name: "cedar_validate", arguments: { policies: original, schema: SCHEMA_JSON } })
|
|
131
|
+
) as { valid: boolean };
|
|
132
|
+
const validateRoundtrip = parseToolResult(
|
|
133
|
+
await client!.callTool({ name: "cedar_validate", arguments: { policies: toCedar.output, schema: SCHEMA_JSON } })
|
|
134
|
+
) as { valid: boolean };
|
|
135
|
+
|
|
136
|
+
expect(validateOriginal.valid).toBe(validateRoundtrip.valid);
|
|
137
|
+
expect(validateOriginal.valid).toBe(true);
|
|
138
|
+
}, 30_000);
|
|
139
|
+
|
|
140
|
+
it("B2 — format idempotency: format(format(P)) === format(P)", async () => {
|
|
141
|
+
// Invariant: formatting a policy twice produces the same output as formatting once.
|
|
142
|
+
// Failure case: an inconsistent formatter that adds/removes whitespace differently
|
|
143
|
+
// on successive runs would break diff tools and CI checks.
|
|
144
|
+
const firstRaw = await client!.callTool({ name: "cedar_format", arguments: { policies: VIEWER_POLICY } });
|
|
145
|
+
const first = parseToolResult(firstRaw) as { formatted: string };
|
|
146
|
+
|
|
147
|
+
const secondRaw = await client!.callTool({ name: "cedar_format", arguments: { policies: first.formatted } });
|
|
148
|
+
const second = parseToolResult(secondRaw) as { formatted: string };
|
|
149
|
+
|
|
150
|
+
expect(second.formatted).toBe(first.formatted);
|
|
151
|
+
}, 30_000);
|
|
152
|
+
|
|
153
|
+
it("B3 — generate-then-authorize agrees: when generator claims Allow, cedar_authorize confirms Allow", async () => {
|
|
154
|
+
// Invariant: if cedar_generate_sample_request claims ready_to_test:true and
|
|
155
|
+
// decision:"Allow", then cedar_authorize on the same request MUST also return Allow.
|
|
156
|
+
// If the generator is unable to construct a satisfying request (ready_to_test:false),
|
|
157
|
+
// that's a known limitation we don't fail on — but if it CLAIMS success and the
|
|
158
|
+
// independent authorize disagrees, that's a real product bug.
|
|
159
|
+
const genRaw = await client!.callTool({
|
|
160
|
+
name: "cedar_generate_sample_request",
|
|
161
|
+
arguments: { policy: ADMIN_POLICY, schema: SCHEMA_JSON, target_decision: "allow" },
|
|
162
|
+
});
|
|
163
|
+
const gen = parseToolResult(genRaw) as {
|
|
164
|
+
principal: string;
|
|
165
|
+
action: string;
|
|
166
|
+
resource: string;
|
|
167
|
+
entities: unknown[];
|
|
168
|
+
decision?: "Allow" | "Deny";
|
|
169
|
+
ready_to_test?: boolean;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (gen.ready_to_test === false || gen.decision !== "Allow") {
|
|
173
|
+
// eslint-disable-next-line no-console
|
|
174
|
+
console.warn("B3 — generator did not claim success for target=allow (ready_to_test=" + gen.ready_to_test + ", decision=" + gen.decision + "); skipping cross-tool assertion.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const authRaw = await client!.callTool({
|
|
179
|
+
name: "cedar_authorize",
|
|
180
|
+
arguments: {
|
|
181
|
+
policies: ADMIN_POLICY,
|
|
182
|
+
principal: gen.principal,
|
|
183
|
+
action: gen.action,
|
|
184
|
+
resource: gen.resource,
|
|
185
|
+
entities: JSON.stringify(gen.entities),
|
|
186
|
+
schema: SCHEMA_JSON,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const auth = parseToolResult(authRaw) as { decision: string };
|
|
190
|
+
expect(auth.decision).toBe("Allow");
|
|
191
|
+
}, 30_000);
|
|
192
|
+
|
|
193
|
+
// B4 exercises the skip path: the generator self-reports ready_to_test:false +
|
|
194
|
+
// decision:"Allow" for target=deny on the ADMIN policy (which permits everything for
|
|
195
|
+
// admin role — no satisfying deny exists for an admin principal). The skip is the
|
|
196
|
+
// correct behavior; the assertion is vacuously true. If the generator one day
|
|
197
|
+
// produces a satisfying deny request, the .skip path would not fire and we'd assert
|
|
198
|
+
// Deny against authorize like B3 does.
|
|
199
|
+
it("B4 — generate-then-authorize agrees: when generator claims Deny, cedar_authorize confirms Deny", async () => {
|
|
200
|
+
// Symmetric invariant for the deny path. Same skip pattern as B3 when the
|
|
201
|
+
// generator self-reports it couldn't produce a satisfying example.
|
|
202
|
+
const genRaw = await client!.callTool({
|
|
203
|
+
name: "cedar_generate_sample_request",
|
|
204
|
+
arguments: { policy: ADMIN_POLICY, schema: SCHEMA_JSON, target_decision: "deny" },
|
|
205
|
+
});
|
|
206
|
+
const gen = parseToolResult(genRaw) as {
|
|
207
|
+
principal: string;
|
|
208
|
+
action: string;
|
|
209
|
+
resource: string;
|
|
210
|
+
entities: unknown[];
|
|
211
|
+
decision?: "Allow" | "Deny";
|
|
212
|
+
ready_to_test?: boolean;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (gen.ready_to_test === false || gen.decision !== "Deny") {
|
|
216
|
+
// eslint-disable-next-line no-console
|
|
217
|
+
console.warn("B4 — generator did not claim success for target=deny (ready_to_test=" + gen.ready_to_test + ", decision=" + gen.decision + "); skipping cross-tool assertion.");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const authRaw = await client!.callTool({
|
|
222
|
+
name: "cedar_authorize",
|
|
223
|
+
arguments: {
|
|
224
|
+
policies: ADMIN_POLICY,
|
|
225
|
+
principal: gen.principal,
|
|
226
|
+
action: gen.action,
|
|
227
|
+
resource: gen.resource,
|
|
228
|
+
entities: JSON.stringify(gen.entities),
|
|
229
|
+
schema: SCHEMA_JSON,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
const auth = parseToolResult(authRaw) as { decision: string };
|
|
233
|
+
|
|
234
|
+
expect(auth.decision).toBe("Deny");
|
|
235
|
+
}, 30_000);
|
|
236
|
+
|
|
237
|
+
it("B5 — diff_schema identity: diffing a schema against itself reports no changes, risk safe", async () => {
|
|
238
|
+
// Invariant: any diff function applied to two equal inputs MUST report zero changes.
|
|
239
|
+
// Failure case: a normalizer that introduces spurious differences (e.g., __cedar:: prefix
|
|
240
|
+
// stripping that's asymmetric) would flag identical schemas as different. We tested this
|
|
241
|
+
// explicitly in the unit suite but the e2e path adds the MCP protocol layer on top —
|
|
242
|
+
// a serialization bug could re-emerge here.
|
|
243
|
+
const raw = await client!.callTool({
|
|
244
|
+
name: "cedar_diff_schema",
|
|
245
|
+
arguments: { blue: SCHEMA_JSON, green: SCHEMA_JSON },
|
|
246
|
+
});
|
|
247
|
+
const result = parseToolResult(raw) as {
|
|
248
|
+
risk_level: string;
|
|
249
|
+
entity_types: { added: unknown[]; removed: unknown[]; modified: unknown[] };
|
|
250
|
+
actions: { added: unknown[]; removed: unknown[]; modified: unknown[] };
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
expect(result.risk_level).toBe("safe");
|
|
254
|
+
expect(result.entity_types.added).toHaveLength(0);
|
|
255
|
+
expect(result.entity_types.removed).toHaveLength(0);
|
|
256
|
+
expect(result.entity_types.modified).toHaveLength(0);
|
|
257
|
+
expect(result.actions.added).toHaveLength(0);
|
|
258
|
+
expect(result.actions.removed).toHaveLength(0);
|
|
259
|
+
expect(result.actions.modified).toHaveLength(0);
|
|
260
|
+
}, 20_000);
|
|
261
|
+
|
|
262
|
+
it("B6 — check_policy_change identity: comparing a policy against itself reports no changes", async () => {
|
|
263
|
+
// Invariant: cedar_check_policy_change(P, P) must say can_update_in_place: true
|
|
264
|
+
// with no changes. Failure case: an over-eager change detector flagging whitespace
|
|
265
|
+
// or comment differences as semantic changes would generate noisy AVP recommendations.
|
|
266
|
+
const raw = await client!.callTool({
|
|
267
|
+
name: "cedar_check_policy_change",
|
|
268
|
+
arguments: { old_policy: VIEWER_POLICY, new_policy: VIEWER_POLICY },
|
|
269
|
+
});
|
|
270
|
+
const result = parseToolResult(raw) as {
|
|
271
|
+
can_update_in_place: boolean;
|
|
272
|
+
changes: unknown[];
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
expect(result.can_update_in_place).toBe(true);
|
|
276
|
+
expect(result.changes).toHaveLength(0);
|
|
277
|
+
}, 20_000);
|
|
278
|
+
|
|
279
|
+
it("B7 — validate + authorize agree on schema-level errors", async () => {
|
|
280
|
+
// Invariant: if cedar_validate flags a policy as invalid against a schema (e.g.,
|
|
281
|
+
// references a missing attribute), then cedar_authorize with validateRequest=true
|
|
282
|
+
// on the same policy+schema should ALSO surface the issue (as a request-level
|
|
283
|
+
// error or as a Deny). The two tools must not disagree about schema validity.
|
|
284
|
+
const badPolicy = `permit (principal, action, resource) when { resource.nonexistent_attr == "x" };`;
|
|
285
|
+
|
|
286
|
+
const validateRaw = await client!.callTool({
|
|
287
|
+
name: "cedar_validate",
|
|
288
|
+
arguments: { policies: badPolicy, schema: SCHEMA_JSON },
|
|
289
|
+
});
|
|
290
|
+
const validate = parseToolResult(validateRaw) as { valid: boolean; errors: Array<{ message: string }> };
|
|
291
|
+
|
|
292
|
+
expect(validate.valid).toBe(false);
|
|
293
|
+
expect(validate.errors.length).toBeGreaterThan(0);
|
|
294
|
+
expect(validate.errors[0].message).toContain("nonexistent_attr");
|
|
295
|
+
|
|
296
|
+
// Now run an authorize with this same policy. Cedar's behavior: a policy that
|
|
297
|
+
// references a missing attribute evaluates to false (silently skipped) at
|
|
298
|
+
// authorize time. So the request returns Deny (no permit applies). The
|
|
299
|
+
// INVARIANT here is "they don't disagree about the policy being broken" —
|
|
300
|
+
// validate says invalid, authorize returns the default-deny. If authorize
|
|
301
|
+
// returned Allow for this policy, it would prove they disagree.
|
|
302
|
+
const authRaw = await client!.callTool({
|
|
303
|
+
name: "cedar_authorize",
|
|
304
|
+
arguments: {
|
|
305
|
+
policies: badPolicy,
|
|
306
|
+
principal: 'DocMgmt::User::"alice"',
|
|
307
|
+
action: 'DocMgmt::Action::"read"',
|
|
308
|
+
resource: 'DocMgmt::Document::"d1"',
|
|
309
|
+
entities: JSON.stringify([
|
|
310
|
+
{ uid: { type: "DocMgmt::User", id: "alice" }, attrs: { name: "Alice", email: "a@b.c" }, parents: [] },
|
|
311
|
+
{ uid: { type: "DocMgmt::Document", id: "d1" }, attrs: { owner: "alice", classification: "public" }, parents: [] },
|
|
312
|
+
]),
|
|
313
|
+
schema: SCHEMA_JSON,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
const auth = parseToolResult(authRaw) as { decision: string };
|
|
317
|
+
|
|
318
|
+
expect(auth.decision).toBe("Deny");
|
|
319
|
+
}, 30_000);
|
|
320
|
+
|
|
321
|
+
it("B8 — validate_template + link_template + validate consistency", async () => {
|
|
322
|
+
// Invariant: if a template validates against a schema, then linking it with
|
|
323
|
+
// valid slot values must produce a policy that ALSO validates. Catches lossy
|
|
324
|
+
// template-linking implementations.
|
|
325
|
+
const template = `permit (
|
|
326
|
+
principal == ?principal,
|
|
327
|
+
action == DocMgmt::Action::"read",
|
|
328
|
+
resource == ?resource
|
|
329
|
+
);`;
|
|
330
|
+
|
|
331
|
+
const validateTplRaw = await client!.callTool({
|
|
332
|
+
name: "cedar_validate_template",
|
|
333
|
+
arguments: { template, schema: SCHEMA_JSON },
|
|
334
|
+
});
|
|
335
|
+
const validateTpl = parseToolResult(validateTplRaw) as { valid: boolean };
|
|
336
|
+
expect(validateTpl.valid).toBe(true);
|
|
337
|
+
|
|
338
|
+
const linkRaw = await client!.callTool({
|
|
339
|
+
name: "cedar_link_template",
|
|
340
|
+
arguments: {
|
|
341
|
+
template,
|
|
342
|
+
principal: 'DocMgmt::User::"alice"',
|
|
343
|
+
resource: 'DocMgmt::Document::"d1"',
|
|
344
|
+
schema: SCHEMA_JSON,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
const link = parseToolResult(linkRaw) as { linked_policy?: string; policy?: string; valid?: boolean };
|
|
348
|
+
const linkedPolicy = link.linked_policy ?? link.policy;
|
|
349
|
+
expect(linkedPolicy).toBeTruthy();
|
|
350
|
+
|
|
351
|
+
// The linked output should pass cedar_validate as a regular static policy
|
|
352
|
+
const validateLinkedRaw = await client!.callTool({
|
|
353
|
+
name: "cedar_validate",
|
|
354
|
+
arguments: { policies: linkedPolicy!, schema: SCHEMA_JSON },
|
|
355
|
+
});
|
|
356
|
+
const validateLinked = parseToolResult(validateLinkedRaw) as { valid: boolean };
|
|
357
|
+
expect(validateLinked.valid).toBe(true);
|
|
358
|
+
}, 30_000);
|
|
359
|
+
});
|