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,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-based tests for cedar-mcp-server tools.
|
|
3
|
+
*
|
|
4
|
+
* Property tests assert invariants that must hold for ALL inputs in a class,
|
|
5
|
+
* not just the hand-picked examples in unit tests. They use `fast-check` to
|
|
6
|
+
* generate ~100 cases per property and shrink failing cases to a minimal
|
|
7
|
+
* reproducer.
|
|
8
|
+
*
|
|
9
|
+
* Calls handlers directly (not through MCP stdio) for speed — properties
|
|
10
|
+
* concern tool semantics, not protocol framing. Protocol-level invariants
|
|
11
|
+
* live in test/integration/e2e/protocol.test.ts.
|
|
12
|
+
*
|
|
13
|
+
* Each property below states the failure case it would catch.
|
|
14
|
+
*
|
|
15
|
+
* Run: npx vitest run test/property
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect } from "vitest";
|
|
18
|
+
import fc from "fast-check";
|
|
19
|
+
import { handleValidate } from "../../src/tools/validate.js";
|
|
20
|
+
import { handleFormat } from "../../src/tools/format.js";
|
|
21
|
+
import { handleAuthorize } from "../../src/tools/authorize.js";
|
|
22
|
+
import { handleDiffSchema } from "../../src/tools/diff-schema.js";
|
|
23
|
+
import { handleCheckChange } from "../../src/tools/check-change.js";
|
|
24
|
+
import { handleTranslate } from "../../src/tools/translate.js";
|
|
25
|
+
import { SCHEMA_JSON, POLICIES } from "../fixtures/docmgmt.js";
|
|
26
|
+
|
|
27
|
+
const SCHEMA_STR = JSON.stringify(SCHEMA_JSON);
|
|
28
|
+
|
|
29
|
+
// ─── Generators ───────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Cedar-legal identifier (Role name, entity id, attribute name). */
|
|
32
|
+
const cedarId = fc.stringMatching(/^[a-zA-Z][a-zA-Z0-9_-]{0,30}$/);
|
|
33
|
+
|
|
34
|
+
/** Cedar-legal action key (lowercase ASCII to avoid clashing with type names). */
|
|
35
|
+
const actionKey = fc.stringMatching(/^[a-z][a-z_]{0,15}$/);
|
|
36
|
+
|
|
37
|
+
/** A pure Membership/RBAC permit policy: permit principal in Role::"X". */
|
|
38
|
+
function membershipPolicy(roleId: string): string {
|
|
39
|
+
return `permit (
|
|
40
|
+
principal in DocMgmt::Role::"${roleId}",
|
|
41
|
+
action,
|
|
42
|
+
resource
|
|
43
|
+
);`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Properties ───────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe("property — cedar_format idempotency across the option space", () => {
|
|
49
|
+
// Property: for any policy P drawn from a real schema's policy set, for any
|
|
50
|
+
// line_width in [40, 200] and indent_width in [0, 8], format is idempotent:
|
|
51
|
+
// format(format(P, w, i), w, i) === format(P, w, i)
|
|
52
|
+
//
|
|
53
|
+
// Failure case caught: a formatter whose output depends on prior input state
|
|
54
|
+
// (e.g., a hidden counter) would produce different output on the second run.
|
|
55
|
+
// A formatter that doesn't strip a trailing newline cleanly would diverge.
|
|
56
|
+
it("format(format(P, w, i), w, i) equals format(P, w, i)", () => {
|
|
57
|
+
fc.assert(
|
|
58
|
+
fc.asyncProperty(
|
|
59
|
+
fc.integer({ min: 40, max: 200 }),
|
|
60
|
+
fc.integer({ min: 0, max: 8 }),
|
|
61
|
+
async (lineWidth, indentWidth) => {
|
|
62
|
+
const first = await handleFormat({ policies: POLICIES, line_width: lineWidth, indent_width: indentWidth });
|
|
63
|
+
if (!first.formatted) return; // formatter rejected; nothing to compare
|
|
64
|
+
const second = await handleFormat({ policies: first.formatted, line_width: lineWidth, indent_width: indentWidth });
|
|
65
|
+
expect(second.formatted).toBe(first.formatted);
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
{ numRuns: 100 } // fast-check default; handler calls are direct (~5ms) so total cost is bounded
|
|
69
|
+
);
|
|
70
|
+
}, 60_000);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("property — cedar_validate is deterministic", () => {
|
|
74
|
+
// Property: running cedar_validate on the same input multiple times produces
|
|
75
|
+
// identical results. Failure case: a stateful or random-output validator
|
|
76
|
+
// would produce inconsistent results across runs, breaking CI repeatability.
|
|
77
|
+
it("two consecutive validate calls on the same input return equal results", async () => {
|
|
78
|
+
await fc.assert(
|
|
79
|
+
fc.asyncProperty(cedarId, async (roleId) => {
|
|
80
|
+
const policy = membershipPolicy(roleId);
|
|
81
|
+
const a = await handleValidate({ policies: policy, schema: SCHEMA_STR });
|
|
82
|
+
const b = await handleValidate({ policies: policy, schema: SCHEMA_STR });
|
|
83
|
+
expect(b).toEqual(a);
|
|
84
|
+
}),
|
|
85
|
+
{ numRuns: 100 }
|
|
86
|
+
);
|
|
87
|
+
}, 60_000);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("property — cedar_authorize is deterministic", () => {
|
|
91
|
+
// Property: running cedar_authorize on the same input multiple times produces
|
|
92
|
+
// identical decisions. Failure case: WASM internal state leaking between
|
|
93
|
+
// calls, or non-deterministic policy selection in the engine.
|
|
94
|
+
it("two consecutive authorize calls return the same decision", async () => {
|
|
95
|
+
const entities = JSON.stringify([
|
|
96
|
+
{ uid: { type: "DocMgmt::User", id: "alice" }, attrs: { name: "Alice", email: "a@b.c" }, parents: [{ type: "DocMgmt::Role", id: "admin" }] },
|
|
97
|
+
{ uid: { type: "DocMgmt::Role", id: "admin" }, attrs: {}, parents: [] },
|
|
98
|
+
{ uid: { type: "DocMgmt::Document", id: "d1" }, attrs: { owner: "alice", classification: "public" }, parents: [] },
|
|
99
|
+
]);
|
|
100
|
+
await fc.assert(
|
|
101
|
+
fc.asyncProperty(actionKey, async (action) => {
|
|
102
|
+
// Use READ from schema; the property tests determinism, not the action choice
|
|
103
|
+
const a = await handleAuthorize({
|
|
104
|
+
policies: POLICIES,
|
|
105
|
+
principal: 'DocMgmt::User::"alice"',
|
|
106
|
+
action: 'DocMgmt::Action::"READ"',
|
|
107
|
+
resource: 'DocMgmt::Document::"d1"',
|
|
108
|
+
entities,
|
|
109
|
+
schema: SCHEMA_STR,
|
|
110
|
+
});
|
|
111
|
+
const b = await handleAuthorize({
|
|
112
|
+
policies: POLICIES,
|
|
113
|
+
principal: 'DocMgmt::User::"alice"',
|
|
114
|
+
action: 'DocMgmt::Action::"READ"',
|
|
115
|
+
resource: 'DocMgmt::Document::"d1"',
|
|
116
|
+
entities,
|
|
117
|
+
schema: SCHEMA_STR,
|
|
118
|
+
});
|
|
119
|
+
expect(b.decision).toBe(a.decision);
|
|
120
|
+
void action; // generator is the fc property driver; we don't use it directly
|
|
121
|
+
}),
|
|
122
|
+
{ numRuns: 100 }
|
|
123
|
+
);
|
|
124
|
+
}, 60_000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("property — cedar_diff_schema add/remove symmetry", () => {
|
|
128
|
+
// Property: for any two schemas S1, S2:
|
|
129
|
+
// diff(S1, S2).entity_types.added.length === diff(S2, S1).entity_types.removed.length
|
|
130
|
+
// i.e., what's added going A→B must be removed going B→A. Same for actions.
|
|
131
|
+
//
|
|
132
|
+
// Failure case: an asymmetric diff implementation that double-counts or skips
|
|
133
|
+
// entries based on direction. Catches one-sided iteration bugs.
|
|
134
|
+
it("entity_types.added going A→B equals entity_types.removed going B→A", async () => {
|
|
135
|
+
const blueSchema = JSON.stringify(SCHEMA_JSON);
|
|
136
|
+
|
|
137
|
+
await fc.assert(
|
|
138
|
+
fc.asyncProperty(cedarId, async (newEntityName) => {
|
|
139
|
+
// Sanitize — Cedar entity type names can't be reserved Cedar tokens
|
|
140
|
+
if (["User", "Role", "Document", "Folder"].includes(newEntityName)) return;
|
|
141
|
+
|
|
142
|
+
// Green = blue + one extra entity type
|
|
143
|
+
const greenObj = JSON.parse(blueSchema) as { DocMgmt: { entityTypes: Record<string, unknown> } };
|
|
144
|
+
greenObj.DocMgmt.entityTypes[newEntityName] = {
|
|
145
|
+
memberOfTypes: [],
|
|
146
|
+
shape: { type: "Record", attributes: {} },
|
|
147
|
+
};
|
|
148
|
+
const greenSchema = JSON.stringify(greenObj);
|
|
149
|
+
|
|
150
|
+
const forward = await handleDiffSchema({ blue: blueSchema, green: greenSchema });
|
|
151
|
+
const reverse = await handleDiffSchema({ blue: greenSchema, green: blueSchema });
|
|
152
|
+
|
|
153
|
+
expect(forward.entity_types.added).toHaveLength(reverse.entity_types.removed.length);
|
|
154
|
+
expect(forward.entity_types.added[0]?.name).toBe(reverse.entity_types.removed[0]?.name);
|
|
155
|
+
}),
|
|
156
|
+
{ numRuns: 100 }
|
|
157
|
+
);
|
|
158
|
+
}, 60_000);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("property — cedar_diff_schema identity always returns safe", () => {
|
|
162
|
+
// Property: for ANY schema S, diff(S, S).risk_level === "safe" and all change
|
|
163
|
+
// arrays are empty. The unit test covers this for one schema; this property
|
|
164
|
+
// covers it for randomly-modified schemas (e.g., with extra entity types added
|
|
165
|
+
// before the diff).
|
|
166
|
+
//
|
|
167
|
+
// Failure case: __cedar:: prefix stripping asymmetry (we tested for it in unit
|
|
168
|
+
// tests, but the property surfaces it across input variations).
|
|
169
|
+
it("diff(S, S) is risk_level safe with empty change arrays", async () => {
|
|
170
|
+
await fc.assert(
|
|
171
|
+
fc.asyncProperty(cedarId, async (newEntityName) => {
|
|
172
|
+
if (["User", "Role", "Document", "Folder"].includes(newEntityName)) return;
|
|
173
|
+
const schemaObj = JSON.parse(JSON.stringify(SCHEMA_JSON)) as { DocMgmt: { entityTypes: Record<string, unknown> } };
|
|
174
|
+
schemaObj.DocMgmt.entityTypes[newEntityName] = {
|
|
175
|
+
memberOfTypes: [],
|
|
176
|
+
shape: { type: "Record", attributes: {} },
|
|
177
|
+
};
|
|
178
|
+
const schemaStr = JSON.stringify(schemaObj);
|
|
179
|
+
const result = await handleDiffSchema({ blue: schemaStr, green: schemaStr });
|
|
180
|
+
expect(result.risk_level).toBe("safe");
|
|
181
|
+
expect(result.entity_types.added).toHaveLength(0);
|
|
182
|
+
expect(result.entity_types.removed).toHaveLength(0);
|
|
183
|
+
expect(result.entity_types.modified).toHaveLength(0);
|
|
184
|
+
}),
|
|
185
|
+
{ numRuns: 100 }
|
|
186
|
+
);
|
|
187
|
+
}, 60_000);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("property — cedar_check_policy_change identity", () => {
|
|
191
|
+
// Property: for ANY policy P, check_policy_change(P, P) reports zero changes
|
|
192
|
+
// and can_update_in_place: true. Even if P contains whitespace, comments, or
|
|
193
|
+
// unusual attribute names, comparing it to itself must yield identity.
|
|
194
|
+
//
|
|
195
|
+
// Failure case: a change detector keyed on textual diff rather than semantic
|
|
196
|
+
// equality would flag whitespace-only differences (e.g., from formatting)
|
|
197
|
+
// on what should be an identity comparison.
|
|
198
|
+
it("check_policy_change(P, P) reports no changes", async () => {
|
|
199
|
+
await fc.assert(
|
|
200
|
+
fc.asyncProperty(cedarId, async (roleId) => {
|
|
201
|
+
const policy = membershipPolicy(roleId);
|
|
202
|
+
const result = await handleCheckChange({ old_policy: policy, new_policy: policy });
|
|
203
|
+
expect(result.changes).toHaveLength(0);
|
|
204
|
+
expect(result.can_update_in_place).toBe(true);
|
|
205
|
+
}),
|
|
206
|
+
{ numRuns: 100 }
|
|
207
|
+
);
|
|
208
|
+
}, 60_000);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("property — cedar_translate roundtrip preserves validation", () => {
|
|
212
|
+
// Property: for any role-membership policy that validates against the DocMgmt
|
|
213
|
+
// schema, translating to JSON and back to Cedar produces a policy that ALSO
|
|
214
|
+
// validates. Failure case: a lossy AST translation that drops a clause would
|
|
215
|
+
// cause the roundtripped version to validate differently from the original.
|
|
216
|
+
it("validate(text) and validate(text→json→text) agree", async () => {
|
|
217
|
+
await fc.assert(
|
|
218
|
+
fc.asyncProperty(cedarId, async (roleId) => {
|
|
219
|
+
const original = membershipPolicy(roleId);
|
|
220
|
+
|
|
221
|
+
const toJson = await handleTranslate({ input: original, type: "policy", direction: "to_json" });
|
|
222
|
+
if (toJson.error || !toJson.output) return; // generator produced a name the parser rejects; skip
|
|
223
|
+
|
|
224
|
+
const toCedar = await handleTranslate({ input: toJson.output, type: "policy", direction: "to_cedar" });
|
|
225
|
+
if (toCedar.error || !toCedar.output) return;
|
|
226
|
+
|
|
227
|
+
const validateOriginal = await handleValidate({ policies: original, schema: SCHEMA_STR });
|
|
228
|
+
const validateRoundtrip = await handleValidate({ policies: toCedar.output, schema: SCHEMA_STR });
|
|
229
|
+
expect(validateRoundtrip.valid).toBe(validateOriginal.valid);
|
|
230
|
+
}),
|
|
231
|
+
{ numRuns: 100 }
|
|
232
|
+
);
|
|
233
|
+
}, 60_000);
|
|
234
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
import { resolveRef } from "../../src/resources/ref-resolver.js";
|
|
7
|
+
|
|
8
|
+
describe("resolveRef", () => {
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpDir = join(tmpdir(), `cedar-ref-test-${Date.now()}`);
|
|
13
|
+
mkdirSync(join(tmpDir, "policies"), { recursive: true });
|
|
14
|
+
mkdirSync(join(tmpDir, "templates"), { recursive: true });
|
|
15
|
+
mkdirSync(join(tmpDir, "template-links"), { recursive: true });
|
|
16
|
+
mkdirSync(join(tmpDir, "entities"), { recursive: true });
|
|
17
|
+
writeFileSync(join(tmpDir, "policies", "admin.cedar"), `permit(principal in DocMgmt::Role::"admin", action, resource);`);
|
|
18
|
+
writeFileSync(join(tmpDir, "templates", "shared-read.cedar"), `permit(principal == ?principal, action == DocMgmt::Action::"READ", resource == ?resource);`);
|
|
19
|
+
writeFileSync(
|
|
20
|
+
join(tmpDir, "template-links", "link-01.json"),
|
|
21
|
+
JSON.stringify({ template_id: "shared-read", slot_values: { "?principal": "DocMgmt::User::\"alice\"", "?resource": "DocMgmt::Document::\"doc1\"" } })
|
|
22
|
+
);
|
|
23
|
+
writeFileSync(join(tmpDir, "entities", "users.json"), JSON.stringify([{ uid: { type: "DocMgmt::User", id: "alice" }, attrs: {}, parents: [] }]));
|
|
24
|
+
writeFileSync(join(tmpDir, "entities", "docs.json"), JSON.stringify([{ uid: { type: "DocMgmt::Document", id: "doc1" }, attrs: {}, parents: [] }]));
|
|
25
|
+
writeFileSync(join(tmpDir, "schema.cedarschema"), `namespace DocMgmt { entity User; entity Document; action READ appliesTo { principal: [User], resource: [Document], context: {} }; }`);
|
|
26
|
+
storeManager.loadFromRoots([{ uri: `file://${tmpDir}`, name: "mystore" }]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
31
|
+
storeManager.loadFromRoots([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── Existing patterns ────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
it("resolves cedar://policies/{store} to all concatenated policies", () => {
|
|
37
|
+
const result = resolveRef("cedar://policies/mystore");
|
|
38
|
+
expect("error" in result).toBe(false);
|
|
39
|
+
if ("content" in result) {
|
|
40
|
+
expect(result.content).toContain("permit");
|
|
41
|
+
expect(result.resolved_from).toBe("cedar://policies/mystore");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("resolves cedar://policies/{store}/{id} to single policy content", () => {
|
|
46
|
+
const result = resolveRef("cedar://policies/mystore/admin");
|
|
47
|
+
expect("error" in result).toBe(false);
|
|
48
|
+
if ("content" in result) expect(result.content).toContain("admin");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("resolves cedar://schema/{store} to schema content", () => {
|
|
52
|
+
const result = resolveRef("cedar://schema/mystore");
|
|
53
|
+
expect("error" in result).toBe(false);
|
|
54
|
+
if ("content" in result) expect(result.content).toContain("DocMgmt");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns error for unknown store", () => {
|
|
58
|
+
const result = resolveRef("cedar://policies/ghost");
|
|
59
|
+
expect("error" in result).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns error for invalid URI", () => {
|
|
63
|
+
const result = resolveRef("not-a-cedar-uri");
|
|
64
|
+
expect("error" in result).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns error for unrecognized URI pattern", () => {
|
|
68
|
+
const result = resolveRef("cedar://unknown/something");
|
|
69
|
+
expect("error" in result).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ─── Templates ────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
it("resolves cedar://templates/{store} to JSON array of template IDs", () => {
|
|
75
|
+
const result = resolveRef("cedar://templates/mystore");
|
|
76
|
+
expect("error" in result).toBe(false);
|
|
77
|
+
if ("content" in result) {
|
|
78
|
+
const ids = JSON.parse(result.content) as string[];
|
|
79
|
+
expect(ids).toContain("shared-read");
|
|
80
|
+
expect(result.resolved_from).toBe("cedar://templates/mystore");
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("resolves cedar://templates/{store}/{id} to template body", () => {
|
|
85
|
+
const result = resolveRef("cedar://templates/mystore/shared-read");
|
|
86
|
+
expect("error" in result).toBe(false);
|
|
87
|
+
if ("content" in result) {
|
|
88
|
+
expect(result.content).toContain("?principal");
|
|
89
|
+
expect(result.content).toContain("?resource");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns error for cedar://templates/{store}/{id} with unknown template", () => {
|
|
94
|
+
const result = resolveRef("cedar://templates/mystore/ghost-template");
|
|
95
|
+
expect("error" in result).toBe(true);
|
|
96
|
+
if ("error" in result) expect(result.error).toMatch(/template.*not found/i);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns error for cedar://templates/{store} with unknown store", () => {
|
|
100
|
+
const result = resolveRef("cedar://templates/ghost-store");
|
|
101
|
+
expect("error" in result).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ─── Template links ───────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
it("resolves cedar://template-links/{store} to JSON array of link IDs", () => {
|
|
107
|
+
const result = resolveRef("cedar://template-links/mystore");
|
|
108
|
+
expect("error" in result).toBe(false);
|
|
109
|
+
if ("content" in result) {
|
|
110
|
+
const ids = JSON.parse(result.content) as string[];
|
|
111
|
+
expect(ids).toContain("link-01");
|
|
112
|
+
expect(result.resolved_from).toBe("cedar://template-links/mystore");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("resolves cedar://template-links/{store}/{id} to link JSON content", () => {
|
|
117
|
+
const result = resolveRef("cedar://template-links/mystore/link-01");
|
|
118
|
+
expect("error" in result).toBe(false);
|
|
119
|
+
if ("content" in result) {
|
|
120
|
+
const link = JSON.parse(result.content) as { template_id: string };
|
|
121
|
+
expect(link.template_id).toBe("shared-read");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("returns error for cedar://template-links/{store}/{id} with unknown link", () => {
|
|
126
|
+
const result = resolveRef("cedar://template-links/mystore/ghost-link");
|
|
127
|
+
expect("error" in result).toBe(true);
|
|
128
|
+
if ("error" in result) expect(result.error).toMatch(/template link.*not found/i);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns error for cedar://template-links/{store} with unknown store", () => {
|
|
132
|
+
const result = resolveRef("cedar://template-links/ghost-store");
|
|
133
|
+
expect("error" in result).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ─── Entities ────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
it("resolves cedar://entities/{store} to merged JSON array from all entity files", () => {
|
|
139
|
+
const result = resolveRef("cedar://entities/mystore");
|
|
140
|
+
expect("error" in result).toBe(false);
|
|
141
|
+
if ("content" in result) {
|
|
142
|
+
const entities = JSON.parse(result.content) as Array<{ uid: { type: string; id: string } }>;
|
|
143
|
+
expect(entities.length).toBe(2);
|
|
144
|
+
const types = entities.map((e) => e.uid.type);
|
|
145
|
+
expect(types).toContain("DocMgmt::User");
|
|
146
|
+
expect(types).toContain("DocMgmt::Document");
|
|
147
|
+
expect(result.resolved_from).toBe("cedar://entities/mystore");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("resolves cedar://entities/{store}/{file_id} to single entity file content", () => {
|
|
152
|
+
const result = resolveRef("cedar://entities/mystore/users");
|
|
153
|
+
expect("error" in result).toBe(false);
|
|
154
|
+
if ("content" in result) {
|
|
155
|
+
const parsed = JSON.parse(result.content) as Array<{ uid: { type: string } }>;
|
|
156
|
+
expect(parsed[0]?.uid.type).toBe("DocMgmt::User");
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns error for cedar://entities/{store}/{id} with unknown file", () => {
|
|
161
|
+
const result = resolveRef("cedar://entities/mystore/ghost-file");
|
|
162
|
+
expect("error" in result).toBe(true);
|
|
163
|
+
if ("error" in result) expect(result.error).toMatch(/entity file.*not found/i);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns error for cedar://entities/{store} with unknown store", () => {
|
|
167
|
+
const result = resolveRef("cedar://entities/ghost-store");
|
|
168
|
+
expect("error" in result).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── Falsifying input: non-array entity file should surface clean error ──
|
|
172
|
+
|
|
173
|
+
it("returns error when an entity file contains a JSON object instead of array", () => {
|
|
174
|
+
// This is the falsifying test: a plausible mistake is wrapping entities in an object.
|
|
175
|
+
// readAllEntities must reject it, not silently produce wrong output.
|
|
176
|
+
writeFileSync(
|
|
177
|
+
join(tmpDir, "entities", "bad-shape.json"),
|
|
178
|
+
JSON.stringify({ uid: { type: "DocMgmt::User", id: "bob" }, attrs: {}, parents: [] })
|
|
179
|
+
);
|
|
180
|
+
// Reload store so the new file is visible
|
|
181
|
+
storeManager.loadFromRoots([{ uri: `file://${tmpDir}`, name: "mystore" }]);
|
|
182
|
+
const result = resolveRef("cedar://entities/mystore");
|
|
183
|
+
expect("error" in result).toBe(true);
|
|
184
|
+
if ("error" in result) expect(result.error).toMatch(/must contain a JSON array/i);
|
|
185
|
+
});
|
|
186
|
+
});
|