@highflame/policy 1.1.3 → 1.2.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/dist/actions.gen.d.ts +21 -0
- package/dist/actions.gen.d.ts.map +1 -1
- package/dist/actions.gen.js +21 -0
- package/dist/actions.gen.js.map +1 -1
- package/dist/builder.d.ts +47 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js.map +1 -1
- package/dist/engine.d.ts +37 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +99 -0
- package/dist/engine.js.map +1 -1
- package/dist/engine.test.d.ts +8 -0
- package/dist/engine.test.d.ts.map +1 -0
- package/dist/engine.test.js +190 -0
- package/dist/engine.test.js.map +1 -0
- package/dist/entities.gen.d.ts +4 -0
- package/dist/entities.gen.d.ts.map +1 -1
- package/dist/entities.gen.js +4 -0
- package/dist/entities.gen.js.map +1 -1
- package/dist/parser.d.ts +34 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +348 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +8 -0
- package/dist/parser.test.d.ts.map +1 -0
- package/dist/parser.test.js +99 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/schema.gen.d.ts +1 -1
- package/dist/schema.gen.d.ts.map +1 -1
- package/dist/schema.gen.js +331 -17
- package/dist/schema.gen.js.map +1 -1
- package/package.json +4 -2
- package/src/actions.gen.ts +21 -0
- package/src/builder.ts +52 -10
- package/src/engine.test.ts +371 -0
- package/src/engine.ts +145 -0
- package/src/entities.gen.ts +4 -0
- package/src/parser.test.ts +116 -0
- package/src/parser.ts +470 -0
- package/src/schema.gen.ts +331 -17
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser unit tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the Cedar text → PolicyRule conversion using the official Cedar engine.
|
|
5
|
+
* These tests demonstrate how a client like highflame-authz would use the parser.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { parseCedarToRules } from './parser.js';
|
|
10
|
+
|
|
11
|
+
describe('parseCedarToRules', () => {
|
|
12
|
+
it('should parse a simple permit policy', () => {
|
|
13
|
+
const cedarText = `
|
|
14
|
+
@id("allow-read-files")
|
|
15
|
+
permit(
|
|
16
|
+
principal is User,
|
|
17
|
+
action == Action::"read_file",
|
|
18
|
+
resource is FilePath
|
|
19
|
+
);
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const result = parseCedarToRules(cedarText);
|
|
23
|
+
|
|
24
|
+
expect(result.errors).toHaveLength(0);
|
|
25
|
+
expect(result.rules).toHaveLength(1);
|
|
26
|
+
expect(result.unstructured).toHaveLength(0);
|
|
27
|
+
|
|
28
|
+
const rule = result.rules[0];
|
|
29
|
+
expect(rule.id).toBe('allow-read-files');
|
|
30
|
+
expect(rule.effect).toBe('permit');
|
|
31
|
+
expect(rule.principal).toEqual({ type: 'User' });
|
|
32
|
+
expect(rule.action).toBe('read_file');
|
|
33
|
+
expect(rule.resource).toEqual({ type: 'FilePath' });
|
|
34
|
+
expect(rule.enabled).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should parse a policy with when conditions', () => {
|
|
38
|
+
const cedarText = `
|
|
39
|
+
@id("block-high-risk")
|
|
40
|
+
forbid(
|
|
41
|
+
principal,
|
|
42
|
+
action == Action::"execute_tool",
|
|
43
|
+
resource
|
|
44
|
+
)
|
|
45
|
+
when {
|
|
46
|
+
context.threat_level == "high"
|
|
47
|
+
};
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const result = parseCedarToRules(cedarText);
|
|
51
|
+
|
|
52
|
+
expect(result.errors).toHaveLength(0);
|
|
53
|
+
expect(result.rules).toHaveLength(1);
|
|
54
|
+
|
|
55
|
+
const rule = result.rules[0];
|
|
56
|
+
expect(rule.id).toBe('block-high-risk');
|
|
57
|
+
expect(rule.effect).toBe('forbid');
|
|
58
|
+
expect(rule.action).toBe('execute_tool');
|
|
59
|
+
|
|
60
|
+
// Check condition was parsed
|
|
61
|
+
expect(rule.conditions.length).toBeGreaterThanOrEqual(0);
|
|
62
|
+
// If condition parsing works, it should have the condition
|
|
63
|
+
// If not, it should be in rawCondition
|
|
64
|
+
if (rule.conditions.length > 0) {
|
|
65
|
+
expect(rule.conditions[0].field).toBe('threat_level');
|
|
66
|
+
expect(rule.conditions[0].operator).toBe('eq');
|
|
67
|
+
expect(rule.conditions[0].value).toBe('high');
|
|
68
|
+
} else {
|
|
69
|
+
expect(rule.rawCondition).toBeDefined();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return errors for invalid Cedar syntax', () => {
|
|
74
|
+
const invalidCedar = `
|
|
75
|
+
permit(
|
|
76
|
+
principal is User
|
|
77
|
+
// missing comma and rest of policy
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const result = parseCedarToRules(invalidCedar);
|
|
81
|
+
|
|
82
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle multiple policies', () => {
|
|
86
|
+
const cedarText = `
|
|
87
|
+
@id("rule-1")
|
|
88
|
+
permit(principal, action == Action::"read", resource);
|
|
89
|
+
|
|
90
|
+
@id("rule-2")
|
|
91
|
+
forbid(principal, action == Action::"delete", resource);
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
const result = parseCedarToRules(cedarText);
|
|
95
|
+
|
|
96
|
+
expect(result.errors).toHaveLength(0);
|
|
97
|
+
expect(result.rules).toHaveLength(2);
|
|
98
|
+
expect(result.rules[0].effect).toBe('permit');
|
|
99
|
+
expect(result.rules[1].effect).toBe('forbid');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should put policies with unless clauses in unstructured', () => {
|
|
103
|
+
const cedarText = `
|
|
104
|
+
permit(principal, action, resource)
|
|
105
|
+
unless {
|
|
106
|
+
context.is_blocked == true
|
|
107
|
+
};
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const result = parseCedarToRules(cedarText);
|
|
111
|
+
|
|
112
|
+
// Unless clauses can't be represented as PolicyRule
|
|
113
|
+
expect(result.rules).toHaveLength(0);
|
|
114
|
+
expect(result.unstructured.length).toBeGreaterThan(0);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cedar Policy Parser
|
|
3
|
+
*
|
|
4
|
+
* Converts Cedar policy text to structured PolicyRule format using the
|
|
5
|
+
* official Cedar engine (cedar-wasm) for parsing.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* 1. Cedar text → Cedar JSON (via cedar-wasm policyToJson)
|
|
9
|
+
* 2. Cedar JSON → PolicyRule (simple JSON mapping)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as cedar from "@cedar-policy/cedar-wasm/nodejs";
|
|
13
|
+
import type { PolicyRule, PolicyCondition, PolicyEntity, PolicyEffect, ConditionOperator } from "./builder.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result of parsing Cedar policies
|
|
17
|
+
*/
|
|
18
|
+
export interface ParseResult {
|
|
19
|
+
/** Policies successfully converted to PolicyRule format */
|
|
20
|
+
rules: PolicyRule[];
|
|
21
|
+
/** Policies that couldn't be fully represented as PolicyRule (raw Cedar text) */
|
|
22
|
+
unstructured: string[];
|
|
23
|
+
/** Any parsing errors encountered */
|
|
24
|
+
errors: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cedar's JSON policy format (from cedar-wasm)
|
|
29
|
+
* @see https://docs.cedarpolicy.com/policies/json-format.html
|
|
30
|
+
*/
|
|
31
|
+
interface CedarPolicyJSON {
|
|
32
|
+
effect: "permit" | "forbid";
|
|
33
|
+
principal: CedarScopeConstraint;
|
|
34
|
+
action: CedarActionConstraint;
|
|
35
|
+
resource: CedarScopeConstraint;
|
|
36
|
+
conditions: CedarCondition[];
|
|
37
|
+
annotations?: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Principal/Resource constraint types
|
|
41
|
+
type CedarScopeConstraint =
|
|
42
|
+
| { op: "All" }
|
|
43
|
+
| { op: "=="; entity: CedarEntityRef }
|
|
44
|
+
| { op: "=="; slot: string }
|
|
45
|
+
| { op: "in"; entity: CedarEntityRef }
|
|
46
|
+
| { op: "in"; slot: string }
|
|
47
|
+
| { op: "is"; entity_type: string; in?: { entity: CedarEntityRef } | { slot: string } };
|
|
48
|
+
|
|
49
|
+
// Action constraint types (slightly different from principal/resource)
|
|
50
|
+
type CedarActionConstraint =
|
|
51
|
+
| { op: "All" }
|
|
52
|
+
| { op: "=="; entity: CedarEntityRef }
|
|
53
|
+
| { op: "in"; entity: CedarEntityRef }
|
|
54
|
+
| { op: "in"; entities: CedarEntityRef[] };
|
|
55
|
+
|
|
56
|
+
// Entity UID can be { type, id } or { __entity: { type, id } }
|
|
57
|
+
type CedarEntityRef =
|
|
58
|
+
| { type: string; id: string }
|
|
59
|
+
| { __entity: { type: string; id: string } };
|
|
60
|
+
|
|
61
|
+
interface CedarCondition {
|
|
62
|
+
kind: "when" | "unless";
|
|
63
|
+
body: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Normalize entity reference to simple { type, id } format
|
|
68
|
+
*/
|
|
69
|
+
function normalizeEntityRef(ref: CedarEntityRef): { type: string; id: string } {
|
|
70
|
+
if ("__entity" in ref) {
|
|
71
|
+
return ref.__entity;
|
|
72
|
+
}
|
|
73
|
+
return ref;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse Cedar policy text and convert to PolicyRule format.
|
|
78
|
+
*
|
|
79
|
+
* Uses the official cedar-wasm engine for parsing, ensuring correctness.
|
|
80
|
+
* Policies with features that can't be represented as PolicyRule (e.g.,
|
|
81
|
+
* unless clauses, complex expressions) are returned in the unstructured array.
|
|
82
|
+
*
|
|
83
|
+
* @param cedarText - Cedar policy text to parse
|
|
84
|
+
* @returns ParseResult with structured rules, unstructured policies, and errors
|
|
85
|
+
*/
|
|
86
|
+
export function parseCedarToRules(cedarText: string): ParseResult {
|
|
87
|
+
const result: ParseResult = {
|
|
88
|
+
rules: [],
|
|
89
|
+
unstructured: [],
|
|
90
|
+
errors: [],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Split the policy set into individual policies and templates
|
|
95
|
+
const partsResult = cedar.policySetTextToParts(cedarText);
|
|
96
|
+
|
|
97
|
+
if (partsResult.type === "failure") {
|
|
98
|
+
for (const error of partsResult.errors) {
|
|
99
|
+
result.errors.push(error.message);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Process each policy
|
|
105
|
+
let index = 0;
|
|
106
|
+
for (const policyText of partsResult.policies) {
|
|
107
|
+
// Convert individual policy to JSON using cedar-wasm
|
|
108
|
+
const jsonResult = cedar.policyToJson(policyText);
|
|
109
|
+
|
|
110
|
+
if (jsonResult.type === "failure") {
|
|
111
|
+
for (const error of jsonResult.errors) {
|
|
112
|
+
result.errors.push(`Policy ${index}: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
index++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const policy = jsonResult.json as CedarPolicyJSON;
|
|
119
|
+
const policyId = policy.annotations?.id || `policy${index}`;
|
|
120
|
+
const conversion = cedarJsonToRule(policy, policyId, index, policyText);
|
|
121
|
+
|
|
122
|
+
if (conversion.error) {
|
|
123
|
+
result.errors.push(`Policy ${policyId}: ${conversion.error}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (conversion.rule) {
|
|
127
|
+
result.rules.push(conversion.rule);
|
|
128
|
+
} else if (conversion.raw) {
|
|
129
|
+
result.unstructured.push(conversion.raw);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
index++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Templates can't be represented as PolicyRule
|
|
136
|
+
for (const templateText of partsResult.policy_templates) {
|
|
137
|
+
result.unstructured.push(templateText);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
} catch (e) {
|
|
141
|
+
result.errors.push(`Parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Convert Cedar JSON policy to PolicyRule.
|
|
149
|
+
* This is pure JSON mapping - no parsing logic.
|
|
150
|
+
*/
|
|
151
|
+
function cedarJsonToRule(
|
|
152
|
+
policy: CedarPolicyJSON,
|
|
153
|
+
policyId: string,
|
|
154
|
+
index: number,
|
|
155
|
+
originalText?: string
|
|
156
|
+
): { rule?: PolicyRule; raw?: string; error?: string } {
|
|
157
|
+
|
|
158
|
+
// Check if this policy can be represented as PolicyRule
|
|
159
|
+
if (!canRepresentAsRule(policy)) {
|
|
160
|
+
// Return original text if available, otherwise convert back from JSON
|
|
161
|
+
const raw = originalText || getRawCedar(policyId, policy);
|
|
162
|
+
return { raw };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const rule: PolicyRule = {
|
|
166
|
+
id: policy.annotations?.id || policyId,
|
|
167
|
+
name: policy.annotations?.name || policy.annotations?.id || policyId,
|
|
168
|
+
effect: policy.effect as PolicyEffect,
|
|
169
|
+
principal: mapScopeToEntity(policy.principal),
|
|
170
|
+
action: mapActionScope(policy.action),
|
|
171
|
+
resource: mapScopeToEntity(policy.resource),
|
|
172
|
+
conditions: [],
|
|
173
|
+
enabled: true,
|
|
174
|
+
order: index,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Map description from annotations
|
|
178
|
+
if (policy.annotations?.description) {
|
|
179
|
+
rule.description = policy.annotations.description;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Map conditions
|
|
183
|
+
const { conditions, rawCondition } = mapConditions(policy.conditions);
|
|
184
|
+
rule.conditions = conditions;
|
|
185
|
+
if (rawCondition) {
|
|
186
|
+
rule.rawCondition = rawCondition;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { rule };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if a Cedar policy can be represented as PolicyRule
|
|
194
|
+
*/
|
|
195
|
+
function canRepresentAsRule(policy: CedarPolicyJSON): boolean {
|
|
196
|
+
// Unless clauses can't be represented
|
|
197
|
+
for (const cond of policy.conditions) {
|
|
198
|
+
if (cond.kind === "unless") {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Template slots can't be represented
|
|
204
|
+
if (hasSlot(policy.principal) || hasSlot(policy.resource)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Multiple entity "in" constraints are complex
|
|
209
|
+
const action = policy.action;
|
|
210
|
+
if (action.op === "in" && "entities" in action && action.entities.length > 1) {
|
|
211
|
+
// Multiple actions are OK, we handle those
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if a scope constraint uses a slot (template)
|
|
219
|
+
*/
|
|
220
|
+
function hasSlot(scope: CedarScopeConstraint): boolean {
|
|
221
|
+
if (scope.op === "All" || scope.op === "is") {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return "slot" in scope;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get raw Cedar text for a policy that can't be represented as PolicyRule
|
|
229
|
+
*/
|
|
230
|
+
function getRawCedar(policyId: string, policy: CedarPolicyJSON): string {
|
|
231
|
+
try {
|
|
232
|
+
// policyToText accepts Policy which is string | PolicyJson
|
|
233
|
+
const textResult = cedar.policyToText(policy as cedar.PolicyJson);
|
|
234
|
+
if (textResult.type === "success") {
|
|
235
|
+
return textResult.text;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Ignore conversion errors
|
|
239
|
+
}
|
|
240
|
+
return `// Complex policy: ${policyId}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Map Cedar scope constraint to PolicyEntity
|
|
245
|
+
*/
|
|
246
|
+
function mapScopeToEntity(scope: CedarScopeConstraint): PolicyEntity | null {
|
|
247
|
+
if (scope.op === "All") {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (scope.op === "==") {
|
|
252
|
+
if ("entity" in scope) {
|
|
253
|
+
const entity = normalizeEntityRef(scope.entity);
|
|
254
|
+
return { type: entity.type, id: entity.id };
|
|
255
|
+
}
|
|
256
|
+
// Slot - can't represent
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (scope.op === "is") {
|
|
261
|
+
// Type constraint
|
|
262
|
+
return { type: scope.entity_type };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (scope.op === "in") {
|
|
266
|
+
if ("entity" in scope) {
|
|
267
|
+
const entity = normalizeEntityRef(scope.entity);
|
|
268
|
+
return { type: entity.type, id: entity.id };
|
|
269
|
+
}
|
|
270
|
+
// Slot - can't represent
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Map action scope to action string(s)
|
|
279
|
+
*/
|
|
280
|
+
function mapActionScope(scope: CedarActionConstraint): string | string[] {
|
|
281
|
+
if (scope.op === "All") {
|
|
282
|
+
return "*";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (scope.op === "==") {
|
|
286
|
+
const entity = normalizeEntityRef(scope.entity);
|
|
287
|
+
return entity.id;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (scope.op === "in") {
|
|
291
|
+
if ("entities" in scope) {
|
|
292
|
+
const actions = scope.entities.map(e => normalizeEntityRef(e).id);
|
|
293
|
+
return actions.length === 1 ? actions[0] : actions;
|
|
294
|
+
}
|
|
295
|
+
if ("entity" in scope) {
|
|
296
|
+
const entity = normalizeEntityRef(scope.entity);
|
|
297
|
+
return entity.id;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return "";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Map Cedar conditions to PolicyCondition array
|
|
306
|
+
*/
|
|
307
|
+
function mapConditions(conditions: CedarCondition[]): {
|
|
308
|
+
conditions: PolicyCondition[];
|
|
309
|
+
rawCondition?: string;
|
|
310
|
+
} {
|
|
311
|
+
const result: PolicyCondition[] = [];
|
|
312
|
+
const rawParts: string[] = [];
|
|
313
|
+
|
|
314
|
+
for (const cond of conditions) {
|
|
315
|
+
if (cond.kind !== "when") {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const parsed = mapConditionBody(cond.body);
|
|
320
|
+
if (parsed.condition) {
|
|
321
|
+
result.push(parsed.condition);
|
|
322
|
+
} else if (parsed.raw) {
|
|
323
|
+
rawParts.push(parsed.raw);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
conditions: result,
|
|
329
|
+
rawCondition: rawParts.length > 0 ? rawParts.join(" && ") : undefined,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Cedar expression types (subset used for condition mapping)
|
|
335
|
+
*/
|
|
336
|
+
interface CedarExpr {
|
|
337
|
+
"."?: { left: CedarExpr; attr: string };
|
|
338
|
+
Var?: string;
|
|
339
|
+
Value?: unknown;
|
|
340
|
+
"=="?: { left: CedarExpr; right: CedarExpr };
|
|
341
|
+
"!="?: { left: CedarExpr; right: CedarExpr };
|
|
342
|
+
"<"?: { left: CedarExpr; right: CedarExpr };
|
|
343
|
+
"<="?: { left: CedarExpr; right: CedarExpr };
|
|
344
|
+
">"?: { left: CedarExpr; right: CedarExpr };
|
|
345
|
+
">="?: { left: CedarExpr; right: CedarExpr };
|
|
346
|
+
contains?: { left: CedarExpr; right: CedarExpr };
|
|
347
|
+
like?: { left: CedarExpr; pattern: Array<"Wildcard" | { Literal: string }> };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Map a Cedar expression body to PolicyCondition
|
|
352
|
+
*
|
|
353
|
+
* Cedar JSON expressions use nested objects with operator keys.
|
|
354
|
+
* Comparison format: { "==": { left: { ".": { left: { Var: "context" }, attr: "field" } }, right: { Value: "x" } } }
|
|
355
|
+
*/
|
|
356
|
+
function mapConditionBody(body: Record<string, unknown>): {
|
|
357
|
+
condition?: PolicyCondition;
|
|
358
|
+
raw?: string;
|
|
359
|
+
} {
|
|
360
|
+
const expr = body as CedarExpr;
|
|
361
|
+
|
|
362
|
+
// Check comparison operators
|
|
363
|
+
for (const op of ["==", "!=", "<", "<=", ">", ">="] as const) {
|
|
364
|
+
const comparison = expr[op];
|
|
365
|
+
if (comparison) {
|
|
366
|
+
const condition = mapComparison(op, comparison);
|
|
367
|
+
if (condition) return { condition };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check contains
|
|
372
|
+
if (expr.contains) {
|
|
373
|
+
const condition = mapContains(expr.contains);
|
|
374
|
+
if (condition) return { condition };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Check like
|
|
378
|
+
if (expr.like) {
|
|
379
|
+
const condition = mapLike(expr.like);
|
|
380
|
+
if (condition) return { condition };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Can't map - return as raw JSON
|
|
384
|
+
return { raw: JSON.stringify(body) };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function mapComparison(op: string, args: { left: CedarExpr; right: CedarExpr }): PolicyCondition | null {
|
|
388
|
+
const field = extractContextField(args.left);
|
|
389
|
+
if (!field) return null;
|
|
390
|
+
|
|
391
|
+
const value = extractLiteralValue(args.right);
|
|
392
|
+
if (value === undefined) return null;
|
|
393
|
+
|
|
394
|
+
const operator = mapOperator(op);
|
|
395
|
+
if (!operator) return null;
|
|
396
|
+
|
|
397
|
+
return { field, operator, value };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function mapContains(args: { left: CedarExpr; right: CedarExpr }): PolicyCondition | null {
|
|
401
|
+
const field = extractContextField(args.left);
|
|
402
|
+
if (!field) return null;
|
|
403
|
+
|
|
404
|
+
const value = extractLiteralValue(args.right);
|
|
405
|
+
if (value === undefined) return null;
|
|
406
|
+
|
|
407
|
+
return { field, operator: "contains", value };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function mapLike(args: { left: CedarExpr; pattern: Array<"Wildcard" | { Literal: string }> }): PolicyCondition | null {
|
|
411
|
+
const field = extractContextField(args.left);
|
|
412
|
+
if (!field) return null;
|
|
413
|
+
|
|
414
|
+
// Convert pattern to string (e.g., ["Wildcard", { Literal: "foo" }, "Wildcard"] -> "*foo*")
|
|
415
|
+
const patternStr = args.pattern.map(p => {
|
|
416
|
+
if (p === "Wildcard") return "*";
|
|
417
|
+
if (typeof p === "object" && "Literal" in p) return p.Literal;
|
|
418
|
+
return "";
|
|
419
|
+
}).join("");
|
|
420
|
+
|
|
421
|
+
return { field, operator: "like", value: patternStr };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Extract field name from context.field access pattern
|
|
426
|
+
* Pattern: { ".": { left: { Var: "context" }, attr: "field_name" } }
|
|
427
|
+
*/
|
|
428
|
+
function extractContextField(expr: CedarExpr): string | null {
|
|
429
|
+
const dotAccess = expr["."];
|
|
430
|
+
if (!dotAccess) return null;
|
|
431
|
+
|
|
432
|
+
// Check if accessing context variable
|
|
433
|
+
const leftExpr = dotAccess.left;
|
|
434
|
+
if (leftExpr.Var !== "context") return null;
|
|
435
|
+
|
|
436
|
+
return dotAccess.attr;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Extract literal value from Cedar JSON
|
|
441
|
+
* Pattern: { Value: <literal> }
|
|
442
|
+
*/
|
|
443
|
+
function extractLiteralValue(expr: CedarExpr): string | number | boolean | string[] | undefined {
|
|
444
|
+
if (!("Value" in expr)) return undefined;
|
|
445
|
+
|
|
446
|
+
const value = expr.Value;
|
|
447
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
if (Array.isArray(value) && value.every(v => typeof v === "string")) {
|
|
451
|
+
return value as string[];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Map Cedar operator to ConditionOperator
|
|
459
|
+
*/
|
|
460
|
+
function mapOperator(cedarOp: string): ConditionOperator | null {
|
|
461
|
+
const mapping: Record<string, ConditionOperator> = {
|
|
462
|
+
"==": "eq",
|
|
463
|
+
"!=": "neq",
|
|
464
|
+
"<": "lt",
|
|
465
|
+
"<=": "lte",
|
|
466
|
+
">": "gt",
|
|
467
|
+
">=": "gte",
|
|
468
|
+
};
|
|
469
|
+
return mapping[cedarOp] || null;
|
|
470
|
+
}
|