@twin.org/rights-management-plugins 0.0.3-next.13 → 0.0.3-next.14

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 (51) hide show
  1. package/dist/es/index.js +5 -0
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/es/models/IDefaultPolicyArbiterConfig.js +4 -0
  4. package/dist/es/models/IDefaultPolicyArbiterConfig.js.map +1 -0
  5. package/dist/es/models/IDefaultPolicyArbiterConstructorOptions.js +4 -0
  6. package/dist/es/models/IDefaultPolicyArbiterConstructorOptions.js.map +1 -0
  7. package/dist/es/models/IPassThroughPolicyObligationEnforcerConstructorOptions.js +4 -0
  8. package/dist/es/models/IPassThroughPolicyObligationEnforcerConstructorOptions.js.map +1 -0
  9. package/dist/es/policyArbiters/defaultPolicyArbiter.js +824 -0
  10. package/dist/es/policyArbiters/defaultPolicyArbiter.js.map +1 -0
  11. package/dist/es/policyArbiters/passThroughPolicyArbiter.js +5 -4
  12. package/dist/es/policyArbiters/passThroughPolicyArbiter.js.map +1 -1
  13. package/dist/es/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.js +5 -4
  14. package/dist/es/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.js.map +1 -1
  15. package/dist/es/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.js +5 -4
  16. package/dist/es/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.js.map +1 -1
  17. package/dist/es/policyExecutionActions/loggingPolicyExecutionAction.js +4 -1
  18. package/dist/es/policyExecutionActions/loggingPolicyExecutionAction.js.map +1 -1
  19. package/dist/es/policyInformationSources/identityPolicyInformationSource.js +2 -1
  20. package/dist/es/policyInformationSources/identityPolicyInformationSource.js.map +1 -1
  21. package/dist/es/policyInformationSources/staticPolicyInformationSource.js +2 -1
  22. package/dist/es/policyInformationSources/staticPolicyInformationSource.js.map +1 -1
  23. package/dist/es/policyObligationEnforcers/passThroughPolicyObligationEnforcer.js +55 -0
  24. package/dist/es/policyObligationEnforcers/passThroughPolicyObligationEnforcer.js.map +1 -0
  25. package/dist/types/index.d.ts +5 -0
  26. package/dist/types/models/IDefaultPolicyArbiterConfig.d.ts +10 -0
  27. package/dist/types/models/IDefaultPolicyArbiterConstructorOptions.d.ts +20 -0
  28. package/dist/types/models/IPassThroughPolicyObligationEnforcerConstructorOptions.d.ts +10 -0
  29. package/dist/types/policyArbiters/defaultPolicyArbiter.d.ts +34 -0
  30. package/dist/types/policyArbiters/passThroughPolicyArbiter.d.ts +5 -4
  31. package/dist/types/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.d.ts +4 -3
  32. package/dist/types/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.d.ts +4 -3
  33. package/dist/types/policyExecutionActions/loggingPolicyExecutionAction.d.ts +3 -2
  34. package/dist/types/policyInformationSources/identityPolicyInformationSource.d.ts +3 -2
  35. package/dist/types/policyInformationSources/staticPolicyInformationSource.d.ts +3 -2
  36. package/dist/types/policyObligationEnforcers/passThroughPolicyObligationEnforcer.d.ts +35 -0
  37. package/docs/changelog.md +23 -0
  38. package/docs/reference/classes/DefaultPolicyArbiter.md +101 -0
  39. package/docs/reference/classes/DefaultPolicyEnforcementProcessor.md +10 -4
  40. package/docs/reference/classes/IdentityPolicyInformationSource.md +7 -1
  41. package/docs/reference/classes/LoggingPolicyExecutionAction.md +7 -1
  42. package/docs/reference/classes/PassThroughPolicyArbiter.md +10 -4
  43. package/docs/reference/classes/PassThroughPolicyEnforcementProcessor.md +10 -4
  44. package/docs/reference/classes/PassThroughPolicyObligationEnforcer.md +107 -0
  45. package/docs/reference/classes/StaticPolicyInformationSource.md +7 -1
  46. package/docs/reference/index.md +5 -0
  47. package/docs/reference/interfaces/IDefaultPolicyArbiterConfig.md +17 -0
  48. package/docs/reference/interfaces/IDefaultPolicyArbiterConstructorOptions.md +39 -0
  49. package/docs/reference/interfaces/IPassThroughPolicyObligationEnforcerConstructorOptions.md +17 -0
  50. package/locales/en.json +16 -0
  51. package/package.json +2 -2
