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.
Files changed (215) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.github/workflows/release.yml +42 -0
  4. package/.nvmrc +1 -0
  5. package/CHANGELOG.md +241 -0
  6. package/CONTRIBUTING.md +83 -0
  7. package/LICENSE +182 -0
  8. package/README.md +1635 -0
  9. package/SECURITY.md +37 -0
  10. package/dist/http-server.d.ts +61 -0
  11. package/dist/http-server.d.ts.map +1 -0
  12. package/dist/http-server.js +194 -0
  13. package/dist/http-server.js.map +1 -0
  14. package/dist/index.d.ts +32 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +270 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/parser/policy-ast.d.ts +49 -0
  19. package/dist/parser/policy-ast.d.ts.map +1 -0
  20. package/dist/parser/policy-ast.js +311 -0
  21. package/dist/parser/policy-ast.js.map +1 -0
  22. package/dist/prompts/index.d.ts +38 -0
  23. package/dist/prompts/index.d.ts.map +1 -0
  24. package/dist/prompts/index.js +172 -0
  25. package/dist/prompts/index.js.map +1 -0
  26. package/dist/resources/ref-resolver.d.ts +23 -0
  27. package/dist/resources/ref-resolver.d.ts.map +1 -0
  28. package/dist/resources/ref-resolver.js +128 -0
  29. package/dist/resources/ref-resolver.js.map +1 -0
  30. package/dist/resources/store-manager.d.ts +64 -0
  31. package/dist/resources/store-manager.d.ts.map +1 -0
  32. package/dist/resources/store-manager.js +221 -0
  33. package/dist/resources/store-manager.js.map +1 -0
  34. package/dist/server.d.ts +18 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +539 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/tools/advise/avp-rules.d.ts +49 -0
  39. package/dist/tools/advise/avp-rules.d.ts.map +1 -0
  40. package/dist/tools/advise/avp-rules.js +59 -0
  41. package/dist/tools/advise/avp-rules.js.map +1 -0
  42. package/dist/tools/advise/cedar-patterns.d.ts +24 -0
  43. package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
  44. package/dist/tools/advise/cedar-patterns.js +57 -0
  45. package/dist/tools/advise/cedar-patterns.js.map +1 -0
  46. package/dist/tools/advise/context-builder.d.ts +28 -0
  47. package/dist/tools/advise/context-builder.d.ts.map +1 -0
  48. package/dist/tools/advise/context-builder.js +89 -0
  49. package/dist/tools/advise/context-builder.js.map +1 -0
  50. package/dist/tools/advise/gotchas.d.ts +15 -0
  51. package/dist/tools/advise/gotchas.d.ts.map +1 -0
  52. package/dist/tools/advise/gotchas.js +83 -0
  53. package/dist/tools/advise/gotchas.js.map +1 -0
  54. package/dist/tools/advise.d.ts +96 -0
  55. package/dist/tools/advise.d.ts.map +1 -0
  56. package/dist/tools/advise.js +258 -0
  57. package/dist/tools/advise.js.map +1 -0
  58. package/dist/tools/authorize-batch.d.ts +35 -0
  59. package/dist/tools/authorize-batch.d.ts.map +1 -0
  60. package/dist/tools/authorize-batch.js +262 -0
  61. package/dist/tools/authorize-batch.js.map +1 -0
  62. package/dist/tools/authorize.d.ts +115 -0
  63. package/dist/tools/authorize.d.ts.map +1 -0
  64. package/dist/tools/authorize.js +373 -0
  65. package/dist/tools/authorize.js.map +1 -0
  66. package/dist/tools/check-change.d.ts +19 -0
  67. package/dist/tools/check-change.d.ts.map +1 -0
  68. package/dist/tools/check-change.js +91 -0
  69. package/dist/tools/check-change.js.map +1 -0
  70. package/dist/tools/diff-schema.d.ts +103 -0
  71. package/dist/tools/diff-schema.d.ts.map +1 -0
  72. package/dist/tools/diff-schema.js +379 -0
  73. package/dist/tools/diff-schema.js.map +1 -0
  74. package/dist/tools/diff-stores.d.ts +45 -0
  75. package/dist/tools/diff-stores.d.ts.map +1 -0
  76. package/dist/tools/diff-stores.js +222 -0
  77. package/dist/tools/diff-stores.js.map +1 -0
  78. package/dist/tools/explain.d.ts +80 -0
  79. package/dist/tools/explain.d.ts.map +1 -0
  80. package/dist/tools/explain.js +187 -0
  81. package/dist/tools/explain.js.map +1 -0
  82. package/dist/tools/format.d.ts +11 -0
  83. package/dist/tools/format.d.ts.map +1 -0
  84. package/dist/tools/format.js +20 -0
  85. package/dist/tools/format.js.map +1 -0
  86. package/dist/tools/generate-sample.d.ts +28 -0
  87. package/dist/tools/generate-sample.d.ts.map +1 -0
  88. package/dist/tools/generate-sample.js +568 -0
  89. package/dist/tools/generate-sample.js.map +1 -0
  90. package/dist/tools/link-template.d.ts +17 -0
  91. package/dist/tools/link-template.d.ts.map +1 -0
  92. package/dist/tools/link-template.js +78 -0
  93. package/dist/tools/link-template.js.map +1 -0
  94. package/dist/tools/list-template-links.d.ts +16 -0
  95. package/dist/tools/list-template-links.d.ts.map +1 -0
  96. package/dist/tools/list-template-links.js +22 -0
  97. package/dist/tools/list-template-links.js.map +1 -0
  98. package/dist/tools/list-templates.d.ts +16 -0
  99. package/dist/tools/list-templates.d.ts.map +1 -0
  100. package/dist/tools/list-templates.js +36 -0
  101. package/dist/tools/list-templates.js.map +1 -0
  102. package/dist/tools/translate.d.ts +11 -0
  103. package/dist/tools/translate.d.ts.map +1 -0
  104. package/dist/tools/translate.js +53 -0
  105. package/dist/tools/translate.js.map +1 -0
  106. package/dist/tools/validate-entities.d.ts +19 -0
  107. package/dist/tools/validate-entities.d.ts.map +1 -0
  108. package/dist/tools/validate-entities.js +88 -0
  109. package/dist/tools/validate-entities.js.map +1 -0
  110. package/dist/tools/validate-schema.d.ts +22 -0
  111. package/dist/tools/validate-schema.d.ts.map +1 -0
  112. package/dist/tools/validate-schema.js +89 -0
  113. package/dist/tools/validate-schema.js.map +1 -0
  114. package/dist/tools/validate-template.d.ts +18 -0
  115. package/dist/tools/validate-template.d.ts.map +1 -0
  116. package/dist/tools/validate-template.js +59 -0
  117. package/dist/tools/validate-template.js.map +1 -0
  118. package/dist/tools/validate.d.ts +90 -0
  119. package/dist/tools/validate.d.ts.map +1 -0
  120. package/dist/tools/validate.js +351 -0
  121. package/dist/tools/validate.js.map +1 -0
  122. package/dist/utils/format-detector.d.ts +49 -0
  123. package/dist/utils/format-detector.d.ts.map +1 -0
  124. package/dist/utils/format-detector.js +298 -0
  125. package/dist/utils/format-detector.js.map +1 -0
  126. package/examples/README.md +36 -0
  127. package/examples/abac-multi-tenant/README.md +150 -0
  128. package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
  129. package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
  130. package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
  131. package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
  132. package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
  133. package/examples/abac-multi-tenant/run.ts +92 -0
  134. package/examples/abac-multi-tenant/schema.json +60 -0
  135. package/examples/api-gateway-path-routing/README.md +154 -0
  136. package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
  137. package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
  138. package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
  139. package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
  140. package/examples/api-gateway-path-routing/run.ts +108 -0
  141. package/examples/api-gateway-path-routing/schema.json +54 -0
  142. package/examples/rbac-document-management/README.md +167 -0
  143. package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
  144. package/examples/rbac-document-management/policies/admin.cedar +6 -0
  145. package/examples/rbac-document-management/policies/editor.cedar +6 -0
  146. package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
  147. package/examples/rbac-document-management/policies/viewer.cedar +6 -0
  148. package/examples/rbac-document-management/run.ts +87 -0
  149. package/examples/rbac-document-management/schema.json +57 -0
  150. package/package.json +50 -0
  151. package/src/http-server.ts +239 -0
  152. package/src/index.ts +294 -0
  153. package/src/parser/policy-ast.ts +345 -0
  154. package/src/prompts/README.md +3 -0
  155. package/src/prompts/index.ts +217 -0
  156. package/src/resources/ref-resolver.ts +134 -0
  157. package/src/resources/store-manager.ts +248 -0
  158. package/src/server.ts +711 -0
  159. package/src/tools/advise/avp-rules.ts +70 -0
  160. package/src/tools/advise/cedar-patterns.ts +73 -0
  161. package/src/tools/advise/context-builder.ts +109 -0
  162. package/src/tools/advise/gotchas.ts +92 -0
  163. package/src/tools/advise.ts +366 -0
  164. package/src/tools/authorize-batch.ts +345 -0
  165. package/src/tools/authorize.ts +464 -0
  166. package/src/tools/check-change.ts +119 -0
  167. package/src/tools/diff-schema.ts +510 -0
  168. package/src/tools/diff-stores.ts +298 -0
  169. package/src/tools/explain.ts +278 -0
  170. package/src/tools/format.ts +33 -0
  171. package/src/tools/generate-sample.ts +665 -0
  172. package/src/tools/link-template.ts +109 -0
  173. package/src/tools/list-template-links.ts +41 -0
  174. package/src/tools/list-templates.ts +55 -0
  175. package/src/tools/translate.ts +66 -0
  176. package/src/tools/validate-entities.ts +125 -0
  177. package/src/tools/validate-schema.ts +128 -0
  178. package/src/tools/validate-template.ts +72 -0
  179. package/src/tools/validate.ts +459 -0
  180. package/src/utils/format-detector.ts +356 -0
  181. package/test/fixtures/docmgmt.ts +121 -0
  182. package/test/fixtures/multitenant.ts +163 -0
  183. package/test/index.test.ts +96 -0
  184. package/test/integration/e2e/behavior.test.ts +359 -0
  185. package/test/integration/e2e/edge-cases.test.ts +365 -0
  186. package/test/integration/e2e/failure-modes.test.ts +266 -0
  187. package/test/integration/e2e/protocol.test.ts +252 -0
  188. package/test/integration/http-smoke.test.ts +588 -0
  189. package/test/integration/smoke.test.ts +475 -0
  190. package/test/prompts/prompts.test.ts +173 -0
  191. package/test/property/properties.test.ts +234 -0
  192. package/test/resources/ref-resolver.test.ts +186 -0
  193. package/test/resources/store-manager.test.ts +344 -0
  194. package/test/setup.test.ts +7 -0
  195. package/test/tools/advise/avp-rules.test.ts +76 -0
  196. package/test/tools/advise.test.ts +339 -0
  197. package/test/tools/authorize-batch.test.ts +459 -0
  198. package/test/tools/authorize.test.ts +682 -0
  199. package/test/tools/check-change.test.ts +104 -0
  200. package/test/tools/cross-fixture.test.ts +170 -0
  201. package/test/tools/diff-schema.test.ts +355 -0
  202. package/test/tools/diff-stores.test.ts +291 -0
  203. package/test/tools/explain.test.ts +221 -0
  204. package/test/tools/format.test.ts +33 -0
  205. package/test/tools/generate-sample.test.ts +480 -0
  206. package/test/tools/link-template.test.ts +90 -0
  207. package/test/tools/list-templates.test.ts +151 -0
  208. package/test/tools/translate.test.ts +89 -0
  209. package/test/tools/validate-entities.test.ts +178 -0
  210. package/test/tools/validate-schema.test.ts +86 -0
  211. package/test/tools/validate-template.test.ts +89 -0
  212. package/test/tools/validate.test.ts +331 -0
  213. package/test/utils/format-detector.test.ts +518 -0
  214. package/tsconfig.json +17 -0
  215. package/vitest.config.ts +13 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Offline runner for the abac-multi-tenant example.
