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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cedar input format detection and normalization.
|
|
3
|
+
*
|
|
4
|
+
* Three AVP SDK variants exist in the wild — all need conversion to Cedar WASM format:
|
|
5
|
+
*
|
|
6
|
+
* Ruby SDK (snake_case): identifier.entity_type / entity_id, string/long/boolean
|
|
7
|
+
* Python/JS SDK (camelCase): identifier.entityType / entityId, string/long/boolean
|
|
8
|
+
* Official API/Console (PascalCase): Identifier.EntityType / EntityId, String/Long/Boolean
|
|
9
|
+
*
|
|
10
|
+
* Cedar WASM format:
|
|
11
|
+
* uid: { type, id }, attrs: { key: rawValue }, parents: [{ type, id }]
|
|
12
|
+
* Entity refs in attrs: { __entity: { type, id } }
|
|
13
|
+
* Extension types: { __extn: { fn, arg } }
|
|
14
|
+
*
|
|
15
|
+
* Detection strategy: case-insensitive key lookup handles all three casing variants
|
|
16
|
+
* in a single code path. One normalizer to rule them all.
|
|
17
|
+
*
|
|
18
|
+
* Cedar CLI format (uid.__entity wrapper): WASM accepts natively — no conversion needed.
|
|
19
|
+
*
|
|
20
|
+
* Attribute value wrapper detection rule:
|
|
21
|
+
* Single-key object whose key (lowercased) is a known AVP type name AND whose value
|
|
22
|
+
* is the matching primitive. Multi-key objects are Cedar Records — not touched.
|
|
23
|
+
*
|
|
24
|
+
* Limitation: a Cedar Record with exactly one field named "string"/"long"/"boolean"
|
|
25
|
+
* would be misidentified. Adding a second field removes the ambiguity.
|
|
26
|
+
*
|
|
27
|
+
* Sources confirmed by SDK docs (2026-05-20):
|
|
28
|
+
* Ruby: entity_type/entity_id/entity_identifier (snake_case)
|
|
29
|
+
* Python/JS: entityType/entityId/entityIdentifier (camelCase)
|
|
30
|
+
* Official API: EntityType/EntityId/EntityIdentifier (PascalCase)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export type InputFormat = "cedar" | "avp" | "cedar_cli";
|
|
34
|
+
|
|
35
|
+
export interface FormatDetectionResult {
|
|
36
|
+
format: InputFormat;
|
|
37
|
+
confidence: "high" | "medium";
|
|
38
|
+
note: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface NormalizedRef {
|
|
42
|
+
type: string;
|
|
43
|
+
id: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface NormalizedRefError {
|
|
47
|
+
error: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Case-insensitive key access ──────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/** Find the value of a key case-insensitively. First match wins. */
|
|
53
|
+
function getCI(obj: Record<string, unknown>, key: string): unknown {
|
|
54
|
+
const lower = key.toLowerCase();
|
|
55
|
+
const found = Object.keys(obj).find((k) => k.toLowerCase() === lower);
|
|
56
|
+
return found !== undefined ? obj[found] : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasKeyCI(obj: Record<string, unknown>, key: string): boolean {
|
|
60
|
+
const lower = key.toLowerCase();
|
|
61
|
+
return Object.keys(obj).some((k) => k.toLowerCase() === lower);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Detection ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function detectFormat(
|
|
67
|
+
entities: unknown[],
|
|
68
|
+
principal: unknown,
|
|
69
|
+
action: unknown,
|
|
70
|
+
resource: unknown
|
|
71
|
+
): FormatDetectionResult {
|
|
72
|
+
// AVP principal/action/resource — any casing of entity_type/entityType/EntityType
|
|
73
|
+
if (isAvpRef(principal) || isAvpActionRef(action) || isAvpRef(resource)) {
|
|
74
|
+
return {
|
|
75
|
+
format: "avp",
|
|
76
|
+
confidence: "high",
|
|
77
|
+
note: "Principal, action, or resource is in AVP format (entity_type/entityType/EntityType keys). Automatically converted to Cedar format.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// AVP entity list: `identifier` key (case-insensitive) is the clearest signal
|
|
82
|
+
if (entities.some(hasAvpIdentifierKey)) {
|
|
83
|
+
return {
|
|
84
|
+
format: "avp",
|
|
85
|
+
confidence: "high",
|
|
86
|
+
note: "Entities are in AVP format (identifier/Identifier key, typed attribute wrappers). Automatically converted to Cedar format.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// AVP-typed attribute values without identifier key (partial AVP)
|
|
91
|
+
if (entities.some(hasAvpTypedAttributeValues)) {
|
|
92
|
+
return {
|
|
93
|
+
format: "avp",
|
|
94
|
+
confidence: "medium",
|
|
95
|
+
note: "Entity attributes appear to use AVP typed wrappers (string/long/boolean/set/record). Automatically unwrapped to raw Cedar values.",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Cedar CLI uid.__entity wrapper — WASM accepts natively, no conversion needed
|
|
100
|
+
if (entities.some(hasCedarCliEntityWrapper)) {
|
|
101
|
+
return {
|
|
102
|
+
format: "cedar_cli",
|
|
103
|
+
confidence: "high",
|
|
104
|
+
note: "Entity UIDs use the __entity wrapper (Cedar CLI format). Compatible with Cedar WASM — no conversion needed.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
format: "cedar",
|
|
110
|
+
confidence: "high",
|
|
111
|
+
note: "Input is in Cedar/WASM format.",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Normalization ────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export function normalizeEntities(entities: unknown[], format: InputFormat): unknown[] {
|
|
118
|
+
if (format === "cedar" || format === "cedar_cli") return entities;
|
|
119
|
+
return entities.map(normalizeAvpEntity);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function normalizePrincipalRef(ref: unknown): NormalizedRef | NormalizedRefError {
|
|
123
|
+
// Cedar string literal: 'Ns::Type::"id"'
|
|
124
|
+
if (typeof ref === "string") {
|
|
125
|
+
const match = ref.match(/^(.+)::"(.+)"$/);
|
|
126
|
+
if (!match) return { error: `Invalid Cedar entity reference: "${ref}". Expected: Namespace::Type::"id"` };
|
|
127
|
+
return { type: match[1]!, id: match[2]! };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (typeof ref !== "object" || ref === null) {
|
|
131
|
+
return { error: `Unrecognized entity reference format: ${JSON.stringify(ref)}` };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const obj = ref as Record<string, unknown>;
|
|
135
|
+
|
|
136
|
+
// WASM native: { type, id }
|
|
137
|
+
if (typeof obj["type"] === "string" && typeof obj["id"] === "string") {
|
|
138
|
+
return { type: obj["type"], id: obj["id"] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// WASM Cedar CLI: { __entity: { type, id } }
|
|
142
|
+
if (obj["__entity"] && typeof obj["__entity"] === "object") {
|
|
143
|
+
const inner = obj["__entity"] as Record<string, unknown>;
|
|
144
|
+
if (typeof inner["type"] === "string" && typeof inner["id"] === "string") {
|
|
145
|
+
return { type: inner["type"], id: inner["id"] };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// AVP entity ref — all three casings (entity_type / entityType / EntityType)
|
|
150
|
+
const entityType = getCI(obj, "entity_type") ?? getCI(obj, "entityType") ?? getCI(obj, "EntityType");
|
|
151
|
+
const entityId = getCI(obj, "entity_id") ?? getCI(obj, "entityId") ?? getCI(obj, "EntityId");
|
|
152
|
+
if (typeof entityType === "string" && typeof entityId === "string") {
|
|
153
|
+
return { type: entityType, id: entityId };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// AVP action ref — all three casings (action_type / actionType / ActionType)
|
|
157
|
+
const actionType = getCI(obj, "action_type") ?? getCI(obj, "actionType") ?? getCI(obj, "ActionType");
|
|
158
|
+
const actionId = getCI(obj, "action_id") ?? getCI(obj, "actionId") ?? getCI(obj, "ActionId");
|
|
159
|
+
if (typeof actionType === "string" && typeof actionId === "string") {
|
|
160
|
+
return { type: actionType, id: actionId };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { error: `Unrecognized entity reference format: ${JSON.stringify(ref)}` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function unwrapAvpAttributes(
|
|
167
|
+
attrs: Record<string, unknown>
|
|
168
|
+
): Record<string, unknown> {
|
|
169
|
+
const result: Record<string, unknown> = {};
|
|
170
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
171
|
+
result[key] = unwrapAvpValue(value);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Private: detection helpers ───────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function isAvpRef(ref: unknown): boolean {
|
|
179
|
+
if (typeof ref !== "object" || ref === null) return false;
|
|
180
|
+
const obj = ref as Record<string, unknown>;
|
|
181
|
+
const hasType =
|
|
182
|
+
typeof getCI(obj, "entity_type") === "string" ||
|
|
183
|
+
typeof getCI(obj, "entityType") === "string" ||
|
|
184
|
+
typeof getCI(obj, "EntityType") === "string";
|
|
185
|
+
const hasId =
|
|
186
|
+
typeof getCI(obj, "entity_id") === "string" ||
|
|
187
|
+
typeof getCI(obj, "entityId") === "string" ||
|
|
188
|
+
typeof getCI(obj, "EntityId") === "string";
|
|
189
|
+
return hasType && hasId;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isAvpActionRef(ref: unknown): boolean {
|
|
193
|
+
if (typeof ref !== "object" || ref === null) return false;
|
|
194
|
+
const obj = ref as Record<string, unknown>;
|
|
195
|
+
const hasType =
|
|
196
|
+
typeof getCI(obj, "action_type") === "string" ||
|
|
197
|
+
typeof getCI(obj, "actionType") === "string" ||
|
|
198
|
+
typeof getCI(obj, "ActionType") === "string";
|
|
199
|
+
const hasId =
|
|
200
|
+
typeof getCI(obj, "action_id") === "string" ||
|
|
201
|
+
typeof getCI(obj, "actionId") === "string" ||
|
|
202
|
+
typeof getCI(obj, "ActionId") === "string";
|
|
203
|
+
return hasType && hasId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasAvpIdentifierKey(entity: unknown): boolean {
|
|
207
|
+
if (typeof entity !== "object" || entity === null) return false;
|
|
208
|
+
return hasKeyCI(entity as Record<string, unknown>, "identifier");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function hasAvpTypedAttributeValues(entity: unknown): boolean {
|
|
212
|
+
if (typeof entity !== "object" || entity === null) return false;
|
|
213
|
+
const e = entity as Record<string, unknown>;
|
|
214
|
+
const rawAttrs =
|
|
215
|
+
getCI(e, "attributes") ??
|
|
216
|
+
getCI(e, "Attributes") ??
|
|
217
|
+
e["attrs"];
|
|
218
|
+
if (!rawAttrs || typeof rawAttrs !== "object") return false;
|
|
219
|
+
return Object.values(rawAttrs as Record<string, unknown>).some(isAvpTypedValue);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function hasCedarCliEntityWrapper(entity: unknown): boolean {
|
|
223
|
+
if (typeof entity !== "object" || entity === null) return false;
|
|
224
|
+
const e = entity as Record<string, unknown>;
|
|
225
|
+
const uid = e["uid"];
|
|
226
|
+
if (typeof uid !== "object" || uid === null) return false;
|
|
227
|
+
return "__entity" in (uid as Record<string, unknown>);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Detects AVP typed value wrappers (case-insensitive type key).
|
|
232
|
+
* Rule: single-key object whose key lowercased is a known AVP type name with matching value type.
|
|
233
|
+
* Multi-key objects are Cedar Records.
|
|
234
|
+
*/
|
|
235
|
+
function isAvpTypedValue(v: unknown): boolean {
|
|
236
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
|
|
237
|
+
const keys = Object.keys(v as object);
|
|
238
|
+
if (keys.length !== 1) return false;
|
|
239
|
+
const key = keys[0]!.toLowerCase();
|
|
240
|
+
const val = (v as Record<string, unknown>)[keys[0]!];
|
|
241
|
+
return (
|
|
242
|
+
(key === "string" && typeof val === "string") ||
|
|
243
|
+
(key === "long" && typeof val === "number") ||
|
|
244
|
+
(key === "boolean" && typeof val === "boolean") ||
|
|
245
|
+
key === "entityidentifier" ||
|
|
246
|
+
key === "entity_identifier" ||
|
|
247
|
+
key === "set" ||
|
|
248
|
+
key === "record" ||
|
|
249
|
+
key === "ipaddr" ||
|
|
250
|
+
key === "ipaddress" ||
|
|
251
|
+
key === "decimal" ||
|
|
252
|
+
key === "datetime" ||
|
|
253
|
+
key === "duration"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Private: value unwrapping ────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
function unwrapAvpValue(v: unknown): unknown {
|
|
260
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return v;
|
|
261
|
+
|
|
262
|
+
const keys = Object.keys(v as object);
|
|
263
|
+
if (keys.length !== 1) return v; // Multi-key object = Cedar Record, not AVP wrapper
|
|
264
|
+
|
|
265
|
+
const key = keys[0]!;
|
|
266
|
+
const lowerKey = key.toLowerCase();
|
|
267
|
+
const val = (v as Record<string, unknown>)[key];
|
|
268
|
+
|
|
269
|
+
switch (lowerKey) {
|
|
270
|
+
case "string":
|
|
271
|
+
return typeof val === "string" ? val : v;
|
|
272
|
+
case "long":
|
|
273
|
+
return typeof val === "number" ? val : v;
|
|
274
|
+
case "boolean":
|
|
275
|
+
return typeof val === "boolean" ? val : v;
|
|
276
|
+
|
|
277
|
+
// Entity reference → WASM __entity
|
|
278
|
+
case "entityidentifier":
|
|
279
|
+
case "entity_identifier": {
|
|
280
|
+
const ref = resolveAvpEntityRef(val);
|
|
281
|
+
return ref ? { __entity: ref } : v;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Set → array (recursively normalize values)
|
|
285
|
+
case "set":
|
|
286
|
+
return Array.isArray(val) ? val.map(unwrapAvpValue) : v;
|
|
287
|
+
|
|
288
|
+
// Record → object (recursively normalize values)
|
|
289
|
+
case "record":
|
|
290
|
+
if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
|
291
|
+
return unwrapAvpAttributes(val as Record<string, unknown>);
|
|
292
|
+
}
|
|
293
|
+
return v;
|
|
294
|
+
|
|
295
|
+
// Cedar extension types → WASM __extn format
|
|
296
|
+
case "ipaddr":
|
|
297
|
+
case "ipaddress":
|
|
298
|
+
return typeof val === "string" ? { __extn: { fn: "ip", arg: val } } : v;
|
|
299
|
+
case "decimal":
|
|
300
|
+
return typeof val === "string" ? { __extn: { fn: "decimal", arg: val } } : v;
|
|
301
|
+
case "datetime":
|
|
302
|
+
return typeof val === "string" ? { __extn: { fn: "datetime", arg: val } } : v;
|
|
303
|
+
case "duration":
|
|
304
|
+
return typeof val === "string" ? { __extn: { fn: "duration", arg: val } } : v;
|
|
305
|
+
|
|
306
|
+
default:
|
|
307
|
+
return v;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Resolves an AVP entity reference object (any casing) to { type, id }. */
|
|
312
|
+
function resolveAvpEntityRef(ref: unknown): { type: string; id: string } | null {
|
|
313
|
+
if (typeof ref !== "object" || ref === null) return null;
|
|
314
|
+
const obj = ref as Record<string, unknown>;
|
|
315
|
+
const type =
|
|
316
|
+
(getCI(obj, "entity_type") ?? getCI(obj, "entityType") ?? getCI(obj, "EntityType")) as string | undefined;
|
|
317
|
+
const id =
|
|
318
|
+
(getCI(obj, "entity_id") ?? getCI(obj, "entityId") ?? getCI(obj, "EntityId")) as string | undefined;
|
|
319
|
+
if (type && id) return { type, id };
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ─── Private: entity normalization ───────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
function normalizeAvpEntity(entity: unknown): unknown {
|
|
326
|
+
if (typeof entity !== "object" || entity === null) return entity;
|
|
327
|
+
const e = entity as Record<string, unknown>;
|
|
328
|
+
|
|
329
|
+
// UID: find identifier/Identifier key (any casing), convert to { type, id }
|
|
330
|
+
const identifierKey = Object.keys(e).find((k) => k.toLowerCase() === "identifier");
|
|
331
|
+
let uid: unknown;
|
|
332
|
+
if (identifierKey) {
|
|
333
|
+
const idObj = e[identifierKey] as Record<string, unknown>;
|
|
334
|
+
const type = getCI(idObj, "entity_type") ?? getCI(idObj, "entityType") ?? getCI(idObj, "EntityType");
|
|
335
|
+
const id = getCI(idObj, "entity_id") ?? getCI(idObj, "entityId") ?? getCI(idObj, "EntityId");
|
|
336
|
+
uid = { type, id };
|
|
337
|
+
} else {
|
|
338
|
+
uid = e["uid"];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Attrs: find attributes/Attributes key (any casing), fall back to attrs
|
|
342
|
+
const attrsKey = Object.keys(e).find((k) => k.toLowerCase() === "attributes");
|
|
343
|
+
const rawAttrs = (attrsKey ? e[attrsKey] : e["attrs"]) ?? {};
|
|
344
|
+
const attrs = unwrapAvpAttributes(rawAttrs as Record<string, unknown>);
|
|
345
|
+
|
|
346
|
+
// Parents: find parents/Parents key (any casing), convert entity_type/entityType/EntityType → type/id
|
|
347
|
+
const parentsKey = Object.keys(e).find((k) => k.toLowerCase() === "parents");
|
|
348
|
+
const rawParents = ((parentsKey ? e[parentsKey] : e["parents"]) ?? []) as unknown[];
|
|
349
|
+
const parents = rawParents.map((p) => {
|
|
350
|
+
if (typeof p !== "object" || p === null) return p;
|
|
351
|
+
const ref = resolveAvpEntityRef(p);
|
|
352
|
+
return ref ?? p;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return { uid, attrs, parents };
|
|
356
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const POLICIES = `
|
|
2
|
+
permit (
|
|
3
|
+
principal in DocMgmt::Role::"admin",
|
|
4
|
+
action,
|
|
5
|
+
resource
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
permit (
|
|
9
|
+
principal in DocMgmt::Role::"editor",
|
|
10
|
+
action in [DocMgmt::Action::"READ", DocMgmt::Action::"WRITE"],
|
|
11
|
+
resource
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
permit (
|
|
15
|
+
principal in DocMgmt::Role::"viewer",
|
|
16
|
+
action == DocMgmt::Action::"READ",
|
|
17
|
+
resource
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
forbid (
|
|
21
|
+
principal,
|
|
22
|
+
action,
|
|
23
|
+
resource
|
|
24
|
+
)
|
|
25
|
+
when {
|
|
26
|
+
resource.classification == "top_secret"
|
|
27
|
+
}
|
|
28
|
+
unless {
|
|
29
|
+
principal in DocMgmt::Role::"admin"
|
|
30
|
+
};
|
|
31
|
+
`.trim();
|
|
32
|
+
|
|
33
|
+
export const SCHEMA_JSON = {
|
|
34
|
+
DocMgmt: {
|
|
35
|
+
entityTypes: {
|
|
36
|
+
User: {
|
|
37
|
+
memberOfTypes: ["Role"],
|
|
38
|
+
shape: {
|
|
39
|
+
type: "Record",
|
|
40
|
+
attributes: {
|
|
41
|
+
name: { type: "String", required: true },
|
|
42
|
+
email: { type: "String", required: true },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
Role: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
|
|
47
|
+
Document: {
|
|
48
|
+
memberOfTypes: ["Folder"],
|
|
49
|
+
shape: {
|
|
50
|
+
type: "Record",
|
|
51
|
+
attributes: {
|
|
52
|
+
owner: { type: "String", required: true },
|
|
53
|
+
classification: { type: "String", required: true },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
Folder: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
|
|
58
|
+
},
|
|
59
|
+
actions: {
|
|
60
|
+
READ: {
|
|
61
|
+
appliesTo: {
|
|
62
|
+
principalTypes: ["User"],
|
|
63
|
+
resourceTypes: ["Document"],
|
|
64
|
+
context: { type: "Record", attributes: {} },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
WRITE: {
|
|
68
|
+
appliesTo: {
|
|
69
|
+
principalTypes: ["User"],
|
|
70
|
+
resourceTypes: ["Document"],
|
|
71
|
+
context: { type: "Record", attributes: {} },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
DELETE: {
|
|
75
|
+
appliesTo: {
|
|
76
|
+
principalTypes: ["User"],
|
|
77
|
+
resourceTypes: ["Document"],
|
|
78
|
+
context: { type: "Record", attributes: {} },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const ENTITIES = [
|
|
86
|
+
{
|
|
87
|
+
uid: { type: "DocMgmt::User", id: "alice" },
|
|
88
|
+
attrs: { name: "Alice Smith", email: "alice@example.com" },
|
|
89
|
+
parents: [{ type: "DocMgmt::Role", id: "admin" }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
uid: { type: "DocMgmt::User", id: "bob" },
|
|
93
|
+
attrs: { name: "Bob Jones", email: "bob@example.com" },
|
|
94
|
+
parents: [{ type: "DocMgmt::Role", id: "editor" }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
uid: { type: "DocMgmt::User", id: "charlie" },
|
|
98
|
+
attrs: { name: "Charlie Brown", email: "charlie@example.com" },
|
|
99
|
+
parents: [{ type: "DocMgmt::Role", id: "viewer" }],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
uid: { type: "DocMgmt::User", id: "dave" },
|
|
103
|
+
attrs: { name: "Dave Wilson", email: "dave@example.com" },
|
|
104
|
+
parents: [],
|
|
105
|
+
},
|
|
106
|
+
{ uid: { type: "DocMgmt::Role", id: "admin" }, attrs: {}, parents: [] },
|
|
107
|
+
{ uid: { type: "DocMgmt::Role", id: "editor" }, attrs: {}, parents: [] },
|
|
108
|
+
{ uid: { type: "DocMgmt::Role", id: "viewer" }, attrs: {}, parents: [] },
|
|
109
|
+
{
|
|
110
|
+
uid: { type: "DocMgmt::Document", id: "doc-public" },
|
|
111
|
+
attrs: { owner: "alice", classification: "public" },
|
|
112
|
+
parents: [{ type: "DocMgmt::Folder", id: "shared" }],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
uid: { type: "DocMgmt::Document", id: "doc-secret" },
|
|
116
|
+
attrs: { owner: "alice", classification: "top_secret" },
|
|
117
|
+
parents: [{ type: "DocMgmt::Folder", id: "classified" }],
|
|
118
|
+
},
|
|
119
|
+
{ uid: { type: "DocMgmt::Folder", id: "shared" }, attrs: {}, parents: [] },
|
|
120
|
+
{ uid: { type: "DocMgmt::Folder", id: "classified" }, attrs: {}, parents: [] },
|
|
121
|
+
];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Dataset 2: ABAC — Multi-Tenant Insurance Platform
|
|
2
|
+
// Namespace: Insurance
|
|
3
|
+
// Principals are Identity entities carrying a `name` attribute; policies check
|
|
4
|
+
// principal.name directly (attribute-based, not role-based membership).
|
|
5
|
+
// Resources are Policy entities with vertical, business_unit, and optional insurer.
|
|
6
|
+
// This exercises: name-based identity, array containment, optional attribute guard,
|
|
7
|
+
// multi-attribute ABAC — all patterns absent from Dataset 1 (DocMgmt RBAC).
|
|
8
|
+
|
|
9
|
+
export const POLICIES = `
|
|
10
|
+
permit (
|
|
11
|
+
principal,
|
|
12
|
+
action in [Insurance::Action::"READ"],
|
|
13
|
+
resource
|
|
14
|
+
)
|
|
15
|
+
when {
|
|
16
|
+
principal.name == "tenant-a" &&
|
|
17
|
+
["tradesmen_and_professionals", "shops_and_salons"].contains(resource.vertical) &&
|
|
18
|
+
resource.business_unit == "mga_uk"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
permit (
|
|
22
|
+
principal,
|
|
23
|
+
action,
|
|
24
|
+
resource
|
|
25
|
+
)
|
|
26
|
+
when {
|
|
27
|
+
principal.name == "bruno"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
permit (
|
|
31
|
+
principal,
|
|
32
|
+
action in [Insurance::Action::"READ"],
|
|
33
|
+
resource
|
|
34
|
+
)
|
|
35
|
+
when {
|
|
36
|
+
principal.name == "tenant-b-user" &&
|
|
37
|
+
resource has insurer &&
|
|
38
|
+
["Harborway Insurance"].contains(resource.insurer) &&
|
|
39
|
+
resource.business_unit == "simplybusiness_us"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
permit (
|
|
43
|
+
principal,
|
|
44
|
+
action in [Insurance::Action::"CREATE", Insurance::Action::"READ"],
|
|
45
|
+
resource
|
|
46
|
+
)
|
|
47
|
+
when {
|
|
48
|
+
principal.name == "tenant-d"
|
|
49
|
+
};
|
|
50
|
+
`.trim();
|
|
51
|
+
|
|
52
|
+
export const SCHEMA_JSON = {
|
|
53
|
+
Insurance: {
|
|
54
|
+
entityTypes: {
|
|
55
|
+
Identity: {
|
|
56
|
+
memberOfTypes: ["Role"],
|
|
57
|
+
shape: {
|
|
58
|
+
type: "Record",
|
|
59
|
+
attributes: {
|
|
60
|
+
name: { type: "String", required: true },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
Role: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
|
|
65
|
+
Policy: {
|
|
66
|
+
memberOfTypes: [],
|
|
67
|
+
shape: {
|
|
68
|
+
type: "Record",
|
|
69
|
+
attributes: {
|
|
70
|
+
vertical: { type: "String", required: true },
|
|
71
|
+
business_unit: { type: "String", required: true },
|
|
72
|
+
insurer: { type: "String", required: false },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
actions: {
|
|
78
|
+
CREATE: {
|
|
79
|
+
appliesTo: {
|
|
80
|
+
principalTypes: ["Identity"],
|
|
81
|
+
resourceTypes: ["Policy"],
|
|
82
|
+
context: { type: "Record", attributes: {} },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
READ: {
|
|
86
|
+
appliesTo: {
|
|
87
|
+
principalTypes: ["Identity"],
|
|
88
|
+
resourceTypes: ["Policy"],
|
|
89
|
+
context: { type: "Record", attributes: {} },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
UPDATE: {
|
|
93
|
+
appliesTo: {
|
|
94
|
+
principalTypes: ["Identity"],
|
|
95
|
+
resourceTypes: ["Policy"],
|
|
96
|
+
context: { type: "Record", attributes: {} },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const ENTITIES = [
|
|
104
|
+
// Principals
|
|
105
|
+
{
|
|
106
|
+
uid: { type: "Insurance::Identity", id: "tenant-a" },
|
|
107
|
+
attrs: { name: "tenant-a" },
|
|
108
|
+
parents: [{ type: "Insurance::Role", id: "partner" }],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
uid: { type: "Insurance::Identity", id: "tenant-c" },
|
|
112
|
+
attrs: { name: "bruno" },
|
|
113
|
+
parents: [{ type: "Insurance::Role", id: "internal" }],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
uid: { type: "Insurance::Identity", id: "tenant-b" },
|
|
117
|
+
attrs: { name: "tenant-b-user" },
|
|
118
|
+
parents: [{ type: "Insurance::Role", id: "partner" }],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
uid: { type: "Insurance::Identity", id: "tenant-d" },
|
|
122
|
+
attrs: { name: "tenant-d" },
|
|
123
|
+
parents: [{ type: "Insurance::Role", id: "internal" }],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
uid: { type: "Insurance::Identity", id: "unknown-client" },
|
|
127
|
+
attrs: { name: "unknown_service" },
|
|
128
|
+
parents: [],
|
|
129
|
+
},
|
|
130
|
+
// Roles
|
|
131
|
+
{ uid: { type: "Insurance::Role", id: "partner" }, attrs: {}, parents: [] },
|
|
132
|
+
{ uid: { type: "Insurance::Role", id: "internal" }, attrs: {}, parents: [] },
|
|
133
|
+
// Resources — insurance policies
|
|
134
|
+
{
|
|
135
|
+
uid: { type: "Insurance::Policy", id: "POL-001" },
|
|
136
|
+
attrs: { vertical: "tradesmen_and_professionals", business_unit: "mga_uk", insurer: "Aviva" },
|
|
137
|
+
parents: [],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
uid: { type: "Insurance::Policy", id: "POL-002" },
|
|
141
|
+
attrs: {
|
|
142
|
+
vertical: "commercial_landlord",
|
|
143
|
+
business_unit: "simplybusiness_us",
|
|
144
|
+
insurer: "Harborway Insurance",
|
|
145
|
+
},
|
|
146
|
+
parents: [],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
uid: { type: "Insurance::Policy", id: "POL-003" },
|
|
150
|
+
attrs: {
|
|
151
|
+
vertical: "tradesmen_and_professionals",
|
|
152
|
+
business_unit: "simplybusiness_us",
|
|
153
|
+
insurer: "Harborway Insurance",
|
|
154
|
+
},
|
|
155
|
+
parents: [],
|
|
156
|
+
},
|
|
157
|
+
// POL-004 intentionally has no `insurer` attribute — tests optional attribute guard
|
|
158
|
+
{
|
|
159
|
+
uid: { type: "Insurance::Policy", id: "POL-004" },
|
|
160
|
+
attrs: { vertical: "shops_and_salons", business_unit: "mga_uk" },
|
|
161
|
+
parents: [],
|
|
162
|
+
},
|
|
163
|
+
];
|