@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.
Files changed (74) hide show
  1. package/README.md +219 -0
  2. package/_schemas/overwatch/context.json +463 -0
  3. package/_schemas/overwatch/schema.cedarschema +184 -0
  4. package/_schemas/palisade/context.json +325 -0
  5. package/_schemas/palisade/schema.cedarschema +168 -0
  6. package/dist/builder.d.ts +1 -2
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js.map +1 -1
  9. package/dist/context.gen.d.ts +1 -94
  10. package/dist/context.gen.d.ts.map +1 -1
  11. package/dist/context.gen.js +1 -97
  12. package/dist/context.gen.js.map +1 -1
  13. package/dist/engine.d.ts +18 -18
  14. package/dist/engine.d.ts.map +1 -1
  15. package/dist/engine.js +44 -28
  16. package/dist/engine.js.map +1 -1
  17. package/dist/engine.test.js.map +1 -1
  18. package/dist/entities.gen.d.ts +1 -0
  19. package/dist/entities.gen.d.ts.map +1 -1
  20. package/dist/entities.gen.js +1 -0
  21. package/dist/entities.gen.js.map +1 -1
  22. package/dist/errors.d.ts +102 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +127 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/overwatch-context.gen.d.ts +31 -0
  31. package/dist/overwatch-context.gen.d.ts.map +1 -0
  32. package/dist/overwatch-context.gen.js +32 -0
  33. package/dist/overwatch-context.gen.js.map +1 -0
  34. package/dist/palisade-context.gen.d.ts +25 -0
  35. package/dist/palisade-context.gen.d.ts.map +1 -0
  36. package/dist/palisade-context.gen.js +26 -0
  37. package/dist/palisade-context.gen.js.map +1 -0
  38. package/dist/parser.d.ts.map +1 -1
  39. package/dist/parser.js +79 -34
  40. package/dist/parser.js.map +1 -1
  41. package/dist/parser.test.js +44 -0
  42. package/dist/parser.test.js.map +1 -1
  43. package/dist/schema.gen.d.ts +1 -1
  44. package/dist/schema.gen.d.ts.map +1 -1
  45. package/dist/schema.gen.js +60 -541
  46. package/dist/schema.gen.js.map +1 -1
  47. package/dist/schemas.d.ts +64 -0
  48. package/dist/schemas.d.ts.map +1 -0
  49. package/dist/schemas.js +70 -0
  50. package/dist/schemas.js.map +1 -0
  51. package/dist/schemas.test.d.ts +8 -0
  52. package/dist/schemas.test.d.ts.map +1 -0
  53. package/dist/schemas.test.js +381 -0
  54. package/dist/schemas.test.js.map +1 -0
  55. package/dist/types.d.ts +1 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js +2 -0
  58. package/dist/types.js.map +1 -1
  59. package/package.json +13 -6
  60. package/src/builder.ts +1 -2
  61. package/src/context.gen.ts +0 -97
  62. package/src/engine.test.ts +0 -1
  63. package/src/engine.ts +62 -33
  64. package/src/entities.gen.ts +1 -0
  65. package/src/errors.ts +195 -0
  66. package/src/index.ts +2 -0
  67. package/src/overwatch-context.gen.ts +34 -0
  68. package/src/palisade-context.gen.ts +28 -0
  69. package/src/parser.test.ts +53 -0
  70. package/src/parser.ts +83 -36
  71. package/src/schema.gen.ts +60 -541
  72. package/src/schemas.test.ts +449 -0
  73. package/src/schemas.ts +91 -0
  74. 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
- 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
- };
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
- // Map description from annotations
178
- if (policy.annotations?.description) {
179
- rule.description = policy.annotations.description;
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
- // Map conditions
183
- const { conditions, rawCondition } = mapConditions(policy.conditions);
184
- rule.conditions = conditions;
185
- if (rawCondition) {
186
- rule.rawCondition = rawCondition;
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
- return `// Complex policy: ${policyId}`;
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
- // Slot - can't represent
257
- return null;
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
- // Type constraint
262
- return { type: scope.entity_type };
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
- // Slot - can't represent
271
- return null;
306
+ if ("slot" in scope) {
307
+ throw ParserError.scopeSlotNotSupported("in", field);
308
+ }
309
+ throw ParserError.scopeMissingEntityList(field);
272
310
  }
273
311
 
274
- return null;
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
- const entity = normalizeEntityRef(scope.entity);
287
- return entity.id;
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
- return "";
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(" && ") : undefined,
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