@twin.org/rights-management-plugins 0.0.3-next.25 → 0.0.3-next.26
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.
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { ArrayHelper, Coerce, ComponentFactory, GeneralError, Guards, Is, ObjectHelper } from "@twin.org/core";
|
|
4
4
|
import { JsonPathHelper } from "@twin.org/data-json-path";
|
|
5
5
|
import { OdrlPolicyHelper, PolicyDecision, PolicyObligationEnforcerFactory } from "@twin.org/rights-management-models";
|
|
6
|
-
import { OdrlConflictStrategyType, OdrlLogicalConstraintType, OdrlOperatorType } from "@twin.org/standards-w3c-odrl";
|
|
6
|
+
import { OdrlConflictStrategyType, OdrlLogicalConstraintType, OdrlOperatorType, OdrlTypes } from "@twin.org/standards-w3c-odrl";
|
|
7
7
|
/**
|
|
8
8
|
* Default Policy Arbiter.
|
|
9
9
|
*/
|
|
@@ -13,25 +13,25 @@ export class DefaultPolicyArbiter {
|
|
|
13
13
|
*/
|
|
14
14
|
static CLASS_NAME = "DefaultPolicyArbiter";
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Default maximum inheritance depth.
|
|
17
17
|
* @internal
|
|
18
18
|
*/
|
|
19
|
-
static
|
|
19
|
+
static _DEFAULT_MAX_INHERITANCE_DEPTH = 10;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* TWIN prefix operations.
|
|
22
22
|
* @internal
|
|
23
23
|
*/
|
|
24
|
-
static
|
|
24
|
+
static _TWIN_PREFIX_OPERATIONS = "twin:";
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* TWIN prefix JSONPath.
|
|
27
27
|
* @internal
|
|
28
28
|
*/
|
|
29
|
-
static
|
|
29
|
+
static _TWIN_PREFIX_JSONPATH = "jsonpath";
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* TWIN prefix information.
|
|
32
32
|
* @internal
|
|
33
33
|
*/
|
|
34
|
-
static
|
|
34
|
+
static _TWIN_PREFIX_INFORMATION = "information";
|
|
35
35
|
/**
|
|
36
36
|
* The logging component.
|
|
37
37
|
* @internal
|
|
@@ -75,6 +75,14 @@ export class DefaultPolicyArbiter {
|
|
|
75
75
|
*/
|
|
76
76
|
async decide(agreement, information, data, action) {
|
|
77
77
|
Guards.object(DefaultPolicyArbiter.CLASS_NAME, "agreement", agreement);
|
|
78
|
+
// ODRL policy profiles extend the vocabulary with additional semantics (e.g. custom
|
|
79
|
+
// operators, left operands). Without profile-aware evaluation logic the arbiter
|
|
80
|
+
// cannot guarantee correctness, so any policy that declares a profile is rejected.
|
|
81
|
+
if (Is.notEmpty(agreement.profile)) {
|
|
82
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "policyProfileNotSupported", {
|
|
83
|
+
policyId: OdrlPolicyHelper.getUid(agreement) ?? ""
|
|
84
|
+
});
|
|
85
|
+
}
|
|
78
86
|
await this._logging.log({
|
|
79
87
|
level: "info",
|
|
80
88
|
source: DefaultPolicyArbiter.CLASS_NAME,
|
|
@@ -86,74 +94,251 @@ export class DefaultPolicyArbiter {
|
|
|
86
94
|
});
|
|
87
95
|
// Resolve and merge inherited policies.
|
|
88
96
|
const mergedPolicy = await this.mergeInheritedPolicies(agreement);
|
|
97
|
+
const expandedPolicy = this.expandCompactPolicyRules(mergedPolicy);
|
|
98
|
+
const dataSources = {
|
|
99
|
+
[`${DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS}${DefaultPolicyArbiter._TWIN_PREFIX_JSONPATH}`]: data,
|
|
100
|
+
[`${DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS}${DefaultPolicyArbiter._TWIN_PREFIX_INFORMATION}`]: information
|
|
101
|
+
};
|
|
89
102
|
// Extract agreement parties once for use in rule evaluation
|
|
90
103
|
const agreementAssigner = OdrlPolicyHelper.getPartyIds(agreement.assigner);
|
|
91
104
|
const agreementAssignee = OdrlPolicyHelper.getPartyIds(agreement.assignee);
|
|
92
|
-
|
|
93
|
-
// -
|
|
94
|
-
// -
|
|
95
|
-
// -
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
const obligationsFulfilled = await this.evaluatePolicyObligations(agreementAssigner, agreementAssignee, expandedPolicy, dataSources);
|
|
106
|
+
// ODRL-style rule evaluation grouped by decision target:
|
|
107
|
+
// - Permission rules authorize if ANY applicable permission on the same target matches.
|
|
108
|
+
// - Default to denied when no permission applies on a target (closed-world for access control).
|
|
109
|
+
// - Conflict strategy controls how applicable permissions/prohibitions are resolved per target.
|
|
110
|
+
const targetStates = Object.create(null);
|
|
111
|
+
const permissions = ArrayHelper.fromObjectOrArray(expandedPolicy.permission ?? []);
|
|
112
|
+
for (const permission of permissions) {
|
|
113
|
+
const decisionTargets = this.resolveRuleDecisionTargets(permission, dataSources);
|
|
114
|
+
for (const decisionTarget of decisionTargets) {
|
|
115
|
+
const state = this.getOrCreateTargetState(targetStates, decisionTarget.target);
|
|
116
|
+
if (await this.evaluatePermission(agreementAssigner, agreementAssignee, expandedPolicy, permission, decisionTarget.refinements, dataSources, action, decisionTarget.target)) {
|
|
117
|
+
state.permissionApplies = true;
|
|
103
118
|
}
|
|
104
119
|
}
|
|
105
120
|
}
|
|
106
|
-
const prohibitions = ArrayHelper.fromObjectOrArray(
|
|
107
|
-
let prohibitionApplies = false;
|
|
121
|
+
const prohibitions = ArrayHelper.fromObjectOrArray(expandedPolicy.prohibition ?? []);
|
|
108
122
|
for (const prohibition of prohibitions) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
const decisionTargets = this.resolveRuleDecisionTargets(prohibition, dataSources);
|
|
124
|
+
for (const decisionTarget of decisionTargets) {
|
|
125
|
+
if (await this.evaluateProhibition(expandedPolicy, agreementAssigner, agreementAssignee, prohibition, decisionTarget.refinements, dataSources, action, decisionTarget.target)) {
|
|
126
|
+
const state = this.getOrCreateTargetState(targetStates, decisionTarget.target);
|
|
127
|
+
state.prohibitionApplies = true;
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
130
|
}
|
|
114
|
-
const conflictStrategy =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
const conflictStrategy = expandedPolicy.conflict ?? OdrlConflictStrategyType.Invalid;
|
|
132
|
+
if (Object.keys(targetStates).length === 0) {
|
|
133
|
+
// Closed-world fallback when the policy has no rules at all.
|
|
134
|
+
return [{ decision: PolicyDecision.Denied, target: "$" }];
|
|
135
|
+
}
|
|
136
|
+
const decisions = [];
|
|
137
|
+
for (const [target, state] of Object.entries(targetStates)) {
|
|
138
|
+
let decision;
|
|
139
|
+
if (state.permissionApplies && state.prohibitionApplies) {
|
|
140
|
+
switch (conflictStrategy) {
|
|
141
|
+
case OdrlConflictStrategyType.Perm:
|
|
142
|
+
decision = PolicyDecision.Granted;
|
|
143
|
+
break;
|
|
144
|
+
case OdrlConflictStrategyType.Prohibit:
|
|
145
|
+
case OdrlConflictStrategyType.Invalid:
|
|
146
|
+
default:
|
|
147
|
+
decision = PolicyDecision.Denied;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
decision = state.permissionApplies ? PolicyDecision.Granted : PolicyDecision.Denied;
|
|
126
153
|
}
|
|
154
|
+
if (!obligationsFulfilled) {
|
|
155
|
+
decision = PolicyDecision.Denied;
|
|
156
|
+
}
|
|
157
|
+
decisions.push({
|
|
158
|
+
decision,
|
|
159
|
+
target
|
|
160
|
+
});
|
|
127
161
|
}
|
|
128
|
-
|
|
129
|
-
|
|
162
|
+
return decisions;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Expand compact/compound policy rule forms into atomic rules.
|
|
166
|
+
* ODRL 2.7 allows compact forms where rule properties can be arrays.
|
|
167
|
+
* Evaluation in this arbiter is performed on expanded atomic rules.
|
|
168
|
+
* @param policy The policy to expand.
|
|
169
|
+
* @returns A policy with expanded rule arrays.
|
|
170
|
+
* @internal
|
|
171
|
+
*/
|
|
172
|
+
expandCompactPolicyRules(policy) {
|
|
173
|
+
const expandedPermissions = this.expandRules(ArrayHelper.fromObjectOrArray(policy.permission ?? []));
|
|
174
|
+
const expandedProhibitions = this.expandRules(ArrayHelper.fromObjectOrArray(policy.prohibition ?? []));
|
|
175
|
+
const expandedObligations = this.expandRules(ArrayHelper.fromObjectOrArray(policy.obligation ?? []));
|
|
176
|
+
return {
|
|
177
|
+
...policy,
|
|
178
|
+
permission: expandedPermissions.length > 0 ? expandedPermissions : undefined,
|
|
179
|
+
prohibition: expandedProhibitions.length > 0 ? expandedProhibitions : undefined,
|
|
180
|
+
obligation: expandedObligations.length > 0 ? expandedObligations : undefined
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Expand a rule list into atomic rules.
|
|
185
|
+
* @param rules The rules to expand.
|
|
186
|
+
* @returns Expanded atomic rules.
|
|
187
|
+
* @internal
|
|
188
|
+
*/
|
|
189
|
+
expandRules(rules) {
|
|
190
|
+
const expanded = [];
|
|
191
|
+
for (const rule of rules) {
|
|
192
|
+
expanded.push(...this.expandRule(rule));
|
|
193
|
+
}
|
|
194
|
+
return expanded;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Expand a single compact/compound rule into atomic rules.
|
|
198
|
+
* @param rule The rule to expand.
|
|
199
|
+
* @returns Expanded atomic rules.
|
|
200
|
+
* @internal
|
|
201
|
+
*/
|
|
202
|
+
expandRule(rule) {
|
|
203
|
+
const targets = this.normalizeRuleField(rule.target);
|
|
204
|
+
const actions = this.normalizeRuleField(rule.action);
|
|
205
|
+
const assigners = this.normalizeRuleField(rule.assigner);
|
|
206
|
+
const assignees = this.normalizeRuleField(rule.assignee);
|
|
207
|
+
const expanded = [];
|
|
208
|
+
for (const target of targets) {
|
|
209
|
+
for (const action of actions) {
|
|
210
|
+
for (const assigner of assigners) {
|
|
211
|
+
for (const assignee of assignees) {
|
|
212
|
+
const atomicRule = { ...rule };
|
|
213
|
+
atomicRule.target = target;
|
|
214
|
+
atomicRule.action = action;
|
|
215
|
+
atomicRule.assigner = assigner;
|
|
216
|
+
atomicRule.assignee = assignee;
|
|
217
|
+
expanded.push(atomicRule);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return expanded;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Normalize a potentially compact rule field into an array for expansion.
|
|
226
|
+
* @param value The field value.
|
|
227
|
+
* @returns Normalized values (or a single undefined when not provided).
|
|
228
|
+
* @internal
|
|
229
|
+
*/
|
|
230
|
+
normalizeRuleField(value) {
|
|
231
|
+
if (Is.undefined(value)) {
|
|
232
|
+
return [undefined];
|
|
233
|
+
}
|
|
234
|
+
const values = ArrayHelper.fromObjectOrArray(value);
|
|
235
|
+
return values.length > 0 ? values : [undefined];
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get an existing target state or create an initial state if it doesn't exist.
|
|
239
|
+
* @param targetStates The dictionary of target states.
|
|
240
|
+
* @param target The target key.
|
|
241
|
+
* @returns The target state.
|
|
242
|
+
* @internal
|
|
243
|
+
*/
|
|
244
|
+
getOrCreateTargetState(targetStates, target) {
|
|
245
|
+
let state = targetStates[target];
|
|
246
|
+
if (Is.undefined(state)) {
|
|
247
|
+
state = {
|
|
248
|
+
permissionApplies: false,
|
|
249
|
+
prohibitionApplies: false
|
|
250
|
+
};
|
|
251
|
+
targetStates[target] = state;
|
|
130
252
|
}
|
|
131
|
-
return
|
|
253
|
+
return state;
|
|
132
254
|
}
|
|
133
255
|
/**
|
|
134
256
|
* Evaluate whether a prohibition applies.
|
|
257
|
+
* @param policy The policy containing the prohibition.
|
|
135
258
|
* @param agreementAssigner The assigner ID from the agreement.
|
|
136
259
|
* @param agreementAssignee The assignee ID from the agreement.
|
|
137
260
|
* @param prohibition The prohibition to evaluate.
|
|
138
|
-
* @param
|
|
261
|
+
* @param targetRefinements Additional constraints from target refinement.
|
|
262
|
+
* @param dataSources The operand lookup sources.
|
|
139
263
|
* @param action Optional action to check against the prohibition's applicable actions.
|
|
140
264
|
* @returns True if the prohibition applies.
|
|
141
265
|
* @internal
|
|
142
266
|
*/
|
|
143
|
-
evaluateProhibition(agreementAssigner, agreementAssignee, prohibition,
|
|
144
|
-
if (!this.isRuleApplicableToParties(prohibition, agreementAssigner, agreementAssignee)) {
|
|
267
|
+
async evaluateProhibition(policy, agreementAssigner, agreementAssignee, prohibition, targetRefinements, dataSources, action, decisionTarget) {
|
|
268
|
+
if (!this.isRuleApplicableToParties(prohibition, agreementAssigner, agreementAssignee, dataSources)) {
|
|
145
269
|
return false;
|
|
146
270
|
}
|
|
147
271
|
// Check if the prohibition's action(s) match the requested action
|
|
148
|
-
if (!this.isActionApplicable(prohibition.action, action)) {
|
|
272
|
+
if (!this.isActionApplicable(prohibition.action, action, dataSources)) {
|
|
149
273
|
return false;
|
|
150
274
|
}
|
|
151
275
|
// ODRL semantics: a rule without constraints is unconditional.
|
|
152
|
-
const constraints =
|
|
153
|
-
|
|
276
|
+
const constraints = [
|
|
277
|
+
...ArrayHelper.fromObjectOrArray(prohibition.constraint ?? []),
|
|
278
|
+
...targetRefinements
|
|
279
|
+
];
|
|
280
|
+
if (constraints.length > 0 &&
|
|
281
|
+
!constraints.every(c => this.evaluateConstraint(c, dataSources))) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const prohibitionTargetLookup = this.tryResolveTargetDataSource(this.buildRuleDataContextTargetId(this.getRuleDataContextTargetId(prohibition.target), decisionTarget), dataSources, true);
|
|
285
|
+
const ruleDataContext = prohibitionTargetLookup.value;
|
|
286
|
+
const remedies = ArrayHelper.fromObjectOrArray(prohibition.remedy ?? []);
|
|
287
|
+
if (remedies.length === 0) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
for (const remedy of remedies) {
|
|
291
|
+
if (!(await this.enforceDuty(policy, remedy, dataSources, ruleDataContext))) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Remedies satisfied: prohibition is treated as no longer infringed.
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Evaluate all policy-level obligations.
|
|
300
|
+
* @param agreementAssigner The assigner ID from the agreement.
|
|
301
|
+
* @param agreementAssignee The assignee ID from the agreement.
|
|
302
|
+
* @param policy The policy containing obligations.
|
|
303
|
+
* @param dataSources The operand lookup sources.
|
|
304
|
+
* @returns True if all applicable obligations are fulfilled.
|
|
305
|
+
* @internal
|
|
306
|
+
*/
|
|
307
|
+
async evaluatePolicyObligations(agreementAssigner, agreementAssignee, policy, dataSources) {
|
|
308
|
+
const obligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []);
|
|
309
|
+
for (const obligation of obligations) {
|
|
310
|
+
if (!(await this.evaluateObligation(agreementAssigner, agreementAssignee, policy, obligation, dataSources))) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Evaluate whether a policy-level obligation is fulfilled.
|
|
318
|
+
* @param agreementAssigner The assigner ID from the agreement.
|
|
319
|
+
* @param agreementAssignee The assignee ID from the agreement.
|
|
320
|
+
* @param policy The policy containing the obligation.
|
|
321
|
+
* @param obligation The obligation to evaluate.
|
|
322
|
+
* @param dataSources The operand lookup sources.
|
|
323
|
+
* @returns True if the obligation is not applicable or is fulfilled.
|
|
324
|
+
* @internal
|
|
325
|
+
*/
|
|
326
|
+
async evaluateObligation(agreementAssigner, agreementAssignee, policy, obligation, dataSources) {
|
|
327
|
+
if (!this.isRuleApplicableToParties(obligation, agreementAssigner, agreementAssignee, dataSources)) {
|
|
154
328
|
return true;
|
|
155
329
|
}
|
|
156
|
-
|
|
330
|
+
const { refinements } = this.resolveRuleTarget(obligation, dataSources);
|
|
331
|
+
const obligationTargetLookup = this.tryResolveTargetDataSource(this.getTargetId(obligation.target), dataSources, true);
|
|
332
|
+
const ruleDataContext = obligationTargetLookup.value;
|
|
333
|
+
const constraints = [
|
|
334
|
+
...ArrayHelper.fromObjectOrArray(obligation.constraint ?? []),
|
|
335
|
+
...refinements
|
|
336
|
+
];
|
|
337
|
+
if (constraints.length > 0 &&
|
|
338
|
+
!constraints.every(c => this.evaluateConstraint(c, dataSources))) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
return this.enforceDuty(policy, obligation, dataSources, ruleDataContext);
|
|
157
342
|
}
|
|
158
343
|
/**
|
|
159
344
|
* Merge inherited policies into the current policy.
|
|
@@ -171,9 +356,9 @@ export class DefaultPolicyArbiter {
|
|
|
171
356
|
conflictStrategies.add(policy.conflict);
|
|
172
357
|
}
|
|
173
358
|
// Start with copies of the current policy's rules
|
|
174
|
-
const mergedPermissions = ArrayHelper.fromObjectOrArray(policy.permission ?? []).map(permission => this.
|
|
175
|
-
const mergedProhibitions = ArrayHelper.fromObjectOrArray(policy.prohibition ?? []).map(prohibition => this.
|
|
176
|
-
const mergedObligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []).map(obligation => this.
|
|
359
|
+
const mergedPermissions = ArrayHelper.fromObjectOrArray(policy.permission ?? []).map(permission => this.applyPolicyDefaultsToRule(policy, permission));
|
|
360
|
+
const mergedProhibitions = ArrayHelper.fromObjectOrArray(policy.prohibition ?? []).map(prohibition => this.applyPolicyDefaultsToRule(policy, prohibition));
|
|
361
|
+
const mergedObligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []).map(obligation => this.applyPolicyDefaultsToRule(policy, obligation));
|
|
177
362
|
// Merge rules from each inherited policy
|
|
178
363
|
for (const inheritedPolicy of inheritedPolicies) {
|
|
179
364
|
if (Is.stringValue(inheritedPolicy.conflict)) {
|
|
@@ -181,15 +366,15 @@ export class DefaultPolicyArbiter {
|
|
|
181
366
|
}
|
|
182
367
|
const inheritedPermissions = ArrayHelper.fromObjectOrArray(inheritedPolicy.permission ?? []);
|
|
183
368
|
if (inheritedPermissions.length > 0) {
|
|
184
|
-
mergedPermissions.push(...inheritedPermissions.map(permission => this.
|
|
369
|
+
mergedPermissions.push(...inheritedPermissions.map(permission => this.applyPolicyDefaultsToRule(inheritedPolicy, permission)));
|
|
185
370
|
}
|
|
186
371
|
const inheritedProhibitions = ArrayHelper.fromObjectOrArray(inheritedPolicy.prohibition ?? []);
|
|
187
372
|
if (inheritedProhibitions.length > 0) {
|
|
188
|
-
mergedProhibitions.push(...inheritedProhibitions.map(prohibition => this.
|
|
373
|
+
mergedProhibitions.push(...inheritedProhibitions.map(prohibition => this.applyPolicyDefaultsToRule(inheritedPolicy, prohibition)));
|
|
189
374
|
}
|
|
190
375
|
const inheritedObligations = ArrayHelper.fromObjectOrArray(inheritedPolicy.obligation ?? []);
|
|
191
376
|
if (inheritedObligations.length > 0) {
|
|
192
|
-
mergedObligations.push(...inheritedObligations.map(obligation => this.
|
|
377
|
+
mergedObligations.push(...inheritedObligations.map(obligation => this.applyPolicyDefaultsToRule(inheritedPolicy, obligation)));
|
|
193
378
|
}
|
|
194
379
|
}
|
|
195
380
|
let mergedConflict;
|
|
@@ -211,11 +396,9 @@ export class DefaultPolicyArbiter {
|
|
|
211
396
|
/**
|
|
212
397
|
* Resolve inherited policies by their UIDs from the Policy Administration Point.
|
|
213
398
|
* Policies can inherit from other policies via the inheritFrom property.
|
|
214
|
-
* Detects circular inheritance and throws an exception if detected.
|
|
215
399
|
* @param policy The policy that may have inheritFrom references.
|
|
216
400
|
* @param visitedPolicyIds Array of policy UIDs already visited in this inheritance chain.
|
|
217
401
|
* @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
402
|
* @internal
|
|
220
403
|
*/
|
|
221
404
|
async resolveInheritedPolicies(policy, visitedPolicyIds, currentDepth) {
|
|
@@ -258,15 +441,17 @@ export class DefaultPolicyArbiter {
|
|
|
258
441
|
return inheritedPolicies;
|
|
259
442
|
}
|
|
260
443
|
/**
|
|
261
|
-
* Apply policy-level assigner
|
|
444
|
+
* Apply policy-level default values (assigner, assignee, target, action) to rules that don't override them.
|
|
262
445
|
* @param policy The policy providing defaults.
|
|
263
446
|
* @param rule The rule to apply defaults to.
|
|
264
|
-
* @returns The rule with policy-level
|
|
447
|
+
* @returns The rule with policy-level defaults applied.
|
|
265
448
|
* @internal
|
|
266
449
|
*/
|
|
267
|
-
|
|
450
|
+
applyPolicyDefaultsToRule(policy, rule) {
|
|
268
451
|
const assigner = Is.empty(rule.assigner) ? policy.assigner : rule.assigner;
|
|
269
452
|
const assignee = Is.empty(rule.assignee) ? policy.assignee : rule.assignee;
|
|
453
|
+
const target = Is.empty(rule.target) ? policy.target : rule.target;
|
|
454
|
+
const action = Is.empty(rule.action) ? policy.action : rule.action;
|
|
270
455
|
const assignerIds = OdrlPolicyHelper.getPartyIds(assigner);
|
|
271
456
|
const ruleAssignerIds = OdrlPolicyHelper.getPartyIds(rule.assigner);
|
|
272
457
|
const assigneeIds = OdrlPolicyHelper.getPartyIds(assignee);
|
|
@@ -289,13 +474,17 @@ export class DefaultPolicyArbiter {
|
|
|
289
474
|
assigneeIds.length === ruleAssigneeIds.length &&
|
|
290
475
|
assigneeIds.every(id => ruleAssigneeIds.includes(id));
|
|
291
476
|
}
|
|
292
|
-
|
|
477
|
+
const targetEqual = target === rule.target;
|
|
478
|
+
const actionEqual = action === rule.action;
|
|
479
|
+
if (assignerEqual && assigneeEqual && targetEqual && actionEqual) {
|
|
293
480
|
return rule;
|
|
294
481
|
}
|
|
295
482
|
return {
|
|
296
483
|
...rule,
|
|
297
484
|
assigner,
|
|
298
|
-
assignee
|
|
485
|
+
assignee,
|
|
486
|
+
target,
|
|
487
|
+
action
|
|
299
488
|
};
|
|
300
489
|
}
|
|
301
490
|
/**
|
|
@@ -306,17 +495,74 @@ export class DefaultPolicyArbiter {
|
|
|
306
495
|
* @returns True if the rule is applicable to the agreement parties.
|
|
307
496
|
* @internal
|
|
308
497
|
*/
|
|
309
|
-
isRuleApplicableToParties(rule, agreementAssigner, agreementAssignee) {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
if (!this.isPartyApplicable(
|
|
498
|
+
isRuleApplicableToParties(rule, agreementAssigner, agreementAssignee, dataSources) {
|
|
499
|
+
const assignerContext = this.resolveRulePartyContext(rule.assigner);
|
|
500
|
+
const assigneeContext = this.resolveRulePartyContext(rule.assignee);
|
|
501
|
+
if (!this.isPartyApplicable(assignerContext.partyIds, agreementAssigner)) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
if (assignerContext.refinements.length > 0 &&
|
|
505
|
+
!assignerContext.refinements.every(c => this.evaluateConstraint(c, dataSources))) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
if (!this.isPartyApplicable(assigneeContext.partyIds, agreementAssignee)) {
|
|
313
509
|
return false;
|
|
314
510
|
}
|
|
315
|
-
if (
|
|
511
|
+
if (assigneeContext.refinements.length > 0 &&
|
|
512
|
+
!assigneeContext.refinements.every(c => this.evaluateConstraint(c, dataSources))) {
|
|
316
513
|
return false;
|
|
317
514
|
}
|
|
318
515
|
return true;
|
|
319
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Resolve rule party identifiers and refinements.
|
|
519
|
+
* PartyCollection source values are currently not supported for party matching.
|
|
520
|
+
* @param party The rule party value.
|
|
521
|
+
* @returns Resolved party identifiers and refinement constraints.
|
|
522
|
+
* @throws GeneralError if PartyCollection source has a value.
|
|
523
|
+
* @internal
|
|
524
|
+
*/
|
|
525
|
+
resolveRulePartyContext(party) {
|
|
526
|
+
const partyIds = [];
|
|
527
|
+
const refinements = [];
|
|
528
|
+
const parties = ArrayHelper.fromObjectOrArray(party ?? []);
|
|
529
|
+
for (const partyEntry of parties) {
|
|
530
|
+
if (Is.stringValue(partyEntry)) {
|
|
531
|
+
partyIds.push(partyEntry);
|
|
532
|
+
}
|
|
533
|
+
else if (Is.object(partyEntry)) {
|
|
534
|
+
// Guard against unsupported ODRL party properties
|
|
535
|
+
if (Is.notEmpty(partyEntry.assignerOf)) {
|
|
536
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyAssignerOfNotSupported");
|
|
537
|
+
}
|
|
538
|
+
if (Is.notEmpty(partyEntry.assigneeOf)) {
|
|
539
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyAssigneeOfNotSupported");
|
|
540
|
+
}
|
|
541
|
+
if (Is.notEmpty(partyEntry.partOf)) {
|
|
542
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyPartOfNotSupported");
|
|
543
|
+
}
|
|
544
|
+
if (OdrlPolicyHelper.getType(partyEntry) === OdrlTypes.PartyCollection) {
|
|
545
|
+
const partyCollectionEntry = partyEntry;
|
|
546
|
+
if (Is.stringValue(partyCollectionEntry.source)) {
|
|
547
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyCollectionSourceNotSupported", {
|
|
548
|
+
source: partyCollectionEntry.source ?? ""
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
refinements.push(...ArrayHelper.fromObjectOrArray(partyCollectionEntry.refinement ?? []));
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
const partyId = OdrlPolicyHelper.getUid(partyEntry);
|
|
555
|
+
if (Is.stringValue(partyId)) {
|
|
556
|
+
partyIds.push(partyId);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
partyIds,
|
|
563
|
+
refinements
|
|
564
|
+
};
|
|
565
|
+
}
|
|
320
566
|
/**
|
|
321
567
|
* Determine whether a rule party constraint applies to the agreement parties.
|
|
322
568
|
* Rule party is treated as a constraint: if specified, it must match at least one agreement party.
|
|
@@ -343,6 +589,7 @@ export class DefaultPolicyArbiter {
|
|
|
343
589
|
}
|
|
344
590
|
/**
|
|
345
591
|
* Determine whether a rule's action(s) match the requested action.
|
|
592
|
+
* Supports exact match, includedIn hierarchy, and implies relationships.
|
|
346
593
|
* If no action is specified in the rule, it applies to all actions (action-agnostic).
|
|
347
594
|
* If an action is specified in the rule and a specific action is requested, they must match.
|
|
348
595
|
* If an action is specified in the rule but no specific action is requested, the rule applies (general evaluation).
|
|
@@ -351,7 +598,7 @@ export class DefaultPolicyArbiter {
|
|
|
351
598
|
* @returns True if the rule's action(s) apply.
|
|
352
599
|
* @internal
|
|
353
600
|
*/
|
|
354
|
-
isActionApplicable(ruleActions, requestedAction) {
|
|
601
|
+
isActionApplicable(ruleActions, requestedAction, dataSources) {
|
|
355
602
|
// If the rule has no action specified, it applies to all actions
|
|
356
603
|
if (Is.empty(ruleActions)) {
|
|
357
604
|
return true;
|
|
@@ -361,72 +608,124 @@ export class DefaultPolicyArbiter {
|
|
|
361
608
|
if (Is.empty(requestedAction)) {
|
|
362
609
|
return true;
|
|
363
610
|
}
|
|
364
|
-
|
|
611
|
+
const requestedActionId = Is.string(requestedAction)
|
|
612
|
+
? requestedAction
|
|
613
|
+
: OdrlPolicyHelper.getUid(requestedAction);
|
|
614
|
+
if (!Is.stringValue(requestedActionId)) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
365
617
|
const ruleActionArray = ArrayHelper.fromObjectOrArray(ruleActions) ?? [];
|
|
366
|
-
// Check if the requested action matches any of the rule's actions
|
|
367
618
|
for (const ruleAction of ruleActionArray) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
? requestedAction
|
|
371
|
-
: OdrlPolicyHelper.getUid(requestedAction);
|
|
372
|
-
if (Is.stringValue(ruleActionId) && Is.stringValue(requestedActionId)) {
|
|
373
|
-
if (ruleActionId === requestedActionId) {
|
|
374
|
-
return true;
|
|
375
|
-
}
|
|
619
|
+
if (this.ruleActionCoversRequested(ruleAction, requestedActionId, dataSources)) {
|
|
620
|
+
return true;
|
|
376
621
|
}
|
|
377
622
|
}
|
|
378
623
|
return false;
|
|
379
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Determine whether a single rule action covers a requested action.
|
|
627
|
+
* Covers exact match plus ODRL action hierarchy semantics.
|
|
628
|
+
* - includedIn: the rule action is a sub-action of a broader parent.
|
|
629
|
+
* A rule naming the narrower action also covers requests for the parent.
|
|
630
|
+
* E.g. rule action "print" with includedIn "reproduce" covers a request for "reproduce".
|
|
631
|
+
* - implies: the rule action entails another action.
|
|
632
|
+
* A rule granting action X also covers action Y when X implies Y.
|
|
633
|
+
* E.g. rule action "distribute" implying "reproduce" covers a request for "reproduce".
|
|
634
|
+
* @param ruleAction The action specified in the rule.
|
|
635
|
+
* @param requestedActionId The requested action identifier.
|
|
636
|
+
* @returns True if the rule action covers the requested action.
|
|
637
|
+
* @internal
|
|
638
|
+
*/
|
|
639
|
+
ruleActionCoversRequested(ruleAction, requestedActionId, dataSources) {
|
|
640
|
+
// Extract the rule action ID — support both @id and rdf:value forms
|
|
641
|
+
let ruleActionId;
|
|
642
|
+
if (Is.string(ruleAction)) {
|
|
643
|
+
ruleActionId = ruleAction;
|
|
644
|
+
}
|
|
645
|
+
else if (Is.object(ruleAction)) {
|
|
646
|
+
ruleActionId = ruleAction["rdf:value"]?.["@id"] ?? OdrlPolicyHelper.getUid(ruleAction);
|
|
647
|
+
}
|
|
648
|
+
// Determine whether this rule action covers the requested action via any semantic path.
|
|
649
|
+
let covers = false;
|
|
650
|
+
if (Is.stringValue(ruleActionId) && ruleActionId === requestedActionId) {
|
|
651
|
+
// Exact match
|
|
652
|
+
covers = true;
|
|
653
|
+
}
|
|
654
|
+
else if (Is.object(ruleAction)) {
|
|
655
|
+
// includedIn: rule action A includedIn B means A is a sub-type of B.
|
|
656
|
+
// A rule that names the narrower action A with includedIn B also covers requests for B.
|
|
657
|
+
if (Is.stringValue(ruleAction.includedIn) && ruleAction.includedIn === requestedActionId) {
|
|
658
|
+
covers = true;
|
|
659
|
+
}
|
|
660
|
+
// implies: rule action A implies B means exercising A also entails B.
|
|
661
|
+
// A rule granting A therefore also grants each implied action.
|
|
662
|
+
if (!covers && (ruleAction.implies ?? []).includes(requestedActionId)) {
|
|
663
|
+
covers = true;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!covers) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
// If the action specifies refinements, all must be satisfied for the action to apply.
|
|
670
|
+
// Refinements constrain the manner in which the action is exercised (e.g. print count <= 5).
|
|
671
|
+
if (Is.object(ruleAction) && Is.notEmpty(ruleAction.refinement)) {
|
|
672
|
+
const refinements = ArrayHelper.fromObjectOrArray(ruleAction.refinement ?? []);
|
|
673
|
+
return refinements.every((refinement) => this.evaluateConstraint(refinement, dataSources));
|
|
674
|
+
}
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
380
677
|
/**
|
|
381
678
|
* Apply a permission and create decisions based on that information.
|
|
382
679
|
* @param agreementAssigner The assigner ID from the agreement.
|
|
383
680
|
* @param agreementAssignee The assignee ID from the agreement.
|
|
384
681
|
* @param policy The policy containing the permission.
|
|
385
682
|
* @param permission The permission to apply.
|
|
386
|
-
* @param
|
|
387
|
-
* @param
|
|
683
|
+
* @param targetRefinements Additional constraints from target refinement.
|
|
684
|
+
* @param dataSources The operand lookup sources.
|
|
388
685
|
* @returns True if the permission applies.
|
|
389
686
|
* @internal
|
|
390
687
|
*/
|
|
391
|
-
async evaluatePermission(agreementAssigner, agreementAssignee, policy, permission,
|
|
392
|
-
if (!this.isRuleApplicableToParties(permission, agreementAssigner, agreementAssignee)) {
|
|
688
|
+
async evaluatePermission(agreementAssigner, agreementAssignee, policy, permission, targetRefinements, dataSources, action, decisionTarget) {
|
|
689
|
+
if (!this.isRuleApplicableToParties(permission, agreementAssigner, agreementAssignee, dataSources)) {
|
|
393
690
|
return false;
|
|
394
691
|
}
|
|
395
692
|
// Check if the permission's action(s) match the requested action
|
|
396
|
-
if (!this.isActionApplicable(permission.action, action)) {
|
|
693
|
+
if (!this.isActionApplicable(permission.action, action, dataSources)) {
|
|
397
694
|
return false;
|
|
398
695
|
}
|
|
399
696
|
// ODRL semantics: a Permission without constraints is unconditional.
|
|
400
|
-
const constraints =
|
|
697
|
+
const constraints = [
|
|
698
|
+
...ArrayHelper.fromObjectOrArray(permission.constraint ?? []),
|
|
699
|
+
...targetRefinements
|
|
700
|
+
];
|
|
701
|
+
const permissionTargetLookup = this.tryResolveTargetDataSource(this.buildRuleDataContextTargetId(this.getRuleDataContextTargetId(permission.target), decisionTarget), dataSources, true);
|
|
702
|
+
const ruleDataContext = permissionTargetLookup.value;
|
|
401
703
|
if (constraints.length === 0) {
|
|
402
|
-
return this.enforcePermissionDuties(policy, permission,
|
|
704
|
+
return this.enforcePermissionDuties(policy, permission, dataSources, ruleDataContext);
|
|
403
705
|
}
|
|
404
|
-
// Resolve the data context for evaluating this permission.
|
|
405
|
-
// If the target starts with twin:information:<key>, the key references an entry in the information map.
|
|
406
|
-
const ruleDataContext = this.resolveRuleDataContext(permission, information, data);
|
|
407
706
|
// All constraints must be satisfied for the permission to apply.
|
|
408
|
-
const constraintsSatisfied = constraints.every(c => this.evaluateConstraint(c,
|
|
707
|
+
const constraintsSatisfied = constraints.every(c => this.evaluateConstraint(c, dataSources));
|
|
409
708
|
if (!constraintsSatisfied) {
|
|
410
709
|
return false;
|
|
411
710
|
}
|
|
412
|
-
return this.enforcePermissionDuties(policy, permission,
|
|
711
|
+
return this.enforcePermissionDuties(policy, permission, dataSources, ruleDataContext);
|
|
413
712
|
}
|
|
414
713
|
/**
|
|
415
714
|
* Enforce duties attached to a permission.
|
|
416
715
|
* @param policy The policy being evaluated.
|
|
417
716
|
* @param permission The permission being evaluated.
|
|
418
|
-
* @param
|
|
419
|
-
* @param
|
|
717
|
+
* @param dataSources The operand lookup sources.
|
|
718
|
+
* @param ruleDataContext The target-scoped data context passed to enforcers.
|
|
420
719
|
* @returns True if all duties are enforced or none are present.
|
|
421
720
|
* @internal
|
|
422
721
|
*/
|
|
423
|
-
async enforcePermissionDuties(policy, permission,
|
|
722
|
+
async enforcePermissionDuties(policy, permission, dataSources, ruleDataContext) {
|
|
424
723
|
const duties = ArrayHelper.fromObjectOrArray(permission.duty ?? []);
|
|
425
724
|
if (duties.length === 0) {
|
|
426
725
|
return true;
|
|
427
726
|
}
|
|
428
727
|
for (const duty of duties) {
|
|
429
|
-
const enforced = await this.enforceDuty(policy, duty,
|
|
728
|
+
const enforced = await this.enforceDuty(policy, duty, dataSources, ruleDataContext);
|
|
430
729
|
if (!enforced) {
|
|
431
730
|
return false;
|
|
432
731
|
}
|
|
@@ -437,46 +736,87 @@ export class DefaultPolicyArbiter {
|
|
|
437
736
|
* Enforce a single duty using registered obligation enforcers.
|
|
438
737
|
* @param policy The policy being evaluated.
|
|
439
738
|
* @param duty The duty to enforce.
|
|
440
|
-
* @param
|
|
441
|
-
* @param
|
|
739
|
+
* @param dataSources The operand lookup sources.
|
|
740
|
+
* @param ruleDataContext The target-scoped data context passed to enforcers.
|
|
442
741
|
* @returns True if any enforcer succeeds.
|
|
443
742
|
* @internal
|
|
444
743
|
*/
|
|
445
|
-
async enforceDuty(policy, duty,
|
|
744
|
+
async enforceDuty(policy, duty, dataSources, ruleDataContext) {
|
|
446
745
|
const enforcerNames = PolicyObligationEnforcerFactory.names();
|
|
746
|
+
const information = dataSources[`${DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS}${DefaultPolicyArbiter._TWIN_PREFIX_INFORMATION}`];
|
|
447
747
|
if (enforcerNames.length === 0) {
|
|
448
748
|
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "noObligationEnforcersRegistered");
|
|
449
749
|
}
|
|
450
750
|
for (const enforcerName of enforcerNames) {
|
|
451
751
|
const enforcer = PolicyObligationEnforcerFactory.get(enforcerName);
|
|
452
|
-
if (await enforcer.enforce(policy, duty, information,
|
|
752
|
+
if (await enforcer.enforce(policy, duty, information, ruleDataContext)) {
|
|
453
753
|
return true;
|
|
454
754
|
}
|
|
455
755
|
}
|
|
456
|
-
|
|
756
|
+
const consequences = ArrayHelper.fromObjectOrArray(duty.consequence ?? []);
|
|
757
|
+
if (consequences.length === 0) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
for (const consequence of consequences) {
|
|
761
|
+
if (!(await this.enforceDuty(policy, consequence, dataSources, ruleDataContext))) {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return true;
|
|
457
766
|
}
|
|
458
767
|
/**
|
|
459
|
-
* Resolve
|
|
460
|
-
*
|
|
461
|
-
* @param
|
|
462
|
-
* @param
|
|
463
|
-
* @
|
|
464
|
-
* @returns The data context to use for JSONPath resolution.
|
|
465
|
-
* @throws GeneralError When the information target key is missing.
|
|
768
|
+
* Resolve a target string to a matching datasource prefix and remaining target value.
|
|
769
|
+
* @param targetId The target identifier to resolve.
|
|
770
|
+
* @param dataSources The available lookup sources.
|
|
771
|
+
* @param resolveValue True to resolve an item from the target path/key.
|
|
772
|
+
* @returns The matching prefix, source, remaining target and optional resolved value.
|
|
466
773
|
* @internal
|
|
467
774
|
*/
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (targetId
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
775
|
+
tryResolveTargetDataSource(targetId, dataSources, resolveValue = false) {
|
|
776
|
+
// If there is no target id, default to the entire "twin:jsonpath" datasource
|
|
777
|
+
if (Is.empty(targetId)) {
|
|
778
|
+
return {
|
|
779
|
+
prefix: `${DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS}${DefaultPolicyArbiter._TWIN_PREFIX_JSONPATH}`,
|
|
780
|
+
source: dataSources[`${DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS}${DefaultPolicyArbiter._TWIN_PREFIX_JSONPATH}`],
|
|
781
|
+
target: "$",
|
|
782
|
+
value: dataSources[`${DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS}${DefaultPolicyArbiter._TWIN_PREFIX_JSONPATH}`]
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
// Otherwise lookup the target id prefix in the datasources and return the remaining suffix as the target path/key
|
|
786
|
+
const prefixes = Object.keys(dataSources).sort((a, b) => b.length - a.length);
|
|
787
|
+
for (const prefix of prefixes) {
|
|
788
|
+
if (targetId.startsWith(`${prefix}:`)) {
|
|
789
|
+
const source = dataSources[prefix];
|
|
790
|
+
const target = targetId.slice(prefix.length + 1);
|
|
791
|
+
if (!target.startsWith("$")) {
|
|
792
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
|
|
793
|
+
target: targetId
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
if (!resolveValue) {
|
|
797
|
+
return {
|
|
798
|
+
prefix,
|
|
799
|
+
source,
|
|
800
|
+
target
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const matches = JsonPathHelper.query(target, source);
|
|
804
|
+
if (matches.length === 0) {
|
|
805
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
|
|
806
|
+
target: targetId
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
prefix,
|
|
811
|
+
source,
|
|
812
|
+
target,
|
|
813
|
+
value: matches.length === 1 ? matches[0].value : matches.map(m => m.value)
|
|
814
|
+
};
|
|
476
815
|
}
|
|
477
|
-
return information[key];
|
|
478
816
|
}
|
|
479
|
-
|
|
817
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
|
|
818
|
+
target: targetId
|
|
819
|
+
});
|
|
480
820
|
}
|
|
481
821
|
/**
|
|
482
822
|
* Extract the target id from the permission.
|
|
@@ -497,24 +837,267 @@ export class DefaultPolicyArbiter {
|
|
|
497
837
|
}
|
|
498
838
|
return Is.string(arr[0]) ? arr[0] : OdrlPolicyHelper.getUid(arr[0]);
|
|
499
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Resolve the target identifier used for rule data context lookup.
|
|
842
|
+
* For AssetCollection targets the `source` property is used because the collection has no `uid`.
|
|
843
|
+
* Falls back to `getTargetId` for all other target forms.
|
|
844
|
+
* @param target The rule target field value.
|
|
845
|
+
* @returns The prefixed target string for data context resolution.
|
|
846
|
+
* @internal
|
|
847
|
+
*/
|
|
848
|
+
getRuleDataContextTargetId(target) {
|
|
849
|
+
const arr = ArrayHelper.fromObjectOrArray(target ?? []);
|
|
850
|
+
if (arr.length === 1) {
|
|
851
|
+
const firstTarget = arr[0];
|
|
852
|
+
if (Is.object(firstTarget) &&
|
|
853
|
+
OdrlPolicyHelper.getType(firstTarget) === OdrlTypes.AssetCollection &&
|
|
854
|
+
Is.stringValue(firstTarget.source)) {
|
|
855
|
+
return firstTarget.source;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return this.getTargetId(target);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Build a concrete prefixed target id for rule data-context lookup.
|
|
862
|
+
* @param baseTargetId The original prefixed target id from the rule.
|
|
863
|
+
* @param decisionTarget The concrete decision JSONPath target.
|
|
864
|
+
* @returns The concrete prefixed target id.
|
|
865
|
+
* @internal
|
|
866
|
+
*/
|
|
867
|
+
buildRuleDataContextTargetId(baseTargetId, decisionTarget) {
|
|
868
|
+
if (!Is.stringValue(decisionTarget) || decisionTarget === "$") {
|
|
869
|
+
return baseTargetId;
|
|
870
|
+
}
|
|
871
|
+
if (!Is.stringValue(baseTargetId)) {
|
|
872
|
+
return undefined;
|
|
873
|
+
}
|
|
874
|
+
const pathStartIndex = baseTargetId.indexOf(":$");
|
|
875
|
+
if (pathStartIndex < 0) {
|
|
876
|
+
return baseTargetId;
|
|
877
|
+
}
|
|
878
|
+
return `${baseTargetId.slice(0, pathStartIndex)}:${decisionTarget}`;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Resolve a rule target into a policy-decision JSONPath target and extracted refinements.
|
|
882
|
+
* For AssetCollection targets, `source` is treated as the decision target and `refinement`
|
|
883
|
+
* constraints are applied as additional rule constraints.
|
|
884
|
+
* @param rule The rule to resolve the target for.
|
|
885
|
+
* @returns The decision target and target refinements.
|
|
886
|
+
* @throws GeneralError if target is invalid or unsupported.
|
|
887
|
+
* @internal
|
|
888
|
+
*/
|
|
889
|
+
resolveRuleTarget(rule, dataSources) {
|
|
890
|
+
const arr = ArrayHelper.fromObjectOrArray(rule.target ?? []);
|
|
891
|
+
if (arr.length === 0) {
|
|
892
|
+
return {
|
|
893
|
+
target: "$",
|
|
894
|
+
refinements: []
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (arr.length > 1) {
|
|
898
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "multipleTargetsNotSupported");
|
|
899
|
+
}
|
|
900
|
+
const firstTarget = arr[0];
|
|
901
|
+
if (Is.object(firstTarget)) {
|
|
902
|
+
// Guard against unsupported ODRL asset properties
|
|
903
|
+
if (Is.notEmpty(firstTarget.hasPolicy)) {
|
|
904
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetHasPolicyNotSupported");
|
|
905
|
+
}
|
|
906
|
+
if (Is.notEmpty(firstTarget.partOf)) {
|
|
907
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetPartOfNotSupported");
|
|
908
|
+
}
|
|
909
|
+
if (Is.object(firstTarget) &&
|
|
910
|
+
OdrlPolicyHelper.getType(firstTarget) === OdrlTypes.AssetCollection) {
|
|
911
|
+
if (!Is.stringValue(firstTarget.source)) {
|
|
912
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetCollectionSourceNotSupported", {
|
|
913
|
+
source: firstTarget.source ?? ""
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
let sourceLookup;
|
|
917
|
+
try {
|
|
918
|
+
sourceLookup = this.tryResolveTargetDataSource(firstTarget.source, dataSources);
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetCollectionSourceNotSupported", {
|
|
922
|
+
source: firstTarget.source
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
target: sourceLookup.target,
|
|
927
|
+
refinements: ArrayHelper.fromObjectOrArray(firstTarget.refinement ?? [])
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const targetId = Is.string(firstTarget) ? firstTarget : OdrlPolicyHelper.getUid(firstTarget);
|
|
932
|
+
if (!Is.stringValue(targetId)) {
|
|
933
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
|
|
934
|
+
target: ""
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const targetLookup = this.tryResolveTargetDataSource(targetId, dataSources);
|
|
938
|
+
return {
|
|
939
|
+
target: targetLookup.target,
|
|
940
|
+
refinements: []
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Resolve decision targets for a rule.
|
|
945
|
+
* AssetCollection wildcard targets with refinements are expanded to per-item targets.
|
|
946
|
+
* @param rule The rule being evaluated.
|
|
947
|
+
* @param dataSources The operand lookup sources.
|
|
948
|
+
* @returns The decision targets and scoped refinements.
|
|
949
|
+
* @internal
|
|
950
|
+
*/
|
|
951
|
+
resolveRuleDecisionTargets(rule, dataSources) {
|
|
952
|
+
const resolvedTarget = this.resolveRuleTarget(rule, dataSources);
|
|
953
|
+
if (!this.shouldExpandToPerItemTargets(rule, resolvedTarget)) {
|
|
954
|
+
return [resolvedTarget];
|
|
955
|
+
}
|
|
956
|
+
const sourceLookup = this.tryResolveTargetDataSource(this.getRuleDataContextTargetId(rule.target), dataSources);
|
|
957
|
+
const matches = JsonPathHelper.query(sourceLookup.target, sourceLookup.source);
|
|
958
|
+
if (matches.length === 0) {
|
|
959
|
+
return [resolvedTarget];
|
|
960
|
+
}
|
|
961
|
+
return matches.map(match => {
|
|
962
|
+
const itemTarget = this.normalizeDecisionTargetPath(match.path ?? resolvedTarget.target);
|
|
963
|
+
return {
|
|
964
|
+
target: itemTarget,
|
|
965
|
+
refinements: resolvedTarget.refinements.map(refinement => this.rewriteRefinementForDecisionTarget(refinement, resolvedTarget.target, itemTarget))
|
|
966
|
+
};
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Determine if a rule should be expanded to per-item targets.
|
|
971
|
+
* @param rule The rule.
|
|
972
|
+
* @param resolvedTarget The resolved target details.
|
|
973
|
+
* @returns True if the rule should emit per-item decisions.
|
|
974
|
+
* @internal
|
|
975
|
+
*/
|
|
976
|
+
shouldExpandToPerItemTargets(rule, resolvedTarget) {
|
|
977
|
+
if (resolvedTarget.refinements.length === 0 || !resolvedTarget.target.includes("[*]")) {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
const targets = ArrayHelper.fromObjectOrArray(rule.target ?? []);
|
|
981
|
+
if (targets.length !== 1 || !Is.object(targets[0])) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
return OdrlPolicyHelper.getType(targets[0]) === OdrlTypes.AssetCollection;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Rewrite a refinement so wildcard paths are scoped to a concrete item target.
|
|
988
|
+
* @param refinement The refinement to rewrite.
|
|
989
|
+
* @param sourceTarget The wildcard source target.
|
|
990
|
+
* @param itemTarget The concrete item target.
|
|
991
|
+
* @returns The rewritten refinement.
|
|
992
|
+
* @internal
|
|
993
|
+
*/
|
|
994
|
+
rewriteRefinementForDecisionTarget(refinement, sourceTarget, itemTarget) {
|
|
995
|
+
const logicalConstraint = this.getLogicalConstraintOperands(refinement);
|
|
996
|
+
if (logicalConstraint) {
|
|
997
|
+
return {
|
|
998
|
+
...refinement,
|
|
999
|
+
[logicalConstraint.operator]: logicalConstraint.constraints.map(item => this.rewriteRefinementForDecisionTarget(item, sourceTarget, itemTarget))
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
const regularConstraint = refinement;
|
|
1003
|
+
return {
|
|
1004
|
+
...regularConstraint,
|
|
1005
|
+
leftOperand: this.rewriteOperandForDecisionTarget(regularConstraint.leftOperand, sourceTarget, itemTarget),
|
|
1006
|
+
rightOperand: this.rewriteOperandForDecisionTarget(regularConstraint.rightOperand, sourceTarget, itemTarget)
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Rewrite JSONPath-based operands from wildcard source to concrete item target.
|
|
1011
|
+
* @param operand The operand to rewrite.
|
|
1012
|
+
* @param sourceTarget The wildcard source target.
|
|
1013
|
+
* @param itemTarget The concrete item target.
|
|
1014
|
+
* @returns The rewritten operand.
|
|
1015
|
+
* @internal
|
|
1016
|
+
*/
|
|
1017
|
+
rewriteOperandForDecisionTarget(operand, sourceTarget, itemTarget) {
|
|
1018
|
+
if (Is.stringValue(operand)) {
|
|
1019
|
+
const prefix = `${DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS}${DefaultPolicyArbiter._TWIN_PREFIX_JSONPATH}:`;
|
|
1020
|
+
if (operand.startsWith(prefix)) {
|
|
1021
|
+
const valuePath = operand.slice(prefix.length);
|
|
1022
|
+
return `${prefix}${this.rewriteWildcardPath(valuePath, sourceTarget, itemTarget)}`;
|
|
1023
|
+
}
|
|
1024
|
+
return operand;
|
|
1025
|
+
}
|
|
1026
|
+
if (Is.object(operand)) {
|
|
1027
|
+
const typedOperand = { ...operand };
|
|
1028
|
+
if (typedOperand["@type"] ===
|
|
1029
|
+
`${DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS}${DefaultPolicyArbiter._TWIN_PREFIX_JSONPATH}` &&
|
|
1030
|
+
Is.stringValue(typedOperand["@value"])) {
|
|
1031
|
+
typedOperand["@value"] = this.rewriteWildcardPath(typedOperand["@value"], sourceTarget, itemTarget);
|
|
1032
|
+
}
|
|
1033
|
+
return typedOperand;
|
|
1034
|
+
}
|
|
1035
|
+
return operand;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Rewrite wildcard source JSONPath segments to a concrete item JSONPath.
|
|
1039
|
+
* @param valuePath The operand path.
|
|
1040
|
+
* @param sourceTarget The wildcard source path.
|
|
1041
|
+
* @param itemTarget The concrete item path.
|
|
1042
|
+
* @returns The rewritten path.
|
|
1043
|
+
* @internal
|
|
1044
|
+
*/
|
|
1045
|
+
rewriteWildcardPath(valuePath, sourceTarget, itemTarget) {
|
|
1046
|
+
if (!sourceTarget.includes("[*]") || !valuePath.includes("[*]")) {
|
|
1047
|
+
return valuePath;
|
|
1048
|
+
}
|
|
1049
|
+
if (valuePath.startsWith(sourceTarget)) {
|
|
1050
|
+
return `${itemTarget}${valuePath.slice(sourceTarget.length)}`;
|
|
1051
|
+
}
|
|
1052
|
+
return valuePath;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Normalize JSONPath strings to dot notation for stable decision targets.
|
|
1056
|
+
* @param path The JSONPath to normalize.
|
|
1057
|
+
* @returns The normalized path.
|
|
1058
|
+
* @internal
|
|
1059
|
+
*/
|
|
1060
|
+
normalizeDecisionTargetPath(path) {
|
|
1061
|
+
return path.replace(/\['([^']+)']/g, ".$1");
|
|
1062
|
+
}
|
|
500
1063
|
/**
|
|
501
1064
|
* Evaluate a single ODRL constraint against the available context.
|
|
502
1065
|
* Supports logical constraint composition through nested refinements.
|
|
503
1066
|
* @param constraint The constraint to evaluate.
|
|
504
|
-
* @param
|
|
1067
|
+
* @param dataSources The operand lookup sources.
|
|
505
1068
|
* @returns True if the constraint is satisfied.
|
|
506
1069
|
* @internal
|
|
507
1070
|
*/
|
|
508
|
-
evaluateConstraint(constraint,
|
|
1071
|
+
evaluateConstraint(constraint, dataSources) {
|
|
509
1072
|
const logicalConstraint = this.getLogicalConstraintOperands(constraint);
|
|
510
1073
|
if (logicalConstraint) {
|
|
511
|
-
return this.evaluateLogicalConstraint(logicalConstraint,
|
|
1074
|
+
return this.evaluateLogicalConstraint(logicalConstraint, dataSources);
|
|
512
1075
|
}
|
|
513
1076
|
// Must be a regular constraint beyond this point
|
|
514
1077
|
const regularConstraint = constraint;
|
|
1078
|
+
// rightOperandReference is not supported — it requires an external IRI lookup that
|
|
1079
|
+
// is outside the scope of the local evaluation engine.
|
|
1080
|
+
if (Is.notEmpty(regularConstraint.rightOperandReference)) {
|
|
1081
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "rightOperandReferenceNotSupported");
|
|
1082
|
+
}
|
|
1083
|
+
// dataType specifies how the rightOperand value should be coerced before comparison.
|
|
1084
|
+
// Without dataType-aware coercion logic the comparison may produce incorrect results.
|
|
1085
|
+
if (Is.notEmpty(regularConstraint.dataType)) {
|
|
1086
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintDataTypeNotSupported");
|
|
1087
|
+
}
|
|
1088
|
+
// unit specifies the measurement unit for the right operand (e.g. currency, length).
|
|
1089
|
+
// Unit-aware comparison is not implemented.
|
|
1090
|
+
if (Is.notEmpty(regularConstraint.unit)) {
|
|
1091
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintUnitNotSupported");
|
|
1092
|
+
}
|
|
1093
|
+
// status represents a state-based evaluation operand (e.g. odrl:policyUsage).
|
|
1094
|
+
// State-based evaluation is not implemented.
|
|
1095
|
+
if (Is.notEmpty(regularConstraint.status)) {
|
|
1096
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintStatusNotSupported");
|
|
1097
|
+
}
|
|
515
1098
|
// Evaluate the main constraint condition
|
|
516
|
-
const leftValue = this.calculateOperandValue(regularConstraint.leftOperand,
|
|
517
|
-
const rightValue = this.calculateOperandValue(regularConstraint.rightOperand,
|
|
1099
|
+
const leftValue = this.calculateOperandValue(regularConstraint.leftOperand, dataSources);
|
|
1100
|
+
const rightValue = this.calculateOperandValue(regularConstraint.rightOperand, dataSources);
|
|
518
1101
|
const mainSatisfied = this.evaluateOperator(regularConstraint.operator, leftValue, rightValue);
|
|
519
1102
|
// If main constraint is not satisfied, the overall constraint fails
|
|
520
1103
|
return mainSatisfied;
|
|
@@ -589,32 +1172,32 @@ export class DefaultPolicyArbiter {
|
|
|
589
1172
|
/**
|
|
590
1173
|
* Evaluate a logical constraint operator against its operands.
|
|
591
1174
|
* @param logicalConstraint The operator and operand list.
|
|
592
|
-
* @param
|
|
1175
|
+
* @param dataSources The operand lookup sources.
|
|
593
1176
|
* @returns True if the logical constraint is satisfied.
|
|
594
1177
|
* @internal
|
|
595
1178
|
*/
|
|
596
|
-
evaluateLogicalConstraint(logicalConstraint,
|
|
1179
|
+
evaluateLogicalConstraint(logicalConstraint, dataSources) {
|
|
597
1180
|
const { operator, constraints } = logicalConstraint;
|
|
598
1181
|
if (constraints.length === 0) {
|
|
599
1182
|
return false;
|
|
600
1183
|
}
|
|
601
1184
|
switch (operator) {
|
|
602
1185
|
case OdrlLogicalConstraintType.And:
|
|
603
|
-
return constraints.every(item => this.evaluateConstraint(item,
|
|
1186
|
+
return constraints.every(item => this.evaluateConstraint(item, dataSources));
|
|
604
1187
|
case OdrlLogicalConstraintType.AndSequence: {
|
|
605
1188
|
for (const item of constraints) {
|
|
606
|
-
if (!this.evaluateConstraint(item,
|
|
1189
|
+
if (!this.evaluateConstraint(item, dataSources)) {
|
|
607
1190
|
return false;
|
|
608
1191
|
}
|
|
609
1192
|
}
|
|
610
1193
|
return true;
|
|
611
1194
|
}
|
|
612
1195
|
case OdrlLogicalConstraintType.Or:
|
|
613
|
-
return constraints.some(item => this.evaluateConstraint(item,
|
|
1196
|
+
return constraints.some(item => this.evaluateConstraint(item, dataSources));
|
|
614
1197
|
case OdrlLogicalConstraintType.Xone: {
|
|
615
1198
|
let satisfied = 0;
|
|
616
1199
|
for (const item of constraints) {
|
|
617
|
-
if (this.evaluateConstraint(item,
|
|
1200
|
+
if (this.evaluateConstraint(item, dataSources)) {
|
|
618
1201
|
satisfied += 1;
|
|
619
1202
|
if (satisfied > 1) {
|
|
620
1203
|
return false;
|
|
@@ -627,25 +1210,57 @@ export class DefaultPolicyArbiter {
|
|
|
627
1210
|
return false;
|
|
628
1211
|
}
|
|
629
1212
|
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Resolve a prefixed operand to its source object and JSONPath expression.
|
|
1215
|
+
* Prefix matching is dictionary-driven so additional operand namespaces can be
|
|
1216
|
+
* added in one place by extending the lookup sources map.
|
|
1217
|
+
* @param operandTypeOrValue The string operand value or typed operand namespace.
|
|
1218
|
+
* @param operandValue The JSONPath expression.
|
|
1219
|
+
* @param dataSources The available lookup sources.
|
|
1220
|
+
* @returns The resolved source and JSONPath, or undefined when not namespaced.
|
|
1221
|
+
* @throws GeneralError if a twin: prefixed operand doesn't resolve to any available datasource key.
|
|
1222
|
+
* @internal
|
|
1223
|
+
*/
|
|
1224
|
+
tryResolveOperandLookup(operandTypeOrValue, operandValue, dataSources) {
|
|
1225
|
+
let lookupTargetId;
|
|
1226
|
+
if (dataSources[operandTypeOrValue]) {
|
|
1227
|
+
if (!Is.stringValue(operandValue)) {
|
|
1228
|
+
return undefined;
|
|
1229
|
+
}
|
|
1230
|
+
lookupTargetId = `${operandTypeOrValue}:${operandValue}`;
|
|
1231
|
+
}
|
|
1232
|
+
else if (operandTypeOrValue.includes(":")) {
|
|
1233
|
+
lookupTargetId = operandTypeOrValue;
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
return undefined;
|
|
1237
|
+
}
|
|
1238
|
+
// Delegate prefixed path matching to shared datasource resolver
|
|
1239
|
+
if (operandTypeOrValue.startsWith(DefaultPolicyArbiter._TWIN_PREFIX_OPERATIONS)) {
|
|
1240
|
+
const resolved = this.tryResolveTargetDataSource(lookupTargetId, dataSources);
|
|
1241
|
+
return {
|
|
1242
|
+
source: resolved.source,
|
|
1243
|
+
jsonPath: resolved.target
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
630
1247
|
/**
|
|
631
1248
|
* Calculate an operand value.
|
|
632
1249
|
* @param operand The operand.
|
|
633
|
-
* @param
|
|
1250
|
+
* @param dataSources The available prefixed operand sources.
|
|
634
1251
|
* @returns The resolved operand value.
|
|
635
1252
|
* @internal
|
|
636
1253
|
*/
|
|
637
|
-
calculateOperandValue(operand,
|
|
638
|
-
// Treat
|
|
639
|
-
//
|
|
1254
|
+
calculateOperandValue(operand, dataSources) {
|
|
1255
|
+
// Treat prefixed operands as selectors against a namespaced source dictionary.
|
|
1256
|
+
// Examples: twin:jsonpath:$.field, twin:information:$.credentials.level
|
|
640
1257
|
let jsonPath;
|
|
1258
|
+
let operandRoot;
|
|
641
1259
|
if (Is.stringValue(operand)) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
operand
|
|
647
|
-
});
|
|
648
|
-
}
|
|
1260
|
+
const lookup = this.tryResolveOperandLookup(operand, operand, dataSources);
|
|
1261
|
+
if (lookup) {
|
|
1262
|
+
jsonPath = lookup.jsonPath;
|
|
1263
|
+
operandRoot = lookup.source;
|
|
649
1264
|
}
|
|
650
1265
|
}
|
|
651
1266
|
else if (Is.object(operand)) {
|
|
@@ -653,12 +1268,10 @@ export class DefaultPolicyArbiter {
|
|
|
653
1268
|
const value = operand["@value"];
|
|
654
1269
|
const type = operand["@type"];
|
|
655
1270
|
if (Is.stringValue(type)) {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
jsonPath = value;
|
|
661
|
-
}
|
|
1271
|
+
const lookup = this.tryResolveOperandLookup(type, value, dataSources);
|
|
1272
|
+
if (lookup) {
|
|
1273
|
+
jsonPath = lookup.jsonPath;
|
|
1274
|
+
operandRoot = lookup.source;
|
|
662
1275
|
}
|
|
663
1276
|
else {
|
|
664
1277
|
const xsdValue = this.coerceXsdType(value, type);
|
|
@@ -670,7 +1283,7 @@ export class DefaultPolicyArbiter {
|
|
|
670
1283
|
}
|
|
671
1284
|
// We have a JSON Path to resolve
|
|
672
1285
|
if (Is.stringValue(jsonPath)) {
|
|
673
|
-
const jsonPaths = JsonPathHelper.query(jsonPath,
|
|
1286
|
+
const jsonPaths = JsonPathHelper.query(jsonPath, operandRoot);
|
|
674
1287
|
if (jsonPaths.length === 0) {
|
|
675
1288
|
// No matches
|
|
676
1289
|
return undefined;
|