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,151 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { handleListTemplates } from "../../src/tools/list-templates.js";
|
|
6
|
+
import { handleListTemplateLinks } from "../../src/tools/list-template-links.js";
|
|
7
|
+
import { StoreManager } from "../../src/resources/store-manager.js";
|
|
8
|
+
|
|
9
|
+
const TEMPLATE_A = `permit(principal == ?principal, action == App::Action::"read", resource == ?resource);`;
|
|
10
|
+
const TEMPLATE_B = `permit(principal, action == App::Action::"write", resource == ?resource);`;
|
|
11
|
+
|
|
12
|
+
function makeStore(baseDir: string, opts: {
|
|
13
|
+
templates?: Record<string, string>;
|
|
14
|
+
links?: Record<string, object>;
|
|
15
|
+
} = {}): { manager: StoreManager; storePath: string } {
|
|
16
|
+
const storePath = join(baseDir, "mystore");
|
|
17
|
+
mkdirSync(join(storePath, "policies"), { recursive: true });
|
|
18
|
+
writeFileSync(join(storePath, "schema.cedarschema"), "namespace App {}");
|
|
19
|
+
|
|
20
|
+
if (opts.templates) {
|
|
21
|
+
mkdirSync(join(storePath, "templates"), { recursive: true });
|
|
22
|
+
for (const [id, content] of Object.entries(opts.templates)) {
|
|
23
|
+
writeFileSync(join(storePath, "templates", `${id}.cedar`), content);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (opts.links) {
|
|
28
|
+
mkdirSync(join(storePath, "template-links"), { recursive: true });
|
|
29
|
+
for (const [id, data] of Object.entries(opts.links)) {
|
|
30
|
+
writeFileSync(join(storePath, "template-links", `${id}.json`), JSON.stringify(data));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const manager = new StoreManager();
|
|
35
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "mystore" }]);
|
|
36
|
+
return { manager, storePath };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("cedar_list_templates", () => {
|
|
40
|
+
let tmpDir: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
tmpDir = join(tmpdir(), `cedar-list-tmpl-${Date.now()}`);
|
|
44
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("LST1 — lists all templates in store with id and slot info", async () => {
|
|
52
|
+
const { manager } = makeStore(tmpDir, {
|
|
53
|
+
templates: { "viewer-access": TEMPLATE_A, "writer-access": TEMPLATE_B },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await handleListTemplates({ store: "mystore" }, manager);
|
|
57
|
+
|
|
58
|
+
expect(result.error).toBeUndefined();
|
|
59
|
+
expect(result.store).toBe("mystore");
|
|
60
|
+
expect(result.templates).toHaveLength(2);
|
|
61
|
+
const ids = result.templates.map(t => t.id);
|
|
62
|
+
expect(ids).toContain("viewer-access");
|
|
63
|
+
expect(ids).toContain("writer-access");
|
|
64
|
+
const viewerTmpl = result.templates.find(t => t.id === "viewer-access")!;
|
|
65
|
+
expect(viewerTmpl.slots).toContain("?principal");
|
|
66
|
+
expect(viewerTmpl.slots).toContain("?resource");
|
|
67
|
+
const writerTmpl = result.templates.find(t => t.id === "writer-access")!;
|
|
68
|
+
expect(writerTmpl.slots).toContain("?resource");
|
|
69
|
+
expect(writerTmpl.slots).not.toContain("?principal");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("LST2 — returns empty list when no templates directory", async () => {
|
|
73
|
+
const { manager } = makeStore(tmpDir);
|
|
74
|
+
|
|
75
|
+
const result = await handleListTemplates({ store: "mystore" }, manager);
|
|
76
|
+
|
|
77
|
+
expect(result.error).toBeUndefined();
|
|
78
|
+
expect(result.templates).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("LST3 — returns error for unknown store", async () => {
|
|
82
|
+
const { manager } = makeStore(tmpDir);
|
|
83
|
+
|
|
84
|
+
const result = await handleListTemplates({ store: "nonexistent" }, manager);
|
|
85
|
+
|
|
86
|
+
expect(result.error).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("cedar_list_template_links", () => {
|
|
91
|
+
let tmpDir: string;
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
tmpDir = join(tmpdir(), `cedar-list-links-${Date.now()}`);
|
|
95
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("LSTL1 — lists template links with template_id and slot_values", async () => {
|
|
103
|
+
const { manager } = makeStore(tmpDir, {
|
|
104
|
+
links: {
|
|
105
|
+
"alice-doc42": { template_id: "viewer-access", slot_values: { "?principal": 'App::User::"alice"', "?resource": 'App::Document::"doc-42"' } },
|
|
106
|
+
"bob-doc99": { template_id: "viewer-access", slot_values: { "?principal": 'App::User::"bob"', "?resource": 'App::Document::"doc-99"' } },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await handleListTemplateLinks({ store: "mystore" }, manager);
|
|
111
|
+
|
|
112
|
+
expect(result.error).toBeUndefined();
|
|
113
|
+
expect(result.store).toBe("mystore");
|
|
114
|
+
expect(result.links).toHaveLength(2);
|
|
115
|
+
const alice = result.links.find(l => l.id === "alice-doc42")!;
|
|
116
|
+
expect(alice.template_id).toBe("viewer-access");
|
|
117
|
+
expect(alice.slot_values["?principal"]).toBe('App::User::"alice"');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("LSTL2 — returns empty list when no template-links directory", async () => {
|
|
121
|
+
const { manager } = makeStore(tmpDir);
|
|
122
|
+
|
|
123
|
+
const result = await handleListTemplateLinks({ store: "mystore" }, manager);
|
|
124
|
+
|
|
125
|
+
expect(result.error).toBeUndefined();
|
|
126
|
+
expect(result.links).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("LSTL3 — returns error for unknown store", async () => {
|
|
130
|
+
const { manager } = makeStore(tmpDir);
|
|
131
|
+
|
|
132
|
+
const result = await handleListTemplateLinks({ store: "nonexistent" }, manager);
|
|
133
|
+
|
|
134
|
+
expect(result.error).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("LSTL4 — malformed link JSON surfaces as structured error not unhandled throw", async () => {
|
|
138
|
+
const { manager, storePath } = makeStore(tmpDir, {
|
|
139
|
+
links: { "good-link": { template_id: "t", slot_values: {} } },
|
|
140
|
+
});
|
|
141
|
+
// Overwrite with malformed JSON after store is loaded
|
|
142
|
+
writeFileSync(join(storePath, "template-links", "bad-link.json"), "{{{not json");
|
|
143
|
+
|
|
144
|
+
// Reload to pick up the new file
|
|
145
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "mystore" }]);
|
|
146
|
+
const result = await handleListTemplateLinks({ store: "mystore" }, manager);
|
|
147
|
+
|
|
148
|
+
expect(result.error).toBeDefined();
|
|
149
|
+
expect(result.error).toMatch(/bad-link/);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { handleTranslate } from "../../src/tools/translate.js";
|
|
3
|
+
import { SCHEMA_JSON } from "../fixtures/docmgmt.js";
|
|
4
|
+
|
|
5
|
+
const SINGLE_POLICY = `permit (
|
|
6
|
+
principal in DocMgmt::Role::"admin",
|
|
7
|
+
action,
|
|
8
|
+
resource
|
|
9
|
+
);`;
|
|
10
|
+
|
|
11
|
+
describe("cedar_translate", () => {
|
|
12
|
+
describe("policy translation", () => {
|
|
13
|
+
it("translates Cedar policy text to JSON", async () => {
|
|
14
|
+
const result = await handleTranslate({
|
|
15
|
+
input: SINGLE_POLICY,
|
|
16
|
+
type: "policy",
|
|
17
|
+
direction: "to_json",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(result.error).toBeNull();
|
|
21
|
+
const json = JSON.parse(result.output!);
|
|
22
|
+
expect(json.effect).toBe("permit");
|
|
23
|
+
expect(json.principal.op).toBe("in");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("translates policy JSON back to Cedar text", async () => {
|
|
27
|
+
const policyJson = {
|
|
28
|
+
effect: "permit",
|
|
29
|
+
principal: { op: "in", entity: { type: "DocMgmt::Role", id: "admin" } },
|
|
30
|
+
action: { op: "All" },
|
|
31
|
+
resource: { op: "All" },
|
|
32
|
+
conditions: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const result = await handleTranslate({
|
|
36
|
+
input: JSON.stringify(policyJson),
|
|
37
|
+
type: "policy",
|
|
38
|
+
direction: "to_cedar",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result.error).toBeNull();
|
|
42
|
+
expect(result.output).toContain("permit");
|
|
43
|
+
expect(result.output).toContain('DocMgmt::Role::"admin"');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("schema translation", () => {
|
|
48
|
+
it("translates Cedar schema JSON to Cedar text", async () => {
|
|
49
|
+
const result = await handleTranslate({
|
|
50
|
+
input: JSON.stringify(SCHEMA_JSON),
|
|
51
|
+
type: "schema",
|
|
52
|
+
direction: "to_cedar",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result.error).toBeNull();
|
|
56
|
+
expect(result.output).toContain("namespace DocMgmt");
|
|
57
|
+
expect(result.output).toContain("entity User");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("translates Cedar schema text to JSON", async () => {
|
|
61
|
+
const cedarSchema = `namespace DocMgmt {
|
|
62
|
+
entity User in [Role] = { name: String, email: String };
|
|
63
|
+
entity Role;
|
|
64
|
+
action read appliesTo { principal: [User], resource: [User], context: {} };
|
|
65
|
+
}`;
|
|
66
|
+
|
|
67
|
+
const result = await handleTranslate({
|
|
68
|
+
input: cedarSchema,
|
|
69
|
+
type: "schema",
|
|
70
|
+
direction: "to_json",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.error).toBeNull();
|
|
74
|
+
const json = JSON.parse(result.output!);
|
|
75
|
+
expect(json.DocMgmt).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns error for invalid input", async () => {
|
|
79
|
+
const result = await handleTranslate({
|
|
80
|
+
input: "this is not a policy",
|
|
81
|
+
type: "policy",
|
|
82
|
+
direction: "to_json",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result.error).not.toBeNull();
|
|
86
|
+
expect(result.output).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { handleValidateEntities } from "../../src/tools/validate-entities.js";
|
|
3
|
+
import { SCHEMA_JSON, ENTITIES } from "../fixtures/docmgmt.js";
|
|
4
|
+
|
|
5
|
+
const SCHEMA_STR = JSON.stringify(SCHEMA_JSON);
|
|
6
|
+
|
|
7
|
+
describe("cedar_validate_entities", () => {
|
|
8
|
+
it("VE1: valid entities + schema → valid:true, no errors", async () => {
|
|
9
|
+
const result = await handleValidateEntities({
|
|
10
|
+
entities: JSON.stringify(ENTITIES),
|
|
11
|
+
schema: SCHEMA_STR,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(result.valid).toBe(true);
|
|
15
|
+
expect(result.errors).toHaveLength(0);
|
|
16
|
+
expect(result.entity_count).toBe(ENTITIES.length);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("VE2: entity of unknown type → unknown_type error with entity_uid", async () => {
|
|
20
|
+
const bad = [
|
|
21
|
+
{ uid: { type: "DocMgmt::Unicorn", id: "rainbow" }, attrs: {}, parents: [] },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const result = await handleValidateEntities({
|
|
25
|
+
entities: JSON.stringify(bad),
|
|
26
|
+
schema: SCHEMA_STR,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(result.valid).toBe(false);
|
|
30
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
31
|
+
const e = result.errors[0];
|
|
32
|
+
expect(e.error_kind).toBe("unknown_type");
|
|
33
|
+
expect(e.entity_uid).toContain("DocMgmt::Unicorn");
|
|
34
|
+
expect(e.entity_uid).toContain("rainbow");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("VE3: missing required attribute → missing_required_attribute error", async () => {
|
|
38
|
+
const bad = [
|
|
39
|
+
{ uid: { type: "DocMgmt::User", id: "alice" }, attrs: { name: "Alice" }, parents: [] },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const result = await handleValidateEntities({
|
|
43
|
+
entities: JSON.stringify(bad),
|
|
44
|
+
schema: SCHEMA_STR,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.valid).toBe(false);
|
|
48
|
+
const e = result.errors[0];
|
|
49
|
+
expect(e.error_kind).toBe("missing_required_attribute");
|
|
50
|
+
expect(e.attribute).toBe("email");
|
|
51
|
+
expect(e.entity_uid).toContain("alice");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("VE4: wrong attribute type → type_mismatch error", async () => {
|
|
55
|
+
const bad = [
|
|
56
|
+
{
|
|
57
|
+
uid: { type: "DocMgmt::User", id: "alice" },
|
|
58
|
+
attrs: { name: 42, email: "alice@example.com" },
|
|
59
|
+
parents: [],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const result = await handleValidateEntities({
|
|
64
|
+
entities: JSON.stringify(bad),
|
|
65
|
+
schema: SCHEMA_STR,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result.valid).toBe(false);
|
|
69
|
+
const e = result.errors[0];
|
|
70
|
+
expect(e.error_kind).toBe("type_mismatch");
|
|
71
|
+
expect(e.attribute).toBe("name");
|
|
72
|
+
expect(e.entity_uid).toContain("alice");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("VE5: unknown attribute → unknown_attribute error", async () => {
|
|
76
|
+
const bad = [
|
|
77
|
+
{
|
|
78
|
+
uid: { type: "DocMgmt::User", id: "alice" },
|
|
79
|
+
attrs: { name: "Alice", email: "a@b.c", bogus: "x" },
|
|
80
|
+
parents: [],
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const result = await handleValidateEntities({
|
|
85
|
+
entities: JSON.stringify(bad),
|
|
86
|
+
schema: SCHEMA_STR,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.valid).toBe(false);
|
|
90
|
+
const e = result.errors[0];
|
|
91
|
+
expect(e.error_kind).toBe("unknown_attribute");
|
|
92
|
+
expect(e.attribute).toBe("bogus");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("VE6: entity with parent of unknown/disallowed type → disallowed_parent_type error", async () => {
|
|
96
|
+
const bad = [
|
|
97
|
+
{
|
|
98
|
+
uid: { type: "DocMgmt::User", id: "alice" },
|
|
99
|
+
attrs: { name: "Alice", email: "a@b.c" },
|
|
100
|
+
parents: [{ type: "DocMgmt::Unicorn", id: "rainbow" }],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const result = await handleValidateEntities({
|
|
105
|
+
entities: JSON.stringify(bad),
|
|
106
|
+
schema: SCHEMA_STR,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(result.valid).toBe(false);
|
|
110
|
+
const e = result.errors[0];
|
|
111
|
+
expect(e.error_kind).toBe("disallowed_parent_type");
|
|
112
|
+
expect(e.entity_uid).toContain("alice");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("VE7: malformed entities JSON → parse_error", async () => {
|
|
116
|
+
const result = await handleValidateEntities({
|
|
117
|
+
entities: "{ not valid json",
|
|
118
|
+
schema: SCHEMA_STR,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result.valid).toBe(false);
|
|
122
|
+
expect(result.errors[0].error_kind).toBe("parse_error");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("VE8: no schema provided → JSON shape only, no type validation", async () => {
|
|
126
|
+
const result = await handleValidateEntities({
|
|
127
|
+
entities: JSON.stringify(ENTITIES),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.valid).toBe(true);
|
|
131
|
+
expect(result.entity_count).toBe(ENTITIES.length);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("VE9: classifyError tags unknown patterns with a self-documenting prefix on the message", async () => {
|
|
135
|
+
const { classifyError } = await import("../../src/tools/validate-entities.js");
|
|
136
|
+
const synthetic = "some future Cedar 5.x error wording the current regex set does not recognize";
|
|
137
|
+
|
|
138
|
+
const result = classifyError(synthetic);
|
|
139
|
+
|
|
140
|
+
expect(result.error_kind).toBe("other");
|
|
141
|
+
expect(result.message).toContain("unrecognized error pattern");
|
|
142
|
+
expect(result.message).toContain(synthetic);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("VE9b: classifyError still classifies a known pattern correctly (regression guard)", async () => {
|
|
146
|
+
const { classifyError } = await import("../../src/tools/validate-entities.js");
|
|
147
|
+
const known = `attribute \`bogus\` on \`DocMgmt::User::"alice"\` should not exist according to the schema`;
|
|
148
|
+
|
|
149
|
+
const result = classifyError(known);
|
|
150
|
+
|
|
151
|
+
expect(result.error_kind).toBe("unknown_attribute");
|
|
152
|
+
expect(result.attribute).toBe("bogus");
|
|
153
|
+
expect(result.message).not.toContain("unrecognized error pattern");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("VEF: falsification — entity with three violations surfaces all three", async () => {
|
|
157
|
+
// wrong type, missing required, unknown attr — all in one entity.
|
|
158
|
+
// WASM may return them as a single error or multiple — test that the
|
|
159
|
+
// collective surface mentions all three concerns somewhere.
|
|
160
|
+
const bad = [
|
|
161
|
+
{
|
|
162
|
+
uid: { type: "DocMgmt::User", id: "alice" },
|
|
163
|
+
attrs: { name: 42, bogus: "x" }, // wrong type + missing email + unknown attr
|
|
164
|
+
parents: [],
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const result = await handleValidateEntities({
|
|
169
|
+
entities: JSON.stringify(bad),
|
|
170
|
+
schema: SCHEMA_STR,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(result.valid).toBe(false);
|
|
174
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
175
|
+
// At minimum, the surfaced error must clearly attribute the violation to 'alice'
|
|
176
|
+
expect(result.errors[0].entity_uid).toContain("alice");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { handleValidateSchema } from "../../src/tools/validate-schema.js";
|
|
3
|
+
import { SCHEMA_JSON } from "../fixtures/docmgmt.js";
|
|
4
|
+
|
|
5
|
+
const SCHEMA_JSON_STR = JSON.stringify(SCHEMA_JSON);
|
|
6
|
+
|
|
7
|
+
const CEDARSCHEMA_TEXT = `
|
|
8
|
+
namespace DocMgmt {
|
|
9
|
+
entity User in [Role] = { name: String, email: String };
|
|
10
|
+
entity Role;
|
|
11
|
+
entity Document in [Folder] = { owner: String, classification: String };
|
|
12
|
+
entity Folder;
|
|
13
|
+
action READ, WRITE, DELETE appliesTo {
|
|
14
|
+
principal: [User],
|
|
15
|
+
resource: [Document]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
`.trim();
|
|
19
|
+
|
|
20
|
+
describe("cedar_validate_schema", () => {
|
|
21
|
+
it("VS1: returns valid:true and detected JSON format for a well-formed JSON schema", async () => {
|
|
22
|
+
const result = await handleValidateSchema({ schema: SCHEMA_JSON_STR });
|
|
23
|
+
|
|
24
|
+
expect(result.valid).toBe(true);
|
|
25
|
+
expect(result.format).toBe("json");
|
|
26
|
+
expect(result.errors).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("VS2: returns valid:true and detected cedarschema format for cedarschema text", async () => {
|
|
30
|
+
const result = await handleValidateSchema({ schema: CEDARSCHEMA_TEXT });
|
|
31
|
+
|
|
32
|
+
expect(result.valid).toBe(true);
|
|
33
|
+
expect(result.format).toBe("cedarschema");
|
|
34
|
+
expect(result.errors).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("VS3: returns structured errors for malformed cedarschema", async () => {
|
|
38
|
+
const result = await handleValidateSchema({ schema: "not a schema" });
|
|
39
|
+
|
|
40
|
+
expect(result.valid).toBe(false);
|
|
41
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
42
|
+
expect(result.errors[0].message).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("VS4: reports namespaces from a JSON schema", async () => {
|
|
46
|
+
const result = await handleValidateSchema({ schema: SCHEMA_JSON_STR });
|
|
47
|
+
|
|
48
|
+
expect(result.namespaces).toContain("DocMgmt");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("VS5: reports counts of entity types, actions, and common types", async () => {
|
|
52
|
+
const result = await handleValidateSchema({ schema: SCHEMA_JSON_STR });
|
|
53
|
+
|
|
54
|
+
expect(result.entity_type_count).toBe(4);
|
|
55
|
+
expect(result.action_count).toBe(3);
|
|
56
|
+
expect(result.common_type_count).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("VS6: returns structured error with source location for syntactically broken cedarschema", async () => {
|
|
60
|
+
const broken = "namespace DocMgmt { entity User { name String } }"; // missing colon
|
|
61
|
+
const result = await handleValidateSchema({ schema: broken });
|
|
62
|
+
|
|
63
|
+
expect(result.valid).toBe(false);
|
|
64
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("VS7: handles an empty string by returning a parse error, not a crash", async () => {
|
|
68
|
+
const result = await handleValidateSchema({ schema: "" });
|
|
69
|
+
|
|
70
|
+
expect(result.valid).toBe(false);
|
|
71
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("VS8: handles namespace-less JSON schema (top-level entityTypes)", async () => {
|
|
75
|
+
const flatSchema = JSON.stringify({
|
|
76
|
+
"": {
|
|
77
|
+
entityTypes: { Foo: { memberOfTypes: [], shape: { type: "Record", attributes: {} } } },
|
|
78
|
+
actions: {},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const result = await handleValidateSchema({ schema: flatSchema });
|
|
82
|
+
|
|
83
|
+
expect(result.valid).toBe(true);
|
|
84
|
+
expect(result.namespaces).toEqual([""]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { handleValidateTemplate } from "../../src/tools/validate-template.js";
|
|
3
|
+
|
|
4
|
+
const SCHEMA = `namespace App {
|
|
5
|
+
entity User;
|
|
6
|
+
entity Document;
|
|
7
|
+
action read appliesTo { principal: [User], resource: [Document], context: {} };
|
|
8
|
+
action write appliesTo { principal: [User], resource: [Document], context: {} };
|
|
9
|
+
}`;
|
|
10
|
+
|
|
11
|
+
const VALID_TEMPLATE = `permit(
|
|
12
|
+
principal == ?principal,
|
|
13
|
+
action == App::Action::"read",
|
|
14
|
+
resource == ?resource
|
|
15
|
+
);`;
|
|
16
|
+
|
|
17
|
+
const RESOURCE_ONLY_TEMPLATE = `permit(
|
|
18
|
+
principal,
|
|
19
|
+
action == App::Action::"read",
|
|
20
|
+
resource == ?resource
|
|
21
|
+
);`;
|
|
22
|
+
|
|
23
|
+
describe("cedar_validate_template", () => {
|
|
24
|
+
it("VT1 — valid template returns valid:true and detected slots", async () => {
|
|
25
|
+
const result = await handleValidateTemplate({ template: VALID_TEMPLATE, schema: SCHEMA });
|
|
26
|
+
|
|
27
|
+
expect(result.error).toBeUndefined();
|
|
28
|
+
expect(result.valid).toBe(true);
|
|
29
|
+
expect(result.errors).toHaveLength(0);
|
|
30
|
+
expect(result.slots_detected).toContain("?principal");
|
|
31
|
+
expect(result.slots_detected).toContain("?resource");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("VT2 — template with only ?resource slot detected", async () => {
|
|
35
|
+
const result = await handleValidateTemplate({ template: RESOURCE_ONLY_TEMPLATE, schema: SCHEMA });
|
|
36
|
+
|
|
37
|
+
expect(result.valid).toBe(true);
|
|
38
|
+
expect(result.slots_detected).toContain("?resource");
|
|
39
|
+
expect(result.slots_detected).not.toContain("?principal");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("VT3 — invalid Cedar syntax returns valid:false with errors", async () => {
|
|
43
|
+
const result = await handleValidateTemplate({
|
|
44
|
+
template: "this is not cedar !!@#$",
|
|
45
|
+
schema: SCHEMA,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result.valid).toBe(false);
|
|
49
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("VT4 — template with unknown action in schema returns validation error", async () => {
|
|
53
|
+
const badTemplate = `permit(
|
|
54
|
+
principal == ?principal,
|
|
55
|
+
action == App::Action::"nonexistent",
|
|
56
|
+
resource == ?resource
|
|
57
|
+
);`;
|
|
58
|
+
|
|
59
|
+
const result = await handleValidateTemplate({ template: badTemplate, schema: SCHEMA });
|
|
60
|
+
|
|
61
|
+
expect(result.valid).toBe(false);
|
|
62
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("VT5 — missing schema returns error", async () => {
|
|
66
|
+
const result = await handleValidateTemplate({ template: VALID_TEMPLATE, schema: "" });
|
|
67
|
+
|
|
68
|
+
expect(result.error).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("VT6 — accepts JSON schema format (not just cedarschema text)", async () => {
|
|
72
|
+
const jsonSchema = JSON.stringify({
|
|
73
|
+
App: {
|
|
74
|
+
entityTypes: {
|
|
75
|
+
User: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
|
|
76
|
+
Document: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
|
|
77
|
+
},
|
|
78
|
+
actions: {
|
|
79
|
+
read: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"], context: { type: "Record", attributes: {} } } },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const result = await handleValidateTemplate({ template: VALID_TEMPLATE, schema: jsonSchema });
|
|
85
|
+
|
|
86
|
+
expect(result.error).toBeUndefined();
|
|
87
|
+
expect(result.valid).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|