@@ -0,0 +1,824 @@
1
+ // Copyright 2025 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { ArrayHelper, Coerce, ComponentFactory, GeneralError, Guards, Is, ObjectHelper } from "@twin.org/core";
4
+ import { JsonPathHelper } from "@twin.org/data-json-path";
5
+ import { OdrlPolicyHelper, PolicyDecision, PolicyObligationEnforcerFactory } from "@twin.org/rights-management-models";
6
+ import { ConflictStrategyType, LogicalConstraintType, OperatorType } from "@twin.org/standards-w3c-odrl";
7
+ /**
8
+ * Default Policy Arbiter.
9
+ */
10
+ export class DefaultPolicyArbiter {
11
+ /**
12
+ * The class name of the Default Policy Arbiter.
13
+ */
14
+ static CLASS_NAME = "DefaultPolicyArbiter";
15
+ /**
16
+ * Type indicating the operand type is JSONPath.
17
+ * @internal
18
+ */
19
+ static _JSON_PATH_TYPE = "jsonpath";
20
+ /**
21
+ * Prefix indicating the operand encodes a JSONPath.
22
+ * @internal
23
+ */
24
+ static _JSON_PATH_OPERAND_PREFIX = `twin:${DefaultPolicyArbiter._JSON_PATH_TYPE}`;
25
+ /**
26
+ * Prefix indicating the permission target references an item in the information map.
27
+ * @internal
28
+ */
29
+ static _INFORMATION_TARGET_PREFIX = "twin:information:";
30
+ /**
31
+ * Default maximum inheritance depth.
32
+ * @internal
33
+ */
34
+ static _DEFAULT_MAX_INHERITANCE_DEPTH = 10;
35
+ /**
36
+ * The logging component.
37
+ * @internal
38
+ */
39
+ _logging;
40
+ /**
41
+ * The policy administration point component.
42
+ * @internal
43
+ */
44
+ _policyAdministrationPoint;
45
+ /**
46
+ * The maximum depth to traverse when resolving inherited policies.
47
+ * @internal
48
+ */
49
+ _maxInheritanceDepth;
50
+ /**
51
+ * Create a new instance of DefaultPolicyArbiter.
52
+ * @param options The options for the default policy arbiter.
53
+ */
54
+ constructor(options) {
55
+ this._logging = ComponentFactory.get(options?.loggingComponentType ?? "logging");
56
+ this._policyAdministrationPoint = ComponentFactory.get(options?.policyAdministrationPointComponentType ?? "policy-administration-point");
57
+ this._maxInheritanceDepth =
58
+ Coerce.integer(options?.config?.maxInheritanceDepth) ??
59
+ DefaultPolicyArbiter._DEFAULT_MAX_INHERITANCE_DEPTH;
60
+ }
61
+ /**
62
+ * Returns the class name of the component.
63
+ * @returns The class name of the component.
64
+ */
65
+ className() {
66
+ return DefaultPolicyArbiter.CLASS_NAME;
67
+ }
68
+ /**
69
+ * Makes decisions regarding policy access to data.
70
+ * @param agreement The agreement to evaluate.
71
+ * @param information Information provided by the requester to determine if a policy can be created.
72
+ * @param data The data to make a decision on.
73
+ * @param action Optional action to make a decision on, if not provided, the arbiter will evaluate all actions in the agreement.
74
+ * @returns The decisions about access to the data.
75
+ */
76
+ async decide(agreement, information, data, action) {
77
+ Guards.object(DefaultPolicyArbiter.CLASS_NAME, "agreement", agreement);
78
+ await this._logging.log({
79
+ level: "info",
80
+ source: DefaultPolicyArbiter.CLASS_NAME,
81
+ ts: Date.now(),
82
+ message: "decidingPolicy",
83
+ data: {
84
+ policyId: agreement.uid
85
+ }
86
+ });
87
+ // Resolve and merge inherited policies.
88
+ const mergedPolicy = await this.mergeInheritedPolicies(agreement);
89
+ // Extract agreement parties once for use in rule evaluation
90
+ const agreementAssigner = OdrlPolicyHelper.getPartyIds(agreement.assigner);
91
+ const agreementAssignee = OdrlPolicyHelper.getPartyIds(agreement.assignee);
92
+ // ODRL-style rule evaluation:
93
+ // - Permission rules authorize if ANY applicable permission matches (OR across permissions).
94
+ // - Default to denied when there are no permissions (closed-world for access control).
95
+ // - Conflict strategy controls how applicable permissions/prohibitions are resolved.
96
+ const permissions = ArrayHelper.fromObjectOrArray(mergedPolicy.permission ?? []);
97
+ let permissionApplies = false;
98
+ if (permissions.length > 0) {
99
+ for (const permission of permissions) {
100
+ if (await this.evaluatePermission(agreementAssigner, agreementAssignee, mergedPolicy, permission, information, data, action)) {
101
+ permissionApplies = true;
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ const prohibitions = ArrayHelper.fromObjectOrArray(mergedPolicy.prohibition ?? []);
107
+ let prohibitionApplies = false;
108
+ for (const prohibition of prohibitions) {
109
+ if (this.evaluateProhibition(agreementAssigner, agreementAssignee, prohibition, data, action)) {
110
+ prohibitionApplies = true;
111
+ break;
112
+ }
113
+ }
114
+ const conflictStrategy = mergedPolicy.conflict ?? ConflictStrategyType.Invalid;
115
+ let decision;
116
+ if (permissionApplies && prohibitionApplies) {
117
+ switch (conflictStrategy) {
118
+ case ConflictStrategyType.Perm:
119
+ decision = PolicyDecision.Granted;
120
+ break;
121
+ case ConflictStrategyType.Prohibit:
122
+ case ConflictStrategyType.Invalid:
123
+ default:
124
+ decision = PolicyDecision.Denied;
125
+ break;
126
+ }
127
+ }
128
+ else {
129
+ decision = permissionApplies ? PolicyDecision.Granted : PolicyDecision.Denied;
130
+ }
131
+ return [{ target: "$", decision }];
132
+ }
133
+ /**
134
+ * Evaluate whether a prohibition applies.
135
+ * @param agreementAssigner The assigner ID from the agreement.
136
+ * @param agreementAssignee The assignee ID from the agreement.
137
+ * @param prohibition The prohibition to evaluate.
138
+ * @param data The request data/context.
139
+ * @param action Optional action to check against the prohibition's applicable actions.
140
+ * @returns True if the prohibition applies.
141
+ * @internal
142
+ */
143
+ evaluateProhibition(agreementAssigner, agreementAssignee, prohibition, data, action) {
144
+ if (!this.isRuleApplicableToParties(prohibition, agreementAssigner, agreementAssignee)) {
145
+ return false;
146
+ }
147
+ // Check if the prohibition's action(s) match the requested action
148
+ if (!this.isActionApplicable(prohibition.action, action)) {
149
+ return false;
150
+ }
151
+ // ODRL semantics: a rule without constraints is unconditional.
152
+ const constraints = ArrayHelper.fromObjectOrArray(prohibition.constraint ?? []);
153
+ if (constraints.length === 0) {
154
+ return true;
155
+ }
156
+ return constraints.every(c => this.evaluateConstraint(c, data));
157
+ }
158
+ /**
159
+ * Merge inherited policies into the current policy.
160
+ * Inherited policies' permissions, prohibitions, and obligations are combined with the current policy's rules.
161
+ * @param policy The policy to evaluate.
162
+ * @returns A new policy with merged rules from all ancestors.
163
+ * @internal
164
+ */
165
+ async mergeInheritedPolicies(policy) {
166
+ const visitedPolicyIds = [];
167
+ visitedPolicyIds.push(policy.uid);
168
+ const inheritedPolicies = await this.resolveInheritedPolicies(policy, visitedPolicyIds, 0);
169
+ const conflictStrategies = new Set();
170
+ if (Is.stringValue(policy.conflict)) {
171
+ conflictStrategies.add(policy.conflict);
172
+ }
173
+ // Start with copies of the current policy's rules
174
+ const mergedPermissions = ArrayHelper.fromObjectOrArray(policy.permission ?? []).map(permission => this.applyPolicyPartiesToRule(policy, permission));
175
+ const mergedProhibitions = ArrayHelper.fromObjectOrArray(policy.prohibition ?? []).map(prohibition => this.applyPolicyPartiesToRule(policy, prohibition));
176
+ const mergedObligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []).map(obligation => this.applyPolicyPartiesToRule(policy, obligation));
177
+ // Merge rules from each inherited policy
178
+ for (const inheritedPolicy of inheritedPolicies) {
179
+ if (Is.stringValue(inheritedPolicy.conflict)) {
180
+ conflictStrategies.add(inheritedPolicy.conflict);
181
+ }
182
+ const inheritedPermissions = ArrayHelper.fromObjectOrArray(inheritedPolicy.permission ?? []);
183
+ if (inheritedPermissions.length > 0) {
184
+ mergedPermissions.push(...inheritedPermissions.map(permission => this.applyPolicyPartiesToRule(inheritedPolicy, permission)));
185
+ }
186
+ const inheritedProhibitions = ArrayHelper.fromObjectOrArray(inheritedPolicy.prohibition ?? []);
187
+ if (inheritedProhibitions.length > 0) {
188
+ mergedProhibitions.push(...inheritedProhibitions.map(prohibition => this.applyPolicyPartiesToRule(inheritedPolicy, prohibition)));
189
+ }
190
+ const inheritedObligations = ArrayHelper.fromObjectOrArray(inheritedPolicy.obligation ?? []);
191
+ if (inheritedObligations.length > 0) {
192
+ mergedObligations.push(...inheritedObligations.map(obligation => this.applyPolicyPartiesToRule(inheritedPolicy, obligation)));
193
+ }
194
+ }
195
+ let mergedConflict;
196
+ if (conflictStrategies.size === 1) {
197
+ mergedConflict = Array.from(conflictStrategies)[0];
198
+ }
199
+ else if (conflictStrategies.size > 1) {
200
+ mergedConflict = ConflictStrategyType.Invalid;
201
+ }
202
+ // Return a new policy with merged rules
203
+ return {
204
+ ...policy,
205
+ conflict: mergedConflict,
206
+ permission: mergedPermissions.length > 0 ? mergedPermissions : undefined,
207
+ prohibition: mergedProhibitions.length > 0 ? mergedProhibitions : undefined,
208
+ obligation: mergedObligations.length > 0 ? mergedObligations : undefined
209
+ };
210
+ }
211
+ /**
212
+ * Resolve inherited policies by their UIDs from the Policy Administration Point.
213
+ * Policies can inherit from other policies via the inheritFrom property.
214
+ * Detects circular inheritance and throws an exception if detected.
215
+ * @param policy The policy that may have inheritFrom references.
216
+ * @param visitedPolicyIds Array of policy UIDs already visited in this inheritance chain.
217
+ * @returns Array of inherited policies fetched from the PAP.
218
+ * @throws GeneralError if a parent policy cannot be found or if circular inheritance is detected.
219
+ * @internal
220
+ */
221
+ async resolveInheritedPolicies(policy, visitedPolicyIds, currentDepth) {
222
+ const inheritedPolicies = [];
223
+ // If policy has no inheritFrom, return empty array
224
+ if (Is.empty(policy.inheritFrom)) {
225
+ return inheritedPolicies;
226
+ }
227
+ const inheritFromIds = ArrayHelper.fromObjectOrArray(policy.inheritFrom) ?? [];
228
+ for (const inheritFromId of inheritFromIds) {
229
+ const nextDepth = currentDepth + 1;
230
+ if (nextDepth > this._maxInheritanceDepth) {
231
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "maxInheritanceDepthExceeded", {
232
+ policyId: policy.uid,
233
+ inheritFromId,
234
+ maxInheritanceDepth: this._maxInheritanceDepth
235
+ });
236
+ }
237
+ // Check for circular inheritance
238
+ if (visitedPolicyIds.includes(inheritFromId)) {
239
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "circularInheritanceDetected", {
240
+ policyId: policy.uid,
241
+ inheritFromId
242
+ });
243
+ }
244
+ // Fetch the inherited policy from the PAP
245
+ const inheritedPolicy = await this._policyAdministrationPoint.get(inheritFromId);
246
+ if (Is.empty(inheritedPolicy)) {
247
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "inheritedPolicyNotFound", {
248
+ inheritFromId
249
+ });
250
+ }
251
+ // Mark this policy as visited
252
+ visitedPolicyIds.push(inheritFromId);
253
+ inheritedPolicies.push(inheritedPolicy);
254
+ // Recursively resolve inherited policies of the parent
255
+ const grandparentPolicies = await this.resolveInheritedPolicies(inheritedPolicy, visitedPolicyIds, nextDepth);
256
+ inheritedPolicies.push(...grandparentPolicies);
257
+ }
258
+ return inheritedPolicies;
259
+ }
260
+ /**
261
+ * Apply policy-level assigner/assignee values to rules that don't override them.
262
+ * @param policy The policy providing defaults.
263
+ * @param rule The rule to apply defaults to.
264
+ * @returns The rule with policy-level assigner/assignee applied.
265
+ * @internal
266
+ */
267
+ applyPolicyPartiesToRule(policy, rule) {
268
+ const assigner = Is.empty(rule.assigner) ? policy.assigner : rule.assigner;
269
+ const assignee = Is.empty(rule.assignee) ? policy.assignee : rule.assignee;
270
+ const assignerIds = OdrlPolicyHelper.getPartyIds(assigner);
271
+ const ruleAssignerIds = OdrlPolicyHelper.getPartyIds(rule.assigner);
272
+ const assigneeIds = OdrlPolicyHelper.getPartyIds(assignee);
273
+ const ruleAssigneeIds = OdrlPolicyHelper.getPartyIds(rule.assignee);
274
+ let assignerEqual = false;
275
+ if (Is.empty(assignerIds) && Is.empty(ruleAssignerIds)) {
276
+ assignerEqual = true;
277
+ }
278
+ else if (!Is.empty(assignerIds) && !Is.empty(ruleAssignerIds)) {
279
+ assignerEqual =
280
+ assignerIds.length === ruleAssignerIds.length &&
281
+ assignerIds.every(id => ruleAssignerIds.includes(id));
282
+ }
283
+ let assigneeEqual = false;
284
+ if (Is.empty(assigneeIds) && Is.empty(ruleAssigneeIds)) {
285
+ assigneeEqual = true;
286
+ }
287
+ else if (!Is.empty(assigneeIds) && !Is.empty(ruleAssigneeIds)) {
288
+ assigneeEqual =
289
+ assigneeIds.length === ruleAssigneeIds.length &&
290
+ assigneeIds.every(id => ruleAssigneeIds.includes(id));
291
+ }
292
+ if (assignerEqual && assigneeEqual) {
293
+ return rule;
294
+ }
295
+ return {
296
+ ...rule,
297
+ assigner,
298
+ assignee
299
+ };
300
+ }
301
+ /**
302
+ * Determine whether a rule applies to the agreement parties based on assigner/assignee.
303
+ * @param rule The rule to evaluate.
304
+ * @param agreementAssigner The assigner ID from the agreement.
305
+ * @param agreementAssignee The assignee ID from the agreement.
306
+ * @returns True if the rule is applicable to the agreement parties.
307
+ * @internal
308
+ */
309
+ isRuleApplicableToParties(rule, agreementAssigner, agreementAssignee) {
310
+ const ruleAssigners = OdrlPolicyHelper.getPartyIds(rule.assigner);
311
+ const ruleAssignees = OdrlPolicyHelper.getPartyIds(rule.assignee);
312
+ if (!this.isPartyApplicable(ruleAssigners, agreementAssigner)) {
313
+ return false;
314
+ }
315
+ if (!this.isPartyApplicable(ruleAssignees, agreementAssignee)) {
316
+ return false;
317
+ }
318
+ return true;
319
+ }
320
+ /**
321
+ * Determine whether a rule party constraint applies to the agreement parties.
322
+ * Rule party is treated as a constraint: if specified, it must match at least one agreement party.
323
+ * @param rulePartyIds The party ids specified on the rule (if any).
324
+ * @param agreementPartyIds The party ids extracted from the agreement.
325
+ * @returns True if the rule party constraint is satisfied.
326
+ * @internal
327
+ */
328
+ isPartyApplicable(rulePartyIds, agreementPartyIds) {
329
+ // No party specified on rule means it applies to any agreement party.
330
+ if (Is.empty(rulePartyIds)) {
331
+ return true;
332
+ }
333
+ // Rule specifies party/parties, but agreement doesn't provide any.
334
+ if (Is.empty(agreementPartyIds)) {
335
+ return false;
336
+ }
337
+ for (const rulePartyId of rulePartyIds) {
338
+ if (agreementPartyIds.includes(rulePartyId)) {
339
+ return true;
340
+ }
341
+ }
342
+ return false;
343
+ }
344
+ /**
345
+ * Determine whether a rule's action(s) match the requested action.
346
+ * If no action is specified in the rule, it applies to all actions (action-agnostic).
347
+ * If an action is specified in the rule and a specific action is requested, they must match.
348
+ * If an action is specified in the rule but no specific action is requested, the rule applies (general evaluation).
349
+ * @param ruleActions The actions defined in the rule (can be string, object, or array).
350
+ * @param requestedAction The action being requested (optional).
351
+ * @returns True if the rule's action(s) apply.
352
+ * @internal
353
+ */
354
+ isActionApplicable(ruleActions, requestedAction) {
355
+ // If the rule has no action specified, it applies to all actions
356
+ if (Is.empty(ruleActions)) {
357
+ return true;
358
+ }
359
+ // If the rule has actions but no specific action is requested,
360
+ // the rule applies (we're evaluating permissions in general)
361
+ if (Is.empty(requestedAction)) {
362
+ return true;
363
+ }
364
+ // Both rule actions and requested action are specified - check for match
365
+ const ruleActionArray = ArrayHelper.fromObjectOrArray(ruleActions) ?? [];
366
+ // Check if the requested action matches any of the rule's actions
367
+ for (const ruleAction of ruleActionArray) {
368
+ const ruleActionId = Is.string(ruleAction)
369
+ ? ruleAction
370
+ : (ruleAction.uid ?? ruleAction["@id"]);
371
+ const requestedActionId = Is.string(requestedAction)
372
+ ? requestedAction
373
+ : (requestedAction?.uid ??
374
+ requestedAction?.["@id"]);
375
+ if (Is.stringValue(ruleActionId) && Is.stringValue(requestedActionId)) {
376
+ if (ruleActionId === requestedActionId) {
377
+ return true;
378
+ }
379
+ }
380
+ }
381
+ return false;
382
+ }
383
+ /**
384
+ * Apply a permission and create decisions based on that information.
385
+ * @param agreementAssigner The assigner ID from the agreement.
386
+ * @param agreementAssignee The assignee ID from the agreement.
387
+ * @param policy The policy containing the permission.
388
+ * @param permission The permission to apply.
389
+ * @param information Additional facts provided by the PIP.
390
+ * @param data The request data/context.
391
+ * @returns True if the permission applies.
392
+ * @internal
393
+ */
394
+ async evaluatePermission(agreementAssigner, agreementAssignee, policy, permission, information, data, action) {
395
+ if (!this.isRuleApplicableToParties(permission, agreementAssigner, agreementAssignee)) {
396
+ return false;
397
+ }
398
+ // Check if the permission's action(s) match the requested action
399
+ if (!this.isActionApplicable(permission.action, action)) {
400
+ return false;
401
+ }
402
+ // ODRL semantics: a Permission without constraints is unconditional.
403
+ const constraints = ArrayHelper.fromObjectOrArray(permission.constraint ?? []);
404
+ if (constraints.length === 0) {
405
+ return this.enforcePermissionDuties(policy, permission, information, data);
406
+ }
407
+ // Resolve the data context for evaluating this permission.
408
+ // If the target starts with twin:information:<key>, the key references an entry in the information map.
409
+ const ruleDataContext = this.resolveRuleDataContext(permission, information, data);
410
+ // All constraints must be satisfied for the permission to apply.
411
+ const constraintsSatisfied = constraints.every(c => this.evaluateConstraint(c, ruleDataContext));
412
+ if (!constraintsSatisfied) {
413
+ return false;
414
+ }
415
+ return this.enforcePermissionDuties(policy, permission, information, ruleDataContext);
416
+ }
417
+ /**
418
+ * Enforce duties attached to a permission.
419
+ * @param policy The policy being evaluated.
420
+ * @param permission The permission being evaluated.
421
+ * @param information Additional facts provided by the PIP.
422
+ * @param data The request data/context.
423
+ * @returns True if all duties are enforced or none are present.
424
+ * @internal
425
+ */
426
+ async enforcePermissionDuties(policy, permission, information, data) {
427
+ const duties = ArrayHelper.fromObjectOrArray(permission.duty ?? []);
428
+ if (duties.length === 0) {
429
+ return true;
430
+ }
431
+ for (const duty of duties) {
432
+ const enforced = await this.enforceDuty(policy, duty, information, data);
433
+ if (!enforced) {
434
+ return false;
435
+ }
436
+ }
437
+ return true;
438
+ }
439
+ /**
440
+ * Enforce a single duty using registered obligation enforcers.
441
+ * @param policy The policy being evaluated.
442
+ * @param duty The duty to enforce.
443
+ * @param information Additional facts provided by the PIP.
444
+ * @param data The request data/context.
445
+ * @returns True if any enforcer succeeds.
446
+ * @internal
447
+ */
448
+ async enforceDuty(policy, duty, information, data) {
449
+ const enforcerNames = PolicyObligationEnforcerFactory.names();
450
+ if (enforcerNames.length === 0) {
451
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "noObligationEnforcersRegistered");
452
+ }
453
+ for (const enforcerName of enforcerNames) {
454
+ const enforcer = PolicyObligationEnforcerFactory.get(enforcerName);
455
+ if (await enforcer.enforce(policy, duty, information, data)) {
456
+ return true;
457
+ }
458
+ }
459
+ return false;
460
+ }
461
+ /**
462
+ * Resolve the data context used when evaluating a permission's constraints.
463
+ * If the permission target is `twin:information:<key>`, the key references an entry in the information map.
464
+ * @param rule The permission being evaluated.
465
+ * @param information Additional facts provided by the PIP.
466
+ * @param data The request data/context.
467
+ * @returns The data context to use for JSONPath resolution.
468
+ * @throws GeneralError When the information target key is missing.
469
+ * @internal
470
+ */
471
+ resolveRuleDataContext(rule, information, data) {
472
+ const targetId = this.getTargetId(rule.target);
473
+ if (targetId?.startsWith(DefaultPolicyArbiter._INFORMATION_TARGET_PREFIX)) {
474
+ const key = targetId.slice(DefaultPolicyArbiter._INFORMATION_TARGET_PREFIX.length);
475
+ if (Is.empty(key) || Is.empty(information?.[key])) {
476
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "informationTargetMissing", {
477
+ key
478
+ });
479
+ }
480
+ return information[key];
481
+ }
482
+ return data;
483
+ }
484
+ /**
485
+ * Extract the target id from the permission.
486
+ * @param target The permission target.
487
+ * @returns The information key, or undefined when the target is not an information reference.
488
+ * @internal
489
+ */
490
+ getTargetId(target) {
491
+ if (Is.undefined(target)) {
492
+ return undefined;
493
+ }
494
+ if (Is.array(target) && target.length > 1) {
495
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "multipleTargetsNotSupported");
496
+ }
497
+ const arr = ArrayHelper.fromObjectOrArray(target ?? []);
498
+ if (arr.length === 0) {
499
+ return undefined;
500
+ }
501
+ return Is.string(arr[0]) ? arr[0] : arr[0].uid;
502
+ }
503
+ /**
504
+ * Evaluate a single ODRL constraint against the available context.
505
+ * Supports logical constraint composition through nested refinements.
506
+ * @param constraint The constraint to evaluate.
507
+ * @param ruleDataContext The request data/context.
508
+ * @returns True if the constraint is satisfied.
509
+ * @internal
510
+ */
511
+ evaluateConstraint(constraint, ruleDataContext) {
512
+ const logicalConstraint = this.getLogicalConstraintOperands(constraint);
513
+ if (logicalConstraint) {
514
+ return this.evaluateLogicalConstraint(logicalConstraint, ruleDataContext);
515
+ }
516
+ // Must be a regular constraint beyond this point
517
+ const regularConstraint = constraint;
518
+ // Evaluate the main constraint condition
519
+ const leftValue = this.calculateOperandValue(regularConstraint.leftOperand, ruleDataContext);
520
+ const rightValue = this.calculateOperandValue(regularConstraint.rightOperand, ruleDataContext);
521
+ const mainSatisfied = this.evaluateOperator(regularConstraint.operator, leftValue, rightValue);
522
+ // If main constraint is not satisfied, the overall constraint fails
523
+ return mainSatisfied;
524
+ }
525
+ /**
526
+ * Extract logical constraint operands when present.
527
+ * @param constraint The constraint to inspect.
528
+ * @returns The logical operator and its operands, or undefined when not logical.
529
+ * @throws GeneralError if logical constraint operands are not unique.
530
+ * @internal
531
+ */
532
+ getLogicalConstraintOperands(constraint) {
533
+ const logicalConstraint = constraint;
534
+ const operators = Object.values(LogicalConstraintType);
535
+ for (const operator of operators) {
536
+ const value = logicalConstraint[operator];
537
+ if (!Is.undefined(value)) {
538
+ const constraints = this.normalizeLogicalConstraintOperands(value);
539
+ this.validateLogicalConstraintOperandUniqueness(constraints, operator);
540
+ return {
541
+ operator,
542
+ constraints
543
+ };
544
+ }
545
+ }
546
+ return undefined;
547
+ }
548
+ /**
549
+ * Normalize logical constraint operands into a constraint array.
550
+ * Handles both IOdrlConstraint and IOdrlLogicalConstraint types.
551
+ * @param raw The raw operand value.
552
+ * @returns The constraint array.
553
+ * @internal
554
+ */
555
+ normalizeLogicalConstraintOperands(raw) {
556
+ let normalized = raw;
557
+ if (Is.object(normalized) &&
558
+ !Is.undefined(normalized["@list"])) {
559
+ normalized = normalized["@list"];
560
+ }
561
+ return (ArrayHelper.fromObjectOrArray(normalized) ?? [])
562
+ .filter(item => Is.object(item))
563
+ .map(item => item);
564
+ }
565
+ /**
566
+ * Validate that all operands in a logical constraint are unique.
567
+ * ODRL spec 2.5.2 requires that all operand values MUST be unique Constraint instances.
568
+ * Uniqueness is checked by uid property and id property.
569
+ * @param constraints The constraint operands to validate.
570
+ * @param operator The logical operator type (for error messaging).
571
+ * @throws GeneralError if duplicate constraints are found.
572
+ * @internal
573
+ */
574
+ validateLogicalConstraintOperandUniqueness(constraints, operator) {
575
+ const seenIdentifiers = new Set();
576
+ for (let i = 0; i < constraints.length; i++) {
577
+ const constraint = constraints[i];
578
+ let identifier;
579
+ // Try to get identifier from uid property
580
+ if (Is.stringValue(constraint.uid)) {
581
+ identifier = constraint.uid;
582
+ }
583
+ else {
584
+ // Try to get identifier from @id property (JSON-LD)
585
+ const jsonLdId = constraint["@id"];
586
+ if (Is.stringValue(jsonLdId)) {
587
+ identifier = jsonLdId;
588
+ }
589
+ }
590
+ // If we have an identifier, check for duplicates
591
+ if (Is.stringValue(identifier)) {
592
+ if (seenIdentifiers.has(identifier)) {
593
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "logicalConstraintOperandNotUnique", {
594
+ operator,
595
+ identifier,
596
+ index: i
597
+ });
598
+ }
599
+ seenIdentifiers.add(identifier);
600
+ }
601
+ }
602
+ }
603
+ /**
604
+ * Evaluate a logical constraint operator against its operands.
605
+ * @param logicalConstraint The operator and operand list.
606
+ * @param ruleDataContext The request data/context.
607
+ * @returns True if the logical constraint is satisfied.
608
+ * @internal
609
+ */
610
+ evaluateLogicalConstraint(logicalConstraint, ruleDataContext) {
611
+ const { operator, constraints } = logicalConstraint;
612
+ if (constraints.length === 0) {
613
+ return false;
614
+ }
615
+ switch (operator) {
616
+ case LogicalConstraintType.And:
617
+ return constraints.every(item => this.evaluateConstraint(item, ruleDataContext));
618
+ case LogicalConstraintType.AndSequence: {
619
+ for (const item of constraints) {
620
+ if (!this.evaluateConstraint(item, ruleDataContext)) {
621
+ return false;
622
+ }
623
+ }
624
+ return true;
625
+ }
626
+ case LogicalConstraintType.Or:
627
+ return constraints.some(item => this.evaluateConstraint(item, ruleDataContext));
628
+ case LogicalConstraintType.Xone: {
629
+ let satisfied = 0;
630
+ for (const item of constraints) {
631
+ if (this.evaluateConstraint(item, ruleDataContext)) {
632
+ satisfied += 1;
633
+ if (satisfied > 1) {
634
+ return false;
635
+ }
636
+ }
637
+ }
638
+ return satisfied === 1;
639
+ }
640
+ default:
641
+ return false;
642
+ }
643
+ }
644
+ /**
645
+ * Calculate an operand value.
646
+ * @param operand The operand.
647
+ * @param ruleDataContext The request data/context.
648
+ * @returns The resolved operand value.
649
+ * @internal
650
+ */
651
+ calculateOperandValue(operand, ruleDataContext) {
652
+ // Treat JSONPath operands as selectors against the current rule data context.
653
+ // Will be in the format twin:jsonpath:<jsonPath>
654
+ let jsonPath;
655
+ if (Is.stringValue(operand)) {
656
+ if (operand.startsWith(`${DefaultPolicyArbiter._JSON_PATH_OPERAND_PREFIX}:`)) {
657
+ jsonPath = operand.slice(DefaultPolicyArbiter._JSON_PATH_OPERAND_PREFIX.length + 1);
658
+ if (jsonPath.length === 0) {
659
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathOperandMissingTarget", {
660
+ operand
661
+ });
662
+ }
663
+ }
664
+ }
665
+ else if (Is.object(operand)) {
666
+ // Is this an object { "@value": "18", "@type": "xsd:integer" } ?
667
+ const value = operand["@value"];
668
+ const type = operand["@type"];
669
+ if (Is.stringValue(type)) {
670
+ // If the type is set we can try and extract the value
671
+ // is it twin:jsonpath ?
672
+ if (type === DefaultPolicyArbiter._JSON_PATH_OPERAND_PREFIX) {
673
+ if (Is.stringValue(value)) {
674
+ jsonPath = value;
675
+ }
676
+ }
677
+ else {
678
+ const xsdValue = this.coerceXsdType(value, type);
679
+ if (!Is.undefined(xsdValue)) {
680
+ return xsdValue;
681
+ }
682
+ }
683
+ }
684
+ }
685
+ // We have a JSON Path to resolve
686
+ if (Is.stringValue(jsonPath)) {
687
+ const jsonPaths = JsonPathHelper.query(jsonPath, ruleDataContext);
688
+ if (jsonPaths.length === 0) {
689
+ // No matches
690
+ return undefined;
691
+ }
692
+ else if (jsonPaths.length === 1) {
693
+ // Single match - return the value directly
694
+ return jsonPaths[0].value;
695
+ }
696
+ // Multiple matches - return array of values
697
+ return jsonPaths.map(p => p.value);
698
+ }
699
+ // Not JSON Path or object value so return as is
700
+ return operand;
701
+ }
702
+ /**
703
+ * Evaluate an ODRL operator against resolved operands.
704
+ * @param operator The operator.
705
+ * @param left The resolved left operand.
706
+ * @param right The resolved right operand.
707
+ * @returns True if the comparison is satisfied.
708
+ * @internal
709
+ */
710
+ evaluateOperator(operator, left, right) {
711
+ // Handle array/collection left values (e.g. JSONPath returning multiple matches).
712
+ const leftValues = ArrayHelper.fromObjectOrArray(left ?? []);
713
+ switch (operator) {
714
+ case OperatorType.Eq:
715
+ return leftValues.some(v => ObjectHelper.equal(v, right, false));
716
+ case OperatorType.Neq:
717
+ return leftValues.every(v => !ObjectHelper.equal(v, right, false));
718
+ case OperatorType.Gt:
719
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a > b));
720
+ case OperatorType.Gteq:
721
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a >= b));
722
+ case OperatorType.Lt:
723
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a < b));
724
+ case OperatorType.Lteq:
725
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a <= b));
726
+ case OperatorType.IsAnyOf: {
727
+ return leftValues.some(v => {
728
+ const stringValue = typeof v === "string" || typeof v === "number" || typeof v === "boolean"
729
+ ? String(v)
730
+ : JSON.stringify(v);
731
+ return (ArrayHelper.fromObjectOrArray(right) ?? []).includes(stringValue);
732
+ });
733
+ }
734
+ case OperatorType.IsAllOf: {
735
+ return ObjectHelper.equal(leftValues, ArrayHelper.fromObjectOrArray(right) ?? [], false);
736
+ }
737
+ case OperatorType.IsNoneOf: {
738
+ return leftValues.every(v => !(ArrayHelper.fromObjectOrArray(right) ?? []).includes(v));
739
+ }
740
+ case OperatorType.LocTimeEq:
741
+ return leftValues.some(v => ObjectHelper.equal(v, right, false));
742
+ case OperatorType.LocTimeGteq:
743
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a >= b));
744
+ case OperatorType.IsA:
745
+ case OperatorType.HasPart:
746
+ case OperatorType.IsPartOf:
747
+ // For now, treat these as simple equality/ordering semantics where meaningful.
748
+ // Profiles can introduce richer semantics via additional arbiters.
749
+ return leftValues.some(v => ObjectHelper.equal(v, right, false));
750
+ default:
751
+ return false;
752
+ }
753
+ }
754
+ /**
755
+ * Compare values with numeric/date/string coercion.
756
+ * @param left The left value.
757
+ * @param right The right value.
758
+ * @param compare Comparison operator.
759
+ * @returns True if ordered comparison passes.
760
+ * @internal
761
+ */
762
+ compareOrdered(left, right, compare) {
763
+ const leftNum = Coerce.number(left);
764
+ const rightNum = Coerce.number(right);
765
+ if (!Is.undefined(leftNum) && !Is.undefined(rightNum)) {
766
+ return compare(leftNum, rightNum);
767
+ }
768
+ const leftDate = Coerce.dateTime(left);
769
+ const rightDate = Coerce.dateTime(right);
770
+ if (!Is.undefined(leftDate) && !Is.undefined(rightDate)) {
771
+ return compare(leftDate.getTime(), rightDate.getTime());
772
+ }
773
+ // Only use string ordering when both operands are actual strings.
774
+ // Avoid coercing other types into strings, as that can cause
775
+ // unintended comparisons like 18 >= "$.minAge" evaluating to true.
776
+ if (Is.string(left) && Is.string(right)) {
777
+ return compare(left.localeCompare(right), 0);
778
+ }
779
+ return false;
780
+ }
781
+ /**
782
+ * Coerce a value to a specific XSD type.
783
+ * @param value The value to coerce.
784
+ * @param type The XSD type.
785
+ * @returns The coerced value, or undefined when coercion is not possible.
786
+ * @internal
787
+ */
788
+ coerceXsdType(value, type) {
789
+ if ([
790
+ "xsd:string",
791
+ "xsd:normalizedString",
792
+ "xsd:token",
793
+ "xsd:anyURI",
794
+ "xsd:QName",
795
+ "xsd:NOTATION"
796
+ ].includes(type)) {
797
+ // Handle standard xsd types
798
+ return Coerce.string(value);
799
+ }
800
+ else if ([
801
+ "xsd:integer",
802
+ "xsd:decimal",
803
+ "xsd:float",
804
+ "xsd:double",
805
+ "xsd:long",
806
+ "xsd:int",
807
+ "xsd:short",
808
+ "xsd:byte"
809
+ ].includes(type)) {
810
+ // Handle standard xsd types
811
+ return Coerce.number(value);
812
+ }
813
+ else if (type === "xsd:boolean") {
814
+ // Handle standard xsd types
815
+ return Coerce.boolean(value);
816
+ }
817
+ else if (["xsd:date", "xsd:dateTime", "xsd:time"].includes(type)) {
818
+ // Handle standard xsd types
819
+ return Coerce.dateTime(value);
820
+ }
821
+ return undefined;
822
+ }
823
+ }
824
+ //# sourceMappingURL=defaultPolicyArbiter.js.map