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,459 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { handleAuthorizeBatch } from "../../src/tools/authorize-batch.js";
|
|
3
|
+
import { POLICIES, SCHEMA_JSON, ENTITIES } from "../fixtures/docmgmt.js";
|
|
4
|
+
|
|
5
|
+
const SCHEMA = JSON.stringify(SCHEMA_JSON);
|
|
6
|
+
const SHARED_ENTITIES = JSON.stringify(ENTITIES);
|
|
7
|
+
|
|
8
|
+
// ─── Convenience request builders ─────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function req(principal: string, action: string, resource: string) {
|
|
11
|
+
return { principal, action, resource };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Test 1: Happy path — 3 requests, two Allow, one Deny ─────────────────────
|
|
15
|
+
|
|
16
|
+
describe("cedar_authorize_batch — happy path", () => {
|
|
17
|
+
it("evaluates 3 requests and returns correct decision matrix", async () => {
|
|
18
|
+
const requests = JSON.stringify([
|
|
19
|
+
// alice (admin) reads public doc → Allow
|
|
20
|
+
req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
21
|
+
// charlie (viewer) reads public doc → Allow
|
|
22
|
+
req('DocMgmt::User::"charlie"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
23
|
+
// dave (no role) reads public doc → Deny
|
|
24
|
+
req('DocMgmt::User::"dave"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const result = await handleAuthorizeBatch({
|
|
28
|
+
policies: POLICIES,
|
|
29
|
+
entities: SHARED_ENTITIES,
|
|
30
|
+
requests,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.total).toBe(3);
|
|
34
|
+
expect(result.allowed).toBe(2);
|
|
35
|
+
expect(result.denied).toBe(1);
|
|
36
|
+
expect(result.errored).toBe(0);
|
|
37
|
+
|
|
38
|
+
expect(result.decisions[0]!.decision).toBe("Allow");
|
|
39
|
+
expect(result.decisions[0]!.index).toBe(0);
|
|
40
|
+
expect(result.decisions[0]!.principal).toBe('DocMgmt::User::"alice"');
|
|
41
|
+
|
|
42
|
+
expect(result.decisions[1]!.decision).toBe("Allow");
|
|
43
|
+
expect(result.decisions[1]!.index).toBe(1);
|
|
44
|
+
expect(result.decisions[1]!.principal).toBe('DocMgmt::User::"charlie"');
|
|
45
|
+
|
|
46
|
+
expect(result.decisions[2]!.decision).toBe("Deny");
|
|
47
|
+
expect(result.decisions[2]!.index).toBe(2);
|
|
48
|
+
expect(result.decisions[2]!.principal).toBe('DocMgmt::User::"dave"');
|
|
49
|
+
|
|
50
|
+
expect(result.summary).toContain("3 request");
|
|
51
|
+
expect(result.summary).toContain("2 Allow");
|
|
52
|
+
expect(result.summary).toContain("1 Deny");
|
|
53
|
+
expect(result.summary).toContain("0 Error");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── Test 2: Mixed valid/invalid — one malformed request, others still process ─
|
|
58
|
+
|
|
59
|
+
describe("cedar_authorize_batch — mixed valid/invalid", () => {
|
|
60
|
+
it("marks malformed-entity request as Error; other requests still evaluate", async () => {
|
|
61
|
+
const requests = JSON.stringify([
|
|
62
|
+
// valid — alice allow
|
|
63
|
+
{
|
|
64
|
+
principal: 'DocMgmt::User::"alice"',
|
|
65
|
+
action: 'DocMgmt::Action::"READ"',
|
|
66
|
+
resource: 'DocMgmt::Document::"doc-public"',
|
|
67
|
+
},
|
|
68
|
+
// invalid — per-request entities is bad JSON string
|
|
69
|
+
{
|
|
70
|
+
principal: 'DocMgmt::User::"bob"',
|
|
71
|
+
action: 'DocMgmt::Action::"READ"',
|
|
72
|
+
resource: 'DocMgmt::Document::"doc-public"',
|
|
73
|
+
entities: "NOT_VALID_JSON{{{",
|
|
74
|
+
},
|
|
75
|
+
// valid — dave deny
|
|
76
|
+
{
|
|
77
|
+
principal: 'DocMgmt::User::"dave"',
|
|
78
|
+
action: 'DocMgmt::Action::"READ"',
|
|
79
|
+
resource: 'DocMgmt::Document::"doc-public"',
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const result = await handleAuthorizeBatch({
|
|
84
|
+
policies: POLICIES,
|
|
85
|
+
entities: SHARED_ENTITIES,
|
|
86
|
+
requests,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.total).toBe(3);
|
|
90
|
+
expect(result.allowed).toBe(1);
|
|
91
|
+
expect(result.denied).toBe(1);
|
|
92
|
+
expect(result.errored).toBe(1);
|
|
93
|
+
|
|
94
|
+
// index 0: Alice → Allow
|
|
95
|
+
expect(result.decisions[0]!.decision).toBe("Allow");
|
|
96
|
+
expect(result.decisions[0]!.index).toBe(0);
|
|
97
|
+
|
|
98
|
+
// index 1: bad entities → Error with an explanation
|
|
99
|
+
expect(result.decisions[1]!.decision).toBe("Error");
|
|
100
|
+
expect(result.decisions[1]!.index).toBe(1);
|
|
101
|
+
expect(result.decisions[1]!.error).toBeDefined();
|
|
102
|
+
expect(result.decisions[1]!.error).toBeTruthy();
|
|
103
|
+
|
|
104
|
+
// index 2: Dave → Deny (continues despite index 1 error)
|
|
105
|
+
expect(result.decisions[2]!.decision).toBe("Deny");
|
|
106
|
+
expect(result.decisions[2]!.index).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("marks malformed principal ref as Error; others still evaluate", async () => {
|
|
110
|
+
const requests = JSON.stringify([
|
|
111
|
+
// valid
|
|
112
|
+
req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
113
|
+
// bad principal — not a valid Cedar ref
|
|
114
|
+
{ principal: "bad-format-no-quotes", action: 'DocMgmt::Action::"READ"', resource: 'DocMgmt::Document::"doc-public"' },
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const result = await handleAuthorizeBatch({
|
|
118
|
+
policies: POLICIES,
|
|
119
|
+
entities: SHARED_ENTITIES,
|
|
120
|
+
requests,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.total).toBe(2);
|
|
124
|
+
expect(result.errored).toBe(1);
|
|
125
|
+
expect(result.decisions[1]!.decision).toBe("Error");
|
|
126
|
+
expect(result.decisions[1]!.error).toContain("Invalid Cedar entity reference");
|
|
127
|
+
expect(result.decisions[0]!.decision).toBe("Allow");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ─── Test 3: Empty array ───────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe("cedar_authorize_batch — empty array", () => {
|
|
134
|
+
it("returns total: 0 and summary mentioning no requests", async () => {
|
|
135
|
+
const result = await handleAuthorizeBatch({
|
|
136
|
+
policies: POLICIES,
|
|
137
|
+
entities: SHARED_ENTITIES,
|
|
138
|
+
requests: JSON.stringify([]),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result.total).toBe(0);
|
|
142
|
+
expect(result.allowed).toBe(0);
|
|
143
|
+
expect(result.denied).toBe(0);
|
|
144
|
+
expect(result.errored).toBe(0);
|
|
145
|
+
expect(result.decisions).toHaveLength(0);
|
|
146
|
+
expect(result.summary).toContain("0 request");
|
|
147
|
+
expect(result.summary).toMatch(/no requests/i);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ─── Test 4: determining_policies are reported on Allow ───────────────────────
|
|
152
|
+
|
|
153
|
+
describe("cedar_authorize_batch — determining_policies", () => {
|
|
154
|
+
it("surfaces determining_policies when a permit fires", async () => {
|
|
155
|
+
const requests = JSON.stringify([
|
|
156
|
+
req('DocMgmt::User::"charlie"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
const result = await handleAuthorizeBatch({
|
|
160
|
+
policies: POLICIES,
|
|
161
|
+
entities: SHARED_ENTITIES,
|
|
162
|
+
requests,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(result.total).toBe(1);
|
|
166
|
+
expect(result.decisions[0]!.decision).toBe("Allow");
|
|
167
|
+
expect(result.decisions[0]!.determining_policies).toBeDefined();
|
|
168
|
+
expect(result.decisions[0]!.determining_policies!.length).toBeGreaterThan(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("determining_policies is empty array (not undefined) for a Deny", async () => {
|
|
172
|
+
const requests = JSON.stringify([
|
|
173
|
+
req('DocMgmt::User::"dave"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
const result = await handleAuthorizeBatch({
|
|
177
|
+
policies: POLICIES,
|
|
178
|
+
entities: SHARED_ENTITIES,
|
|
179
|
+
requests,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result.decisions[0]!.decision).toBe("Deny");
|
|
183
|
+
// Deny by default (no permit matched): reason array is empty
|
|
184
|
+
expect(result.decisions[0]!.determining_policies).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("determining_policies is populated for a forbid that overrides a permit", async () => {
|
|
188
|
+
// bob (editor) reading top_secret doc — permit fires but forbid wins → Deny
|
|
189
|
+
// Cedar puts the forbid policy id in reason when it overrides
|
|
190
|
+
const requests = JSON.stringify([
|
|
191
|
+
req('DocMgmt::User::"bob"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-secret"'),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const result = await handleAuthorizeBatch({
|
|
195
|
+
policies: POLICIES,
|
|
196
|
+
entities: SHARED_ENTITIES,
|
|
197
|
+
requests,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(result.decisions[0]!.decision).toBe("Deny");
|
|
201
|
+
// The forbid is the determining policy — Cedar includes it in reason
|
|
202
|
+
expect(result.decisions[0]!.determining_policies).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─── Test 5: Schema violation falsification ────────────────────────────────────
|
|
207
|
+
//
|
|
208
|
+
// Assumption going in: a request with a wrong principal type (e.g. DocMgmt::Role
|
|
209
|
+
// used as principal instead of DocMgmt::User) with schema+validateRequest=true
|
|
210
|
+
// should cause Cedar WASM to return type:"failure", which we map to decision "Error"
|
|
211
|
+
// (NOT a "Deny"). Verified by scratch/probe-schema-violation.ts:
|
|
212
|
+
// type: failure, errors: ["principal type `DocMgmt::Role` is not valid for ..."]
|
|
213
|
+
//
|
|
214
|
+
// Without schema, the same wrong principal type silently returns decision "allow"
|
|
215
|
+
// because no policy check rejects it (admin role is in the admin group).
|
|
216
|
+
// Our implementation maps type:"failure" → "Error" — confirmed below.
|
|
217
|
+
|
|
218
|
+
describe("cedar_authorize_batch — schema violation falsification", () => {
|
|
219
|
+
it("schema-violating request is Error (not thrown, not Deny) — Cedar type:failure mapped", async () => {
|
|
220
|
+
// DocMgmt::Role::"admin" as principal is invalid — schema says principals must be User
|
|
221
|
+
const requests = JSON.stringify([
|
|
222
|
+
req(
|
|
223
|
+
'DocMgmt::Role::"admin"', // WRONG type — not a User
|
|
224
|
+
'DocMgmt::Action::"READ"',
|
|
225
|
+
'DocMgmt::Document::"doc-public"'
|
|
226
|
+
),
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
const result = await handleAuthorizeBatch({
|
|
230
|
+
policies: POLICIES,
|
|
231
|
+
schema: SCHEMA,
|
|
232
|
+
entities: SHARED_ENTITIES,
|
|
233
|
+
requests,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Must NOT throw — result is a structured AuthorizeBatchResult
|
|
237
|
+
expect(result).toBeDefined();
|
|
238
|
+
expect(result.total).toBe(1);
|
|
239
|
+
|
|
240
|
+
const d = result.decisions[0]!;
|
|
241
|
+
// Cedar returns type:"failure" for this → we map to "Error"
|
|
242
|
+
expect(d.decision).toBe("Error");
|
|
243
|
+
expect(d.error).toBeDefined();
|
|
244
|
+
expect(d.error).toContain("DocMgmt::Role");
|
|
245
|
+
|
|
246
|
+
// Counters must be consistent
|
|
247
|
+
expect(result.errored).toBe(1);
|
|
248
|
+
expect(result.allowed).toBe(0);
|
|
249
|
+
expect(result.denied).toBe(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("schema-violating request does NOT cause an Error when schema is omitted (no validation)", async () => {
|
|
253
|
+
// Probe confirmed: without schema, Cedar evaluates freely — wrong type → Allow (admin policy matches)
|
|
254
|
+
const requests = JSON.stringify([
|
|
255
|
+
req(
|
|
256
|
+
'DocMgmt::Role::"admin"',
|
|
257
|
+
'DocMgmt::Action::"READ"',
|
|
258
|
+
'DocMgmt::Document::"doc-public"'
|
|
259
|
+
),
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
const result = await handleAuthorizeBatch({
|
|
263
|
+
policies: POLICIES,
|
|
264
|
+
// no schema — validation disabled
|
|
265
|
+
entities: SHARED_ENTITIES,
|
|
266
|
+
requests,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Without schema the request is not Error; Cedar evaluates it against policies
|
|
270
|
+
// DocMgmt::Role::"admin" is in DocMgmt::Role::"admin" so permit(principal in Role::"admin", ...) fires
|
|
271
|
+
expect(result.decisions[0]!.decision).not.toBe("Error");
|
|
272
|
+
expect(result.errored).toBe(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("non-existent action with schema → Error", async () => {
|
|
276
|
+
const requests = JSON.stringify([
|
|
277
|
+
req(
|
|
278
|
+
'DocMgmt::User::"alice"',
|
|
279
|
+
'DocMgmt::Action::"NONEXISTENT"',
|
|
280
|
+
'DocMgmt::Document::"doc-public"'
|
|
281
|
+
),
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
const result = await handleAuthorizeBatch({
|
|
285
|
+
policies: POLICIES,
|
|
286
|
+
schema: SCHEMA,
|
|
287
|
+
entities: SHARED_ENTITIES,
|
|
288
|
+
requests,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(result.decisions[0]!.decision).toBe("Error");
|
|
292
|
+
expect(result.decisions[0]!.error).toContain("NONEXISTENT");
|
|
293
|
+
expect(result.errored).toBe(1);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ─── Test 6: Shared entities vs per-request entities ──────────────────────────
|
|
298
|
+
|
|
299
|
+
describe("cedar_authorize_batch — shared vs per-request entities", () => {
|
|
300
|
+
it("per-request entities override shared entities for that request", async () => {
|
|
301
|
+
// Shared entities: alice is admin (allow READ)
|
|
302
|
+
// Request for alice but per-request entities has alice with no role (deny)
|
|
303
|
+
const aliceNoRole = JSON.stringify([
|
|
304
|
+
{ uid: { type: "DocMgmt::User", id: "alice" }, attrs: { name: "Alice", email: "a@b.com" }, parents: [] },
|
|
305
|
+
{ uid: { type: "DocMgmt::Document", id: "doc-public" }, attrs: { owner: "alice", classification: "public" }, parents: [] },
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
const requests = JSON.stringify([
|
|
309
|
+
{
|
|
310
|
+
principal: 'DocMgmt::User::"alice"',
|
|
311
|
+
action: 'DocMgmt::Action::"READ"',
|
|
312
|
+
resource: 'DocMgmt::Document::"doc-public"',
|
|
313
|
+
entities: aliceNoRole, // per-request override: alice has no role
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
const result = await handleAuthorizeBatch({
|
|
318
|
+
policies: POLICIES,
|
|
319
|
+
entities: SHARED_ENTITIES, // shared: alice is admin
|
|
320
|
+
requests,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// With per-request entities (no role for alice), Deny should win
|
|
324
|
+
expect(result.decisions[0]!.decision).toBe("Deny");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("uses shared entities when request omits its own", async () => {
|
|
328
|
+
const requests = JSON.stringify([
|
|
329
|
+
req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
const result = await handleAuthorizeBatch({
|
|
333
|
+
policies: POLICIES,
|
|
334
|
+
entities: SHARED_ENTITIES,
|
|
335
|
+
requests,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(result.decisions[0]!.decision).toBe("Allow");
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ─── Test 7: Input validation edge cases ──────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
describe("cedar_authorize_batch — input validation", () => {
|
|
345
|
+
it("returns zeroResult when neither policies nor policy_ref is provided", async () => {
|
|
346
|
+
const result = await handleAuthorizeBatch({
|
|
347
|
+
requests: JSON.stringify([req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"')]),
|
|
348
|
+
});
|
|
349
|
+
expect(result.total).toBe(0);
|
|
350
|
+
expect(result.summary).toContain("required");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("returns zeroResult for non-array requests", async () => {
|
|
354
|
+
const result = await handleAuthorizeBatch({
|
|
355
|
+
policies: POLICIES,
|
|
356
|
+
requests: JSON.stringify({ not: "an array" }),
|
|
357
|
+
});
|
|
358
|
+
expect(result.total).toBe(0);
|
|
359
|
+
expect(result.summary).toContain("array");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("returns zeroResult for invalid requests JSON", async () => {
|
|
363
|
+
const result = await handleAuthorizeBatch({
|
|
364
|
+
policies: POLICIES,
|
|
365
|
+
requests: "NOT_JSON{{{",
|
|
366
|
+
});
|
|
367
|
+
expect(result.total).toBe(0);
|
|
368
|
+
expect(result.summary).toContain("JSON");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("returns zeroResult when shared entities is invalid JSON", async () => {
|
|
372
|
+
const result = await handleAuthorizeBatch({
|
|
373
|
+
policies: POLICIES,
|
|
374
|
+
entities: "NOT_JSON",
|
|
375
|
+
requests: JSON.stringify([]),
|
|
376
|
+
});
|
|
377
|
+
expect(result.total).toBe(0);
|
|
378
|
+
expect(result.summary).toContain("entities");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("summary uses singular 'request' for exactly 1 request", async () => {
|
|
382
|
+
const requests = JSON.stringify([
|
|
383
|
+
req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
384
|
+
]);
|
|
385
|
+
const result = await handleAuthorizeBatch({
|
|
386
|
+
policies: POLICIES,
|
|
387
|
+
entities: SHARED_ENTITIES,
|
|
388
|
+
requests,
|
|
389
|
+
});
|
|
390
|
+
expect(result.summary).toMatch(/^1 request:/);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ─── kickoff-14 14a: H1 stable-ID resolution parity with cedar_authorize ──────
|
|
395
|
+
|
|
396
|
+
const ADMIN_POLICY_TEXT = `permit (principal in DocMgmt::Role::"admin", action, resource);`;
|
|
397
|
+
const EDITOR_POLICY_TEXT = `permit (principal in DocMgmt::Role::"editor", action in [DocMgmt::Action::"READ", DocMgmt::Action::"WRITE"], resource);`;
|
|
398
|
+
|
|
399
|
+
describe("cedar_authorize_batch — kickoff-14 14a stable-ID resolution", () => {
|
|
400
|
+
it("policiesMap input → determining_policies returns basenames, not positional", async () => {
|
|
401
|
+
const requests = JSON.stringify([
|
|
402
|
+
req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
const result = await handleAuthorizeBatch({
|
|
406
|
+
policiesMap: { admin: ADMIN_POLICY_TEXT, editor: EDITOR_POLICY_TEXT },
|
|
407
|
+
entities: SHARED_ENTITIES,
|
|
408
|
+
requests,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(result.decisions[0]!.decision).toBe("Allow");
|
|
412
|
+
expect(result.decisions[0]!.determining_policies).toEqual(["admin"]);
|
|
413
|
+
expect(result.decisions[0]!.determining_policies).not.toContain("policy0");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("policiesMap with @id annotation → @id wins over basename", async () => {
|
|
417
|
+
const annotated = `@id("admin-policy-v2")\n${ADMIN_POLICY_TEXT}`;
|
|
418
|
+
const requests = JSON.stringify([
|
|
419
|
+
req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
420
|
+
]);
|
|
421
|
+
|
|
422
|
+
const result = await handleAuthorizeBatch({
|
|
423
|
+
policiesMap: { admin: annotated, editor: EDITOR_POLICY_TEXT },
|
|
424
|
+
entities: SHARED_ENTITIES,
|
|
425
|
+
requests,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(result.decisions[0]!.decision).toBe("Allow");
|
|
429
|
+
expect(result.decisions[0]!.determining_policies).toEqual(["admin-policy-v2"]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("inline policies string falls back to positional IDs the same way buildStaticPolicies does (regression)", async () => {
|
|
433
|
+
// The flat-string path retains positional fallback because the caller did
|
|
434
|
+
// not supply file basenames. Verifies the existing inline path is unaffected.
|
|
435
|
+
const requests = JSON.stringify([
|
|
436
|
+
req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
437
|
+
]);
|
|
438
|
+
|
|
439
|
+
const result = await handleAuthorizeBatch({
|
|
440
|
+
policies: POLICIES,
|
|
441
|
+
entities: SHARED_ENTITIES,
|
|
442
|
+
requests,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(result.decisions[0]!.decision).toBe("Allow");
|
|
446
|
+
const determining = result.decisions[0]!.determining_policies ?? [];
|
|
447
|
+
expect(determining).toHaveLength(1);
|
|
448
|
+
expect(determining[0]).toMatch(/^policy\d+$/);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("error message names policiesMap as a valid input alternative", async () => {
|
|
452
|
+
const result = await handleAuthorizeBatch({
|
|
453
|
+
requests: JSON.stringify([
|
|
454
|
+
req('DocMgmt::User::"alice"', 'DocMgmt::Action::"READ"', 'DocMgmt::Document::"doc-public"'),
|
|
455
|
+
]),
|
|
456
|
+
});
|
|
457
|
+
expect(result.summary).toContain("policiesMap");
|
|
458
|
+
});
|
|
459
|
+
});
|