3
+ * Usage: npx tsx examples/abac-multi-tenant/run.ts
4
+ */
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { handleAuthorize } from "../../src/tools/authorize.js";
9
+ import { handleValidate } from "../../src/tools/validate.js";
10
+ import { handleExplain } from "../../src/tools/explain.js";
11
+ import { handleCheckChange } from "../../src/tools/check-change.js";
12
+ import { handleGenerateSample } from "../../src/tools/generate-sample.js";
13
+
14
+ const dir = new URL(".", import.meta.url).pathname;
15
+ const read = (p: string) => readFileSync(join(dir, p), "utf8");
16
+
17
+ const schema = read("schema.json");
18
+ const entities = read("entities/users-and-docs.json");
19
+
20
+ const policies = [
21
+ read("policies/owner-full-access.cedar"),
22
+ read("policies/member-read-internal.cedar"),
23
+ read("policies/premium-share-guard.cedar"),
24
+ read("policies/private-doc-guard.cedar"),
25
+ ].join("\n\n");
26
+
27
+ function section(title: string) {
28
+ console.log(`\n${"─".repeat(60)}`);
29
+ console.log(` ${title}`);
30
+ console.log("─".repeat(60));
31
+ }
32
+
33
+ // ─── cedar_validate ───────────────────────────────────────────────────────────
34
+
35
+ section("cedar_validate — all policies against schema");
36
+ const vr = await handleValidate({ policies, schema });
37
+ console.log(` valid: ${vr.valid} | policies: ${vr.policy_count}`);
38
+ if (vr.errors.length) console.log(" errors:", vr.errors);
39
+
40
+ // ─── cedar_authorize ──────────────────────────────────────────────────────────
41
+
42
+ section("cedar_authorize — ABAC decisions");
43
+ const cases = [
44
+ { label: "alice reads q4-roadmap (owner, internal)", p: 'SaaS::User::"alice"', a: 'SaaS::Action::"READ"', r: 'SaaS::Document::"q4-roadmap"', expected: "Allow" },
45
+ { label: "bob reads q4-roadmap (non-owner, internal)", p: 'SaaS::User::"bob"', a: 'SaaS::Action::"READ"', r: 'SaaS::Document::"q4-roadmap"', expected: "Allow" },
46
+ { label: "charlie reads salary-review (non-owner, private)", p: 'SaaS::User::"charlie"', a: 'SaaS::Action::"READ"', r: 'SaaS::Document::"salary-review"', expected: "Deny" },
47
+ { label: "alice reads salary-review (owner, private)", p: 'SaaS::User::"alice"', a: 'SaaS::Action::"READ"', r: 'SaaS::Document::"salary-review"', expected: "Allow" },
48
+ { label: "charlie shares public-changelog (free plan)", p: 'SaaS::User::"charlie"', a: 'SaaS::Action::"SHARE"', r: 'SaaS::Document::"public-changelog"', expected: "Deny" },
49
+ { label: "bob shares public-changelog (pro plan)", p: 'SaaS::User::"bob"', a: 'SaaS::Action::"SHARE"', r: 'SaaS::Document::"public-changelog"', expected: "Allow" },
50
+ ];
51
+
52
+ for (const c of cases) {
53
+ const r = await handleAuthorize({ policies, principal: c.p, action: c.a, resource: c.r, entities, schema });
54
+ const pass = r.decision === c.expected ? "✓" : "✗";
55
+ console.log(` ${pass} ${c.label}: ${r.decision}`);
56
+ }
57
+
58
+ // ─── cedar_explain ────────────────────────────────────────────────────────────
59
+
60
+ section("cedar_explain — premium-share-guard");
61
+ const er = await handleExplain({ policy: read("policies/premium-share-guard.cedar") });
62
+ console.log(` summary: ${er.summary}`);
63
+ console.log(` patterns: ${er.patterns_detected.join(", ")}`);
64
+ console.log(` conditions:`);
65
+ for (const c of er.conditions) console.log(` [${c.kind}] ${c.text}`);
66
+
67
+ // ─── cedar_generate_sample_request ───────────────────────────────────────────
68
+
69
+ section("cedar_generate_sample_request — deny for member-read-internal");
70
+ const gr = await handleGenerateSample({
71
+ policy: read("policies/member-read-internal.cedar"),
72
+ schema,
73
+ target_decision: "deny",
74
+ });
75
+ console.log(` decision: ${gr.decision}`);
76
+ console.log(` resource attrs: ${JSON.stringify(gr.entities.find(e => e.uid.type.includes("Document"))?.attrs)}`);
77
+ console.log(` explanation: ${gr.explanation}`);
78
+
79
+ // ─── cedar_check_policy_change ────────────────────────────────────────────────
80
+
81
+ section("cedar_check_policy_change — owner check vs role check");
82
+ const cr = await handleCheckChange({
83
+ old_policy: `permit(principal, action, resource) when { principal.name == resource.owner_id };`,
84
+ new_policy: `permit(principal in SaaS::Role::"editor", action, resource);`,
85
+ });
86
+ console.log(` can_update_in_place: ${cr.can_update_in_place}`);
87
+ for (const c of cr.changes) {
88
+ console.log(` field=${c.field} in_place_allowed=${c.in_place_allowed}`);
89
+ }
90
+ console.log(` recommendation: ${cr.recommendation}`);
91
+
92
+ console.log("\n✓ All done.");
@@ -0,0 +1,60 @@
1
+ {
2
+ "SaaS": {
3
+ "entityTypes": {
4
+ "User": {
5
+ "memberOfTypes": [],
6
+ "shape": {
7
+ "type": "Record",
8
+ "attributes": {
9
+ "name": { "type": "String", "required": true },
10
+ "plan": { "type": "String", "required": true }
11
+ }
12
+ }
13
+ },
14
+ "Workspace": {
15
+ "memberOfTypes": [],
16
+ "shape": { "type": "Record", "attributes": {} }
17
+ },
18
+ "Document": {
19
+ "memberOfTypes": ["Workspace"],
20
+ "shape": {
21
+ "type": "Record",
22
+ "attributes": {
23
+ "visibility": { "type": "String", "required": true },
24
+ "owner_id": { "type": "String", "required": true }
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "actions": {
30
+ "READ": {
31
+ "appliesTo": {
32
+ "principalTypes": ["User"],
33
+ "resourceTypes": ["Document"],
34
+ "context": { "type": "Record", "attributes": {} }
35
+ }
36
+ },
37
+ "WRITE": {
38
+ "appliesTo": {
39
+ "principalTypes": ["User"],
40
+ "resourceTypes": ["Document"],
41
+ "context": { "type": "Record", "attributes": {} }
42
+ }
43
+ },
44
+ "SHARE": {
45
+ "appliesTo": {
46
+ "principalTypes": ["User"],
47
+ "resourceTypes": ["Document"],
48
+ "context": { "type": "Record", "attributes": {} }
49
+ }
50
+ },
51
+ "DELETE": {
52
+ "appliesTo": {
53
+ "principalTypes": ["User"],
54
+ "resourceTypes": ["Document"],
55
+ "context": { "type": "Record", "attributes": {} }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,154 @@
1
+ # API Gateway Path Routing
2
+
3
+ Role-based access control for a REST API gateway — combining role membership, HTTP method restriction, path matching, and depth limiting in a single Cedar policy.
4
+
5
+ ## What this example covers
6
+
7
+ Three policies that mirror a real API gateway authorization model:
8
+
9
+ - **Role-based action restriction**: `action in [API::Action::"GET", API::Action::"POST"]` — developers can read and create, but not update or delete
10
+ - **Exact path match**: `resource.path == "/api/v1/projects"` — collection endpoint
11
+ - **Path matching with depth limiting**: `like "/api/v1/projects/*" && !(like "/api/v1/projects/*/*")` — allows one path segment deep, blocks sub-resources
12
+ - **No depth limit for viewers**: viewers can GET at any depth, developers cannot POST beyond one level
13
+
14
+ The `cedar_generate_sample_request` results show the depth-limiting in action: the allow path is `/api/v1/projects/x`, the deny path is `/api/v1/projects/x/x`.
15
+
16
+ ## Quick start
17
+
18
+ Configure the MCP server in Claude Code (`.claude/settings.json`):
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "cedar": {
24
+ "command": "npx",
25
+ "args": ["-y", "cedar-mcp-server"]
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ Or run offline:
32
+
33
+ ```bash
34
+ npx tsx examples/api-gateway-path-routing/run.ts
35
+ ```
36
+
37
+ ## Files
38
+
39
+ ```
40
+ schema.json API namespace: User, Role, Endpoint (path, method)
41
+ policies/
42
+ admin-full-access.cedar Admins: any action, any endpoint
43
+ developer-projects.cedar Developers: GET/POST, depth-limited to one level
44
+ viewer-readonly.cedar Viewers: GET only, any depth
45
+ entities/
46
+ users-and-roles.json Alice (admin), Bob (developer), Charlie (viewer)
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Tool examples — copy and paste to Claude Code
52
+
53
+ ### cedar_authorize
54
+
55
+ ```
56
+ Would Bob be allowed to GET /api/v1/projects/proj-1/tasks?
57
+
58
+ Policies: [paste all .cedar files]
59
+ Principal: API::User::"bob"
60
+ Action: API::Action::"GET"
61
+ Resource: API::Endpoint::"req"
62
+ Entities: [paste entities/users-and-roles.json, then add this endpoint entity:
63
+ { "uid": { "type": "API::Endpoint", "id": "req" },
64
+ "attrs": { "path": "/api/v1/projects/proj-1/tasks", "method": "GET" },
65
+ "parents": [] }]
66
+ Schema: [paste schema.json]
67
+ ```
68
+
69
+ Expected: **Deny** — `/api/v1/projects/proj-1/tasks` is two levels deep. The `&&` condition fails because the path matches `like "/api/v1/projects/*/*"`, so `!(like "/api/v1/projects/*/*")` is false.
70
+
71
+ ```
72
+ Would Charlie be allowed to GET /api/v1/projects/proj-1/tasks?
73
+ ```
74
+
75
+ Expected: **Allow** — the `viewer-readonly` policy has no depth limit. Charlie can GET at any depth.
76
+
77
+ ### cedar_explain
78
+
79
+ ```
80
+ Explain this Cedar policy:
81
+
82
+ permit (
83
+ principal in API::Role::"developer",
84
+ action in [API::Action::"GET", API::Action::"POST"],
85
+ resource
86
+ )
87
+ when {
88
+ resource.path == "/api/v1/projects"
89
+ || (
90
+ resource.path like "/api/v1/projects/*"
91
+ && !(resource.path like "/api/v1/projects/*/*")
92
+ )
93
+ };
94
+ ```
95
+
96
+ Expected: a breakdown showing the two-part `||` condition — exact match at collection level, plus depth-limited path match — with the `like` patterns rendered as Cedar syntax.
97
+
98
+ ### cedar_generate_sample_request
99
+
100
+ ```
101
+ Generate a sample request that would be ALLOWED by this policy:
102
+
103
+ permit (
104
+ principal in API::Role::"developer",
105
+ action in [API::Action::"GET", API::Action::"POST"],
106
+ resource
107
+ )
108
+ when {
109
+ resource.path == "/api/v1/projects"
110
+ || (
111
+ resource.path like "/api/v1/projects/*"
112
+ && !(resource.path like "/api/v1/projects/*/*")
113
+ )
114
+ };
115
+
116
+ Schema: [paste schema.json]
117
+ Target decision: allow
118
+ ```
119
+
120
+ Expected: a path like `/api/v1/projects` or `/api/v1/projects/x` (one level deep).
121
+
122
+ ```
123
+ Generate a sample request that would be DENIED.
124
+ Target decision: deny
125
+ ```
126
+
127
+ Expected: a path like `/api/v1/projects/x/x` — two levels deep, which satisfies the negative `like` pattern and makes `!(like "/api/v1/projects/*/*")` false.
128
+
129
+ ---
130
+
131
+ ## Test cases
132
+
133
+ | Principal | Action | Path | Expected | Reason |
134
+ |-----------|--------|------|----------|--------|
135
+ | alice (admin) | DELETE | /api/v1/projects/proj-1 | **Allow** | Admin policy permits everything |
136
+ | bob (developer) | GET | /api/v1/projects | **Allow** | Exact collection match |
137
+ | bob (developer) | POST | /api/v1/projects/proj-1 | **Allow** | One level deep, POST permitted |
138
+ | bob (developer) | GET | /api/v1/projects/proj-1/tasks | **Deny** | Two levels deep, depth limit triggered |
139
+ | bob (developer) | DELETE | /api/v1/projects | **Deny** | DELETE not in developer action list |
140
+ | charlie (viewer) | GET | /api/v1/projects/proj-1 | **Allow** | Viewer can GET one level |
141
+ | charlie (viewer) | GET | /api/v1/projects/proj-1/tasks | **Allow** | Viewer has no depth limit |
142
+ | charlie (viewer) | POST | /api/v1/projects | **Deny** | Viewer cannot POST |
143
+
144
+ ---
145
+
146
+ ## Common pitfalls in this pattern
147
+
148
+ **Cedar `*` matches `/`** — unlike shell globs or URL matchers, Cedar's `*` wildcard matches any character sequence including path separators. `like "/api/v1/*"` matches `/api/v1/projects/proj-1/tasks/comments`. Depth limiting requires explicit negation: `&& !(like "/api/v1/projects/*/*")`.
149
+
150
+ **Viewer and developer have different depth semantics — intentionally.** The developer policy has depth-limiting because developers are expected to act on individual resources, not traverse sub-resource trees. The viewer policy has no depth limit because read-only access to deeper paths is lower risk. This is a deliberate design choice, not an oversight.
151
+
152
+ **Path matching is evaluated at request time, not at policy store creation.** The `resource.path` attribute comes from your entity store — the value your application puts there when building the authorization request. Cedar does not parse URLs. If your application sends `path: "/api/v1/projects/proj-1"` in the entity, that is what Cedar evaluates. Normalizing paths (trailing slash, URL encoding) is your application's responsibility.
153
+
154
+ **Action names are Cedar entity IDs, not HTTP methods.** `API::Action::"GET"` is a Cedar entity identifier that happens to be named `GET`. You decide the mapping between Cedar action IDs and HTTP methods in your application layer. Cedar doesn't know what HTTP is.
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "uid": { "type": "API::User", "id": "alice" },
4
+ "attrs": {},
5
+ "parents": [{ "type": "API::Role", "id": "admin" }]
6
+ },
7
+ {
8
+ "uid": { "type": "API::User", "id": "bob" },
9
+ "attrs": {},
10
+ "parents": [{ "type": "API::Role", "id": "developer" }]
11
+ },
12
+ {
13
+ "uid": { "type": "API::User", "id": "charlie" },
14
+ "attrs": {},
15
+ "parents": [{ "type": "API::Role", "id": "viewer" }]
16
+ },
17
+ { "uid": { "type": "API::Role", "id": "admin" }, "attrs": {}, "parents": [] },
18
+ { "uid": { "type": "API::Role", "id": "developer" }, "attrs": {}, "parents": [] },
19
+ { "uid": { "type": "API::Role", "id": "viewer" }, "attrs": {}, "parents": [] }
20
+ ]
@@ -0,0 +1,6 @@
1
+ // Admins have full access to all endpoints and methods
2
+ permit (
3
+ principal in API::Role::"admin",
4
+ action,
5
+ resource
6
+ );
@@ -0,0 +1,14 @@
1
+ // Developers can GET and POST to the projects collection and individual projects
2
+ // Depth-limiting: allows /api/v1/projects and /api/v1/projects/{id} but NOT deeper paths
3
+ permit (
4
+ principal in API::Role::"developer",
5
+ action in [API::Action::"GET", API::Action::"POST"],
6
+ resource
7
+ )
8
+ when {
9
+ resource.path == "/api/v1/projects"
10
+ || (
11
+ resource.path like "/api/v1/projects/*"
12
+ && !(resource.path like "/api/v1/projects/*/*")
13
+ )
14
+ };
@@ -0,0 +1,10 @@
1
+ // Viewers can only GET — collection level and one level deep
2
+ permit (
3
+ principal in API::Role::"viewer",
4
+ action == API::Action::"GET",
5
+ resource
6
+ )
7
+ when {
8
+ resource.path == "/api/v1/projects"
9
+ || resource.path like "/api/v1/projects/*"
10
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Offline runner for the api-gateway-path-routing example.
3
+ * Usage: npx tsx examples/api-gateway-path-routing/run.ts
4
+ */
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { handleAuthorize } from "../../src/tools/authorize.js";
9
+ import { handleValidate } from "../../src/tools/validate.js";
10
+ import { handleExplain } from "../../src/tools/explain.js";
11
+ import { handleGenerateSample } from "../../src/tools/generate-sample.js";
12
+
13
+ const dir = new URL(".", import.meta.url).pathname;
14
+ const read = (p: string) => readFileSync(join(dir, p), "utf8");
15
+
16
+ const schema = read("schema.json");
17
+ const entities = read("entities/users-and-roles.json");
18
+
19
+ const policies = [
20
+ read("policies/admin-full-access.cedar"),
21
+ read("policies/developer-projects.cedar"),
22
+ read("policies/viewer-readonly.cedar"),
23
+ ].join("\n\n");
24
+
25
+ function section(title: string) {
26
+ console.log(`\n${"─".repeat(60)}`);
27
+ console.log(` ${title}`);
28
+ console.log("─".repeat(60));
29
+ }
30
+
31
+ // Helper: build an endpoint entity on the fly for a given path + method
32
+ function withEndpoint(path: string, method: string) {
33
+ const base = JSON.parse(entities) as unknown[];
34
+ base.push({
35
+ uid: { type: "API::Endpoint", id: "req" },
36
+ attrs: { path, method },
37
+ parents: [],
38
+ });
39
+ return JSON.stringify(base);
40
+ }
41
+
42
+ // ─── cedar_validate ───────────────────────────────────────────────────────────
43
+
44
+ section("cedar_validate — all policies against schema");
45
+ const vr = await handleValidate({ policies, schema });
46
+ console.log(` valid: ${vr.valid} | policies: ${vr.policy_count}`);
47
+
48
+ // ─── cedar_authorize — path depth tests ───────────────────────────────────────
49
+
50
+ section("cedar_authorize — path-depth routing decisions");
51
+
52
+ const routeCases = [
53
+ { label: "bob GET /api/v1/projects (collection)", user: "bob", action: "GET", path: "/api/v1/projects", expected: "Allow" },
54
+ { label: "bob POST /api/v1/projects/proj-1 (one level deep)", user: "bob", action: "POST", path: "/api/v1/projects/proj-1", expected: "Allow" },
55
+ { label: "bob GET /api/v1/projects/proj-1/tasks (two levels — blocked)", user: "bob", action: "GET", path: "/api/v1/projects/proj-1/tasks", expected: "Deny" },
56
+ { label: "bob DELETE /api/v1/projects (DELETE not permitted)", user: "bob", action: "DELETE", path: "/api/v1/projects", expected: "Deny" },
57
+ { label: "charlie GET /api/v1/projects/proj-1 (viewer, one level)", user: "charlie", action: "GET", path: "/api/v1/projects/proj-1", expected: "Allow" },
58
+ { label: "charlie GET /api/v1/projects/proj-1/tasks (viewer, no depth limit — allowed)", user: "charlie", action: "GET", path: "/api/v1/projects/proj-1/tasks", expected: "Allow" },
59
+ { label: "alice DELETE /api/v1/projects/proj-1 (admin, anything)", user: "alice", action: "DELETE", path: "/api/v1/projects/proj-1", expected: "Allow" },
60
+ ];
61
+
62
+ for (const c of routeCases) {
63
+ const ents = withEndpoint(c.path, c.action);
64
+ const r = await handleAuthorize({
65
+ policies,
66
+ principal: `API::User::"${c.user}"`,
67
+ action: `API::Action::"${c.action}"`,
68
+ resource: 'API::Endpoint::"req"',
69
+ entities: ents,
70
+ schema,
71
+ });
72
+ const pass = r.decision === c.expected ? "✓" : "✗";
73
+ console.log(` ${pass} ${c.label}: ${r.decision}`);
74
+ }
75
+
76
+ // ─── cedar_explain ────────────────────────────────────────────────────────────
77
+
78
+ section("cedar_explain — developer-projects (path-matching + depth limit)");
79
+ const er = await handleExplain({ policy: read("policies/developer-projects.cedar") });
80
+ console.log(` summary: ${er.summary}`);
81
+ console.log(` conditions:`);
82
+ for (const c of er.conditions) console.log(` [${c.kind}] ${c.text}`);
83
+
84
+ // ─── cedar_generate_sample_request ───────────────────────────────────────────
85
+
86
+ section("cedar_generate_sample_request — allow path for developer-projects");
87
+ const allowSample = await handleGenerateSample({
88
+ policy: read("policies/developer-projects.cedar"),
89
+ schema,
90
+ target_decision: "allow",
91
+ });
92
+ console.log(` decision: ${allowSample.decision}`);
93
+ const ep = allowSample.entities.find(e => e.uid.type.includes("Endpoint"));
94
+ console.log(` generated path: ${ep?.attrs?.path}`);
95
+ console.log(` explanation: ${allowSample.explanation}`);
96
+
97
+ section("cedar_generate_sample_request — deny path (too deep)");
98
+ const denySample = await handleGenerateSample({
99
+ policy: read("policies/developer-projects.cedar"),
100
+ schema,
101
+ target_decision: "deny",
102
+ });
103
+ console.log(` decision: ${denySample.decision}`);
104
+ const ep2 = denySample.entities.find(e => e.uid.type.includes("Endpoint"));
105
+ console.log(` generated path: ${ep2?.attrs?.path}`);
106
+ console.log(` explanation: ${denySample.explanation}`);
107
+
108
+ console.log("\n✓ All done.");
@@ -0,0 +1,54 @@
1
+ {
2
+ "API": {
3
+ "entityTypes": {
4
+ "User": {
5
+ "memberOfTypes": ["Role"],
6
+ "shape": { "type": "Record", "attributes": {} }
7
+ },
8
+ "Role": {
9
+ "memberOfTypes": [],
10
+ "shape": { "type": "Record", "attributes": {} }
11
+ },
12
+ "Endpoint": {
13
+ "memberOfTypes": [],
14
+ "shape": {
15
+ "type": "Record",
16
+ "attributes": {
17
+ "path": { "type": "String", "required": true },
18
+ "method": { "type": "String", "required": true }
19
+ }
20
+ }
21
+ }
22
+ },
23
+ "actions": {
24
+ "GET": {
25
+ "appliesTo": {
26
+ "principalTypes": ["User"],
27
+ "resourceTypes": ["Endpoint"],
28
+ "context": { "type": "Record", "attributes": {} }
29
+ }
30
+ },
31
+ "POST": {
32
+ "appliesTo": {
33
+ "principalTypes": ["User"],
34
+ "resourceTypes": ["Endpoint"],
35
+ "context": { "type": "Record", "attributes": {} }
36
+ }
37
+ },
38
+ "PUT": {
39
+ "appliesTo": {
40
+ "principalTypes": ["User"],
41
+ "resourceTypes": ["Endpoint"],
42
+ "context": { "type": "Record", "attributes": {} }
43
+ }
44
+ },
45
+ "DELETE": {
46
+ "appliesTo": {
47
+ "principalTypes": ["User"],
48
+ "resourceTypes": ["Endpoint"],
49
+ "context": { "type": "Record", "attributes": {} }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }