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,344 @@
|
|
|
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 { StoreManager } from "../../src/resources/store-manager.js";
|
|
6
|
+
|
|
7
|
+
// ─── Fixture helpers ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function createTestStore(baseDir: string, storeName: string): string {
|
|
10
|
+
const storePath = join(baseDir, storeName);
|
|
11
|
+
mkdirSync(join(storePath, "policies"), { recursive: true });
|
|
12
|
+
|
|
13
|
+
writeFileSync(
|
|
14
|
+
join(storePath, "policies", "admin.cedar"),
|
|
15
|
+
`permit(principal in DocMgmt::Role::"admin", action, resource);`
|
|
16
|
+
);
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(storePath, "policies", "viewer.cedar"),
|
|
19
|
+
`permit(principal in DocMgmt::Role::"viewer", action == DocMgmt::Action::"READ", resource);`
|
|
20
|
+
);
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(storePath, "schema.cedarschema"),
|
|
23
|
+
`namespace DocMgmt { entity User in [Role]; entity Role; entity Document; action READ appliesTo { principal: [User], resource: [Document], context: {} }; }`
|
|
24
|
+
);
|
|
25
|
+
return storePath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
describe("StoreManager", () => {
|
|
31
|
+
let tmpDir: string;
|
|
32
|
+
let manager: StoreManager;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tmpDir = join(tmpdir(), `cedar-test-${Date.now()}`);
|
|
36
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
37
|
+
manager = new StoreManager();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("loadFromRoots", () => {
|
|
45
|
+
it("loads a single root as a named store", () => {
|
|
46
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
47
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
48
|
+
expect(manager.listStoreNames()).toContain("blue");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("derives store name from URI last segment when root has no name", () => {
|
|
52
|
+
const storePath = createTestStore(tmpDir, "production");
|
|
53
|
+
manager.loadFromRoots([{ uri: `file://${storePath}` }]);
|
|
54
|
+
expect(manager.listStoreNames()).toContain("production");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("loads multiple roots as separate stores", () => {
|
|
58
|
+
createTestStore(tmpDir, "blue");
|
|
59
|
+
createTestStore(tmpDir, "green");
|
|
60
|
+
manager.loadFromRoots([
|
|
61
|
+
{ uri: `file://${tmpDir}/blue`, name: "blue" },
|
|
62
|
+
{ uri: `file://${tmpDir}/green`, name: "green" },
|
|
63
|
+
]);
|
|
64
|
+
expect(manager.listStoreNames()).toEqual(expect.arrayContaining(["blue", "green"]));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("clears previous stores on reload", () => {
|
|
68
|
+
createTestStore(tmpDir, "old");
|
|
69
|
+
manager.loadFromRoots([{ uri: `file://${tmpDir}/old`, name: "old" }]);
|
|
70
|
+
expect(manager.listStoreNames()).toContain("old");
|
|
71
|
+
|
|
72
|
+
createTestStore(tmpDir, "new");
|
|
73
|
+
manager.loadFromRoots([{ uri: `file://${tmpDir}/new`, name: "new" }]);
|
|
74
|
+
expect(manager.listStoreNames()).not.toContain("old");
|
|
75
|
+
expect(manager.listStoreNames()).toContain("new");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("handles trailing slash in root URI", () => {
|
|
79
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
80
|
+
manager.loadFromRoots([{ uri: `file://${storePath}/` }]);
|
|
81
|
+
expect(manager.listStoreNames()).toContain("blue");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("listPolicies", () => {
|
|
86
|
+
it("returns policy IDs (filenames without .cedar extension)", () => {
|
|
87
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
88
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
89
|
+
const policies = manager.listPolicies("blue");
|
|
90
|
+
expect(policies).toContain("admin");
|
|
91
|
+
expect(policies).toContain("viewer");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns empty array when policies directory does not exist", () => {
|
|
95
|
+
mkdirSync(join(tmpDir, "empty"), { recursive: true });
|
|
96
|
+
manager.loadFromRoots([{ uri: `file://${tmpDir}/empty`, name: "empty" }]);
|
|
97
|
+
expect(manager.listPolicies("empty")).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("throws for unknown store name", () => {
|
|
101
|
+
expect(() => manager.listPolicies("nonexistent")).toThrow(/store.*not found/i);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("readPolicy", () => {
|
|
106
|
+
it("returns policy content by ID", () => {
|
|
107
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
108
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
109
|
+
const content = manager.readPolicy("blue", "admin");
|
|
110
|
+
expect(content).toContain("permit");
|
|
111
|
+
expect(content).toContain("admin");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("throws for unknown policy ID", () => {
|
|
115
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
116
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
117
|
+
expect(() => manager.readPolicy("blue", "nonexistent")).toThrow(/policy.*not found/i);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("throws for unknown store", () => {
|
|
121
|
+
expect(() => manager.readPolicy("ghost", "admin")).toThrow(/store.*not found/i);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("readSchema", () => {
|
|
126
|
+
it("reads .cedarschema file", () => {
|
|
127
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
128
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
129
|
+
const schema = manager.readSchema("blue");
|
|
130
|
+
expect(schema).toContain("DocMgmt");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("reads schema.json when .cedarschema is absent", () => {
|
|
134
|
+
mkdirSync(join(tmpDir, "json-store", "policies"), { recursive: true });
|
|
135
|
+
writeFileSync(
|
|
136
|
+
join(tmpDir, "json-store", "schema.json"),
|
|
137
|
+
JSON.stringify({ DocMgmt: { entityTypes: {}, actions: {} } })
|
|
138
|
+
);
|
|
139
|
+
manager.loadFromRoots([{ uri: `file://${tmpDir}/json-store`, name: "json-store" }]);
|
|
140
|
+
const schema = manager.readSchema("json-store");
|
|
141
|
+
expect(schema).toContain("DocMgmt");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("throws for unknown store", () => {
|
|
145
|
+
expect(() => manager.readSchema("ghost")).toThrow(/store.*not found/i);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("throws when no schema file exists in store", () => {
|
|
149
|
+
mkdirSync(join(tmpDir, "no-schema", "policies"), { recursive: true });
|
|
150
|
+
manager.loadFromRoots([{ uri: `file://${tmpDir}/no-schema`, name: "no-schema" }]);
|
|
151
|
+
expect(() => manager.readSchema("no-schema")).toThrow(/schema.*not found/i);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("readAllPolicies", () => {
|
|
156
|
+
it("returns all policies concatenated", () => {
|
|
157
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
158
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
159
|
+
const all = manager.readAllPolicies("blue");
|
|
160
|
+
expect(all).toContain("admin");
|
|
161
|
+
expect(all).toContain("viewer");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("security", () => {
|
|
166
|
+
it("rejects non-file:// URIs at loadFromRoots time (does not silently accept)", () => {
|
|
167
|
+
manager.loadFromRoots([{ uri: "https://example.com/policies", name: "remote" }]);
|
|
168
|
+
// Non-file:// root should be skipped, not silently accepted
|
|
169
|
+
expect(manager.listStoreNames()).not.toContain("remote");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("disambiguates store names when two roots share the same last path segment", () => {
|
|
173
|
+
// Two different paths but same last segment — should NOT silently overwrite
|
|
174
|
+
mkdirSync(join(tmpDir, "team-a", "production", "policies"), { recursive: true });
|
|
175
|
+
mkdirSync(join(tmpDir, "team-b", "production", "policies"), { recursive: true });
|
|
176
|
+
writeFileSync(join(tmpDir, "team-a", "production", "schema.cedarschema"), "namespace A {}");
|
|
177
|
+
writeFileSync(join(tmpDir, "team-b", "production", "schema.cedarschema"), "namespace B {}");
|
|
178
|
+
|
|
179
|
+
manager.loadFromRoots([
|
|
180
|
+
{ uri: `file://${tmpDir}/team-a/production` },
|
|
181
|
+
{ uri: `file://${tmpDir}/team-b/production` },
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// Both stores must survive — with disambiguated names
|
|
185
|
+
expect(manager.listStoreNames().length).toBe(2);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("rejects policy IDs with path traversal characters", () => {
|
|
189
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
190
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
191
|
+
expect(() => manager.readPolicy("blue", "..")).toThrow(/Invalid policy ID/i);
|
|
192
|
+
expect(() => manager.readPolicy("blue", "../../../etc/passwd")).toThrow(/Invalid policy ID/i);
|
|
193
|
+
expect(() => manager.readPolicy("blue", "admin/subdir")).toThrow(/Invalid policy ID/i);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("isPathAllowed", () => {
|
|
198
|
+
it("returns true for paths inside a loaded root", () => {
|
|
199
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
200
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
201
|
+
expect(manager.isPathAllowed(`${storePath}/policies/admin.cedar`)).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("returns false for paths outside all roots", () => {
|
|
205
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
206
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
207
|
+
expect(manager.isPathAllowed("/etc/passwd")).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("listEntities", () => {
|
|
212
|
+
it("returns entity file IDs (filenames without .json extension), sorted", () => {
|
|
213
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
214
|
+
mkdirSync(join(storePath, "entities"), { recursive: true });
|
|
215
|
+
writeFileSync(join(storePath, "entities", "users.json"), JSON.stringify([]));
|
|
216
|
+
writeFileSync(join(storePath, "entities", "docs.json"), JSON.stringify([]));
|
|
217
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
218
|
+
expect(manager.listEntities("blue")).toEqual(["docs", "users"]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("returns empty array when entities directory does not exist", () => {
|
|
222
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
223
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
224
|
+
expect(manager.listEntities("blue")).toEqual([]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("throws for unknown store name", () => {
|
|
228
|
+
expect(() => manager.listEntities("nonexistent")).toThrow(/store.*not found/i);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("readEntities", () => {
|
|
233
|
+
it("returns entity file content as text", () => {
|
|
234
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
235
|
+
mkdirSync(join(storePath, "entities"), { recursive: true });
|
|
236
|
+
const payload = JSON.stringify([{ uid: { type: "DocMgmt::User", id: "alice" }, attrs: {}, parents: [] }]);
|
|
237
|
+
writeFileSync(join(storePath, "entities", "users.json"), payload);
|
|
238
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
239
|
+
expect(manager.readEntities("blue", "users")).toBe(payload);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("throws for unknown entity file ID", () => {
|
|
243
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
244
|
+
mkdirSync(join(storePath, "entities"), { recursive: true });
|
|
245
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
246
|
+
expect(() => manager.readEntities("blue", "ghost")).toThrow(/entity file.*not found/i);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("throws for path traversal in entity file ID", () => {
|
|
250
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
251
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
252
|
+
expect(() => manager.readEntities("blue", "..")).toThrow(/Invalid entity file ID/i);
|
|
253
|
+
expect(() => manager.readEntities("blue", "../../../etc/passwd")).toThrow(/Invalid entity file ID/i);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("throws for unknown store", () => {
|
|
257
|
+
expect(() => manager.readEntities("ghost", "users")).toThrow(/store.*not found/i);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("getDefaultStore (10d auto-discovery)", () => {
|
|
262
|
+
it("returns { kind: 'none' } when no stores are loaded", () => {
|
|
263
|
+
// Fresh manager — nothing loaded.
|
|
264
|
+
const result = manager.getDefaultStore();
|
|
265
|
+
expect(result.kind).toBe("none");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("returns { kind: 'single', store } when exactly one store is loaded", () => {
|
|
269
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
270
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
271
|
+
|
|
272
|
+
const result = manager.getDefaultStore();
|
|
273
|
+
expect(result.kind).toBe("single");
|
|
274
|
+
if (result.kind === "single") {
|
|
275
|
+
expect(result.store.name).toBe("blue");
|
|
276
|
+
expect(result.store.path).toBe(storePath);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("returns { kind: 'ambiguous', names } when multiple stores are loaded", () => {
|
|
281
|
+
createTestStore(tmpDir, "blue");
|
|
282
|
+
createTestStore(tmpDir, "green");
|
|
283
|
+
manager.loadFromRoots([
|
|
284
|
+
{ uri: `file://${tmpDir}/blue`, name: "blue" },
|
|
285
|
+
{ uri: `file://${tmpDir}/green`, name: "green" },
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
const result = manager.getDefaultStore();
|
|
289
|
+
expect(result.kind).toBe("ambiguous");
|
|
290
|
+
if (result.kind === "ambiguous") {
|
|
291
|
+
expect(result.names).toEqual(expect.arrayContaining(["blue", "green"]));
|
|
292
|
+
expect(result.names).toHaveLength(2);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe("readAllEntities", () => {
|
|
298
|
+
it("merges entity arrays from all files into one JSON array", () => {
|
|
299
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
300
|
+
mkdirSync(join(storePath, "entities"), { recursive: true });
|
|
301
|
+
writeFileSync(
|
|
302
|
+
join(storePath, "entities", "users.json"),
|
|
303
|
+
JSON.stringify([{ uid: { type: "DocMgmt::User", id: "alice" }, attrs: {}, parents: [] }])
|
|
304
|
+
);
|
|
305
|
+
writeFileSync(
|
|
306
|
+
join(storePath, "entities", "docs.json"),
|
|
307
|
+
JSON.stringify([{ uid: { type: "DocMgmt::Document", id: "doc1" }, attrs: {}, parents: [] }])
|
|
308
|
+
);
|
|
309
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
310
|
+
const merged = JSON.parse(manager.readAllEntities("blue")) as Array<{ uid: { type: string } }>;
|
|
311
|
+
expect(merged.length).toBe(2);
|
|
312
|
+
const types = merged.map((e) => e.uid.type);
|
|
313
|
+
expect(types).toContain("DocMgmt::User");
|
|
314
|
+
expect(types).toContain("DocMgmt::Document");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("returns empty JSON array when entities directory is absent", () => {
|
|
318
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
319
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
320
|
+
const result = manager.readAllEntities("blue");
|
|
321
|
+
expect(JSON.parse(result)).toEqual([]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("throws when an entity file contains a JSON object instead of an array", () => {
|
|
325
|
+
// Falsifying input: an object is valid JSON but not a valid entity array — must error clearly.
|
|
326
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
327
|
+
mkdirSync(join(storePath, "entities"), { recursive: true });
|
|
328
|
+
writeFileSync(
|
|
329
|
+
join(storePath, "entities", "bad.json"),
|
|
330
|
+
JSON.stringify({ uid: { type: "DocMgmt::User", id: "alice" }, attrs: {}, parents: [] })
|
|
331
|
+
);
|
|
332
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
333
|
+
expect(() => manager.readAllEntities("blue")).toThrow(/must contain a JSON array/i);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("throws when an entity file contains invalid JSON", () => {
|
|
337
|
+
const storePath = createTestStore(tmpDir, "blue");
|
|
338
|
+
mkdirSync(join(storePath, "entities"), { recursive: true });
|
|
339
|
+
writeFileSync(join(storePath, "entities", "broken.json"), "{ not valid json");
|
|
340
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "blue" }]);
|
|
341
|
+
expect(() => manager.readAllEntities("blue")).toThrow(/invalid JSON/i);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
classifyAvpChange,
|
|
4
|
+
AVP_VALIDATION_ERRORS,
|
|
5
|
+
} from "../../../src/tools/advise/avp-rules.js";
|
|
6
|
+
|
|
7
|
+
describe("classifyAvpChange", () => {
|
|
8
|
+
describe("in-place targets via UpdatePolicy", () => {
|
|
9
|
+
it.each([
|
|
10
|
+
["action"],
|
|
11
|
+
["when_clause"],
|
|
12
|
+
["unless_clause"],
|
|
13
|
+
["policy_name"],
|
|
14
|
+
])("classifies %s as in_place_via_update_policy", (changeField) => {
|
|
15
|
+
const result = classifyAvpChange(changeField);
|
|
16
|
+
expect(result.mode).toBe("in_place_via_update_policy");
|
|
17
|
+
expect(result.rationale).toBeTruthy();
|
|
18
|
+
expect(result.rationale.length).toBeGreaterThan(0);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("delete-recreate targets", () => {
|
|
23
|
+
it.each([
|
|
24
|
+
["effect"],
|
|
25
|
+
["principal"],
|
|
26
|
+
["resource"],
|
|
27
|
+
["policy_type_conversion"],
|
|
28
|
+
])("classifies %s as requires_delete_recreate", (changeField) => {
|
|
29
|
+
const result = classifyAvpChange(changeField);
|
|
30
|
+
expect(result.mode).toBe("requires_delete_recreate");
|
|
31
|
+
expect(result.rationale).toBeTruthy();
|
|
32
|
+
expect(result.rationale.length).toBeGreaterThan(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("new policy path", () => {
|
|
37
|
+
it("classifies new_policy as new_policy_via_create_policy", () => {
|
|
38
|
+
const result = classifyAvpChange("new_policy");
|
|
39
|
+
expect(result.mode).toBe("new_policy_via_create_policy");
|
|
40
|
+
expect(result.rationale).toBeTruthy();
|
|
41
|
+
expect(result.rationale.length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("default branch for unrecognized fields", () => {
|
|
46
|
+
it.each([
|
|
47
|
+
["unrecognized_field"],
|
|
48
|
+
[""],
|
|
49
|
+
])("falls through to in_place_via_update_policy for %j with unclassified rationale", (changeField) => {
|
|
50
|
+
const result = classifyAvpChange(changeField);
|
|
51
|
+
expect(result.mode).toBe("in_place_via_update_policy");
|
|
52
|
+
expect(result.rationale).toContain("Change type unclassified");
|
|
53
|
+
expect(result.rationale.length).toBeGreaterThan(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("AVP_VALIDATION_ERRORS constant", () => {
|
|
59
|
+
it("contains exactly 10 entries", () => {
|
|
60
|
+
expect(AVP_VALIDATION_ERRORS).toHaveLength(10);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("every entry has a non-empty string id and description", () => {
|
|
64
|
+
for (const entry of AVP_VALIDATION_ERRORS) {
|
|
65
|
+
expect(typeof entry.id).toBe("string");
|
|
66
|
+
expect(entry.id.length).toBeGreaterThan(0);
|
|
67
|
+
expect(typeof entry.description).toBe("string");
|
|
68
|
+
expect(entry.description.length).toBeGreaterThan(0);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("has unique ids across all entries", () => {
|
|
73
|
+
const ids = AVP_VALIDATION_ERRORS.map((e) => e.id);
|
|
74
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
75
|
+
});
|
|
76
|
+
});
|