@highflame/policy 1.2.0 → 2.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/README.md +219 -0
- package/_schemas/overwatch/context.json +463 -0
- package/_schemas/overwatch/schema.cedarschema +184 -0
- package/_schemas/palisade/context.json +325 -0
- package/_schemas/palisade/schema.cedarschema +168 -0
- package/dist/builder.d.ts +1 -2
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js.map +1 -1
- package/dist/context.gen.d.ts +1 -94
- package/dist/context.gen.d.ts.map +1 -1
- package/dist/context.gen.js +1 -97
- package/dist/context.gen.js.map +1 -1
- package/dist/engine.d.ts +18 -18
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +44 -28
- package/dist/engine.js.map +1 -1
- package/dist/engine.test.js.map +1 -1
- package/dist/entities.gen.d.ts +1 -0
- package/dist/entities.gen.d.ts.map +1 -1
- package/dist/entities.gen.js +1 -0
- package/dist/entities.gen.js.map +1 -1
- package/dist/errors.d.ts +102 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +127 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/overwatch-context.gen.d.ts +31 -0
- package/dist/overwatch-context.gen.d.ts.map +1 -0
- package/dist/overwatch-context.gen.js +32 -0
- package/dist/overwatch-context.gen.js.map +1 -0
- package/dist/palisade-context.gen.d.ts +25 -0
- package/dist/palisade-context.gen.d.ts.map +1 -0
- package/dist/palisade-context.gen.js +26 -0
- package/dist/palisade-context.gen.js.map +1 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +79 -34
- package/dist/parser.js.map +1 -1
- package/dist/parser.test.js +44 -0
- package/dist/parser.test.js.map +1 -1
- package/dist/schema.gen.d.ts +1 -1
- package/dist/schema.gen.d.ts.map +1 -1
- package/dist/schema.gen.js +60 -541
- package/dist/schema.gen.js.map +1 -1
- package/dist/schemas.d.ts +64 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +70 -0
- package/dist/schemas.js.map +1 -0
- package/dist/schemas.test.d.ts +8 -0
- package/dist/schemas.test.d.ts.map +1 -0
- package/dist/schemas.test.js +381 -0
- package/dist/schemas.test.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +13 -6
- package/src/builder.ts +1 -2
- package/src/context.gen.ts +0 -97
- package/src/engine.test.ts +0 -1
- package/src/engine.ts +62 -33
- package/src/entities.gen.ts +1 -0
- package/src/errors.ts +195 -0
- package/src/index.ts +2 -0
- package/src/overwatch-context.gen.ts +34 -0
- package/src/palisade-context.gen.ts +28 -0
- package/src/parser.test.ts +53 -0
- package/src/parser.ts +83 -36
- package/src/schema.gen.ts +60 -541
- package/src/schemas.test.ts +449 -0
- package/src/schemas.ts +91 -0
- package/src/types.ts +3 -0
package/src/parser.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import * as cedar from "@cedar-policy/cedar-wasm/nodejs";
|
|
13
13
|
import type { PolicyRule, PolicyCondition, PolicyEntity, PolicyEffect, ConditionOperator } from "./builder.js";
|
|
14
|
+
import { ParserError, ErrorCodes } from "./errors.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Result of parsing Cedar policies
|
|
@@ -141,6 +142,23 @@ export function parseCedarToRules(cedarText: string): ParseResult {
|
|
|
141
142
|
result.errors.push(`Parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
// Check for duplicate policy IDs and add warnings
|
|
146
|
+
const idOccurrences = new Map<string, number[]>();
|
|
147
|
+
result.rules.forEach((rule, idx) => {
|
|
148
|
+
if (rule.id) {
|
|
149
|
+
const indices = idOccurrences.get(rule.id) || [];
|
|
150
|
+
indices.push(idx);
|
|
151
|
+
idOccurrences.set(rule.id, indices);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
for (const [id, indices] of idOccurrences) {
|
|
155
|
+
if (indices.length > 1) {
|
|
156
|
+
result.errors.push(
|
|
157
|
+
`[${ErrorCodes.PARSE_DUPLICATE_ID}] Duplicate policy ID '${id}' found at indices [${indices.join(", ")}]`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
144
162
|
return result;
|
|
145
163
|
}
|
|
146
164
|
|
|
@@ -162,31 +180,37 @@ function cedarJsonToRule(
|
|
|
162
180
|
return { raw };
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
try {
|
|
184
|
+
const rule: PolicyRule = {
|
|
185
|
+
id: policy.annotations?.id || policyId,
|
|
186
|
+
name: policy.annotations?.name || policy.annotations?.id || policyId,
|
|
187
|
+
effect: policy.effect as PolicyEffect,
|
|
188
|
+
principal: mapScopeToEntity(policy.principal, "principal"),
|
|
189
|
+
action: mapActionScope(policy.action),
|
|
190
|
+
resource: mapScopeToEntity(policy.resource, "resource"),
|
|
191
|
+
conditions: [],
|
|
192
|
+
enabled: true,
|
|
193
|
+
order: index,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Map description from annotations
|
|
197
|
+
if (policy.annotations?.description) {
|
|
198
|
+
rule.description = policy.annotations.description;
|
|
199
|
+
}
|
|
176
200
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
rule.
|
|
180
|
-
|
|
201
|
+
// Map conditions
|
|
202
|
+
const { conditions, rawCondition } = mapConditions(policy.conditions);
|
|
203
|
+
rule.conditions = conditions;
|
|
204
|
+
if (rawCondition) {
|
|
205
|
+
rule.rawCondition = rawCondition;
|
|
206
|
+
}
|
|
181
207
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
return { rule };
|
|
209
|
+
} catch (e) {
|
|
210
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
211
|
+
const raw = originalText || getRawCedar(policyId, policy);
|
|
212
|
+
return { raw, error };
|
|
187
213
|
}
|
|
188
|
-
|
|
189
|
-
return { rule };
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
/**
|
|
@@ -237,13 +261,21 @@ function getRawCedar(policyId: string, policy: CedarPolicyJSON): string {
|
|
|
237
261
|
} catch {
|
|
238
262
|
// Ignore conversion errors
|
|
239
263
|
}
|
|
240
|
-
|
|
264
|
+
// Sanitize policyId to prevent newline injection attacks
|
|
265
|
+
const safeId = policyId.replace(/[\n\r]/g, " ");
|
|
266
|
+
return `// Complex policy: ${safeId}`;
|
|
241
267
|
}
|
|
242
268
|
|
|
243
269
|
/**
|
|
244
270
|
* Map Cedar scope constraint to PolicyEntity
|
|
271
|
+
*
|
|
272
|
+
* Returns null for unconstrained ("All") scopes.
|
|
273
|
+
* Throws ParserError for malformed constraints to prevent silent misinterpretation.
|
|
274
|
+
*
|
|
275
|
+
* @param scope - The Cedar scope constraint
|
|
276
|
+
* @param field - The field name ("principal" or "resource") for error context
|
|
245
277
|
*/
|
|
246
|
-
function mapScopeToEntity(scope: CedarScopeConstraint): PolicyEntity | null {
|
|
278
|
+
function mapScopeToEntity(scope: CedarScopeConstraint, field: string): PolicyEntity | null {
|
|
247
279
|
if (scope.op === "All") {
|
|
248
280
|
return null;
|
|
249
281
|
}
|
|
@@ -253,13 +285,17 @@ function mapScopeToEntity(scope: CedarScopeConstraint): PolicyEntity | null {
|
|
|
253
285
|
const entity = normalizeEntityRef(scope.entity);
|
|
254
286
|
return { type: entity.type, id: entity.id };
|
|
255
287
|
}
|
|
256
|
-
|
|
257
|
-
|
|
288
|
+
if ("slot" in scope) {
|
|
289
|
+
throw ParserError.scopeSlotNotSupported("==", field);
|
|
290
|
+
}
|
|
291
|
+
throw ParserError.scopeMissingEntity("==", field);
|
|
258
292
|
}
|
|
259
293
|
|
|
260
294
|
if (scope.op === "is") {
|
|
261
|
-
|
|
262
|
-
|
|
295
|
+
if (scope.entity_type) {
|
|
296
|
+
return { type: scope.entity_type };
|
|
297
|
+
}
|
|
298
|
+
throw ParserError.scopeMissingEntityType(field);
|
|
263
299
|
}
|
|
264
300
|
|
|
265
301
|
if (scope.op === "in") {
|
|
@@ -267,15 +303,19 @@ function mapScopeToEntity(scope: CedarScopeConstraint): PolicyEntity | null {
|
|
|
267
303
|
const entity = normalizeEntityRef(scope.entity);
|
|
268
304
|
return { type: entity.type, id: entity.id };
|
|
269
305
|
}
|
|
270
|
-
|
|
271
|
-
|
|
306
|
+
if ("slot" in scope) {
|
|
307
|
+
throw ParserError.scopeSlotNotSupported("in", field);
|
|
308
|
+
}
|
|
309
|
+
throw ParserError.scopeMissingEntityList(field);
|
|
272
310
|
}
|
|
273
311
|
|
|
274
|
-
|
|
312
|
+
throw ParserError.scopeUnsupportedOp((scope as { op: string }).op, field);
|
|
275
313
|
}
|
|
276
314
|
|
|
277
315
|
/**
|
|
278
316
|
* Map action scope to action string(s)
|
|
317
|
+
*
|
|
318
|
+
* Throws ParserError for malformed constraints to prevent silent misinterpretation.
|
|
279
319
|
*/
|
|
280
320
|
function mapActionScope(scope: CedarActionConstraint): string | string[] {
|
|
281
321
|
if (scope.op === "All") {
|
|
@@ -283,8 +323,11 @@ function mapActionScope(scope: CedarActionConstraint): string | string[] {
|
|
|
283
323
|
}
|
|
284
324
|
|
|
285
325
|
if (scope.op === "==") {
|
|
286
|
-
|
|
287
|
-
|
|
326
|
+
if ("entity" in scope) {
|
|
327
|
+
const entity = normalizeEntityRef(scope.entity);
|
|
328
|
+
return entity.id;
|
|
329
|
+
}
|
|
330
|
+
throw ParserError.actionMissingEntity("==");
|
|
288
331
|
}
|
|
289
332
|
|
|
290
333
|
if (scope.op === "in") {
|
|
@@ -296,9 +339,10 @@ function mapActionScope(scope: CedarActionConstraint): string | string[] {
|
|
|
296
339
|
const entity = normalizeEntityRef(scope.entity);
|
|
297
340
|
return entity.id;
|
|
298
341
|
}
|
|
342
|
+
throw ParserError.actionMissingEntities();
|
|
299
343
|
}
|
|
300
344
|
|
|
301
|
-
|
|
345
|
+
throw ParserError.actionUnsupportedOp((scope as { op: string }).op);
|
|
302
346
|
}
|
|
303
347
|
|
|
304
348
|
/**
|
|
@@ -324,9 +368,11 @@ function mapConditions(conditions: CedarCondition[]): {
|
|
|
324
368
|
}
|
|
325
369
|
}
|
|
326
370
|
|
|
371
|
+
// Store raw conditions as a valid JSON array instead of joining with " && "
|
|
372
|
+
// This ensures downstream systems can parse the rawCondition field
|
|
327
373
|
return {
|
|
328
374
|
conditions: result,
|
|
329
|
-
rawCondition: rawParts.length > 0 ? rawParts.join("
|
|
375
|
+
rawCondition: rawParts.length > 0 ? `[${rawParts.join(",")}]` : undefined,
|
|
330
376
|
};
|
|
331
377
|
}
|
|
332
378
|
|
|
@@ -412,9 +458,10 @@ function mapLike(args: { left: CedarExpr; pattern: Array<"Wildcard" | { Literal:
|
|
|
412
458
|
if (!field) return null;
|
|
413
459
|
|
|
414
460
|
// Convert pattern to string (e.g., ["Wildcard", { Literal: "foo" }, "Wildcard"] -> "*foo*")
|
|
461
|
+
// Escape literal * characters to distinguish from wildcards on round-trip
|
|
415
462
|
const patternStr = args.pattern.map(p => {
|
|
416
463
|
if (p === "Wildcard") return "*";
|
|
417
|
-
if (typeof p === "object" && "Literal" in p) return p.Literal;
|
|
464
|
+
if (typeof p === "object" && "Literal" in p) return p.Literal.replace(/\*/g, "\\*");
|
|
418
465
|
return "";
|
|
419
466
|
}).join("");
|
|
420
467
|
|