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,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,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,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
|
+
}
|