@twin.org/rights-management-plugins 0.0.3-next.4 → 0.0.3-next.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/es/index.js +18 -10
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IAutomationPolicyExecutionActionConfig.js +4 -0
- package/dist/es/models/IAutomationPolicyExecutionActionConfig.js.map +1 -0
- package/dist/es/models/IAutomationPolicyExecutionActionConstructorOptions.js +2 -0
- package/dist/es/models/IAutomationPolicyExecutionActionConstructorOptions.js.map +1 -0
- package/dist/es/models/{IExamplePolicyArbiterConstructorOptions.js → IDefaultPolicyArbiterConfig.js} +1 -1
- package/dist/es/models/IDefaultPolicyArbiterConfig.js.map +1 -0
- package/dist/es/models/{IExamplePolicyRequesterConstructorOptions.js → IDefaultPolicyArbiterConstructorOptions.js} +1 -1
- package/dist/es/models/IDefaultPolicyArbiterConstructorOptions.js.map +1 -0
- package/dist/es/models/{IExamplePolicyEnforcementProcessorConstructorOptions.js → IDefaultPolicyEnforcementProcessorConstructorOptions.js} +1 -1
- package/dist/es/models/IDefaultPolicyEnforcementProcessorConstructorOptions.js.map +1 -0
- package/dist/es/models/ILoggingPolicyExecutionActionConfig.js.map +1 -1
- package/dist/es/models/{IExampleDataAccessHandlerConstructorOptions.js → IPassThroughPolicyArbiterConstructorOptions.js} +1 -1
- package/dist/es/models/IPassThroughPolicyArbiterConstructorOptions.js.map +1 -0
- package/dist/es/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.js +4 -0
- package/dist/es/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.js.map +1 -0
- package/dist/es/models/IPassThroughPolicyNegotiatorConstructorOptions.js +4 -0
- package/dist/es/models/IPassThroughPolicyNegotiatorConstructorOptions.js.map +1 -0
- package/dist/es/models/IPassThroughPolicyObligationEnforcerConstructorOptions.js +4 -0
- package/dist/es/models/IPassThroughPolicyObligationEnforcerConstructorOptions.js.map +1 -0
- package/dist/es/models/{IExamplePolicyNegotiatorConstructorOptions.js → IPassThroughPolicyRequesterConstructorOptions.js} +1 -1
- package/dist/es/models/IPassThroughPolicyRequesterConstructorOptions.js.map +1 -0
- package/dist/es/models/IStaticPolicyInformationSource.js.map +1 -1
- package/dist/es/policyArbiters/defaultPolicyArbiter.js +1657 -0
- package/dist/es/policyArbiters/defaultPolicyArbiter.js.map +1 -0
- package/dist/es/policyArbiters/passThroughPolicyArbiter.js +59 -0
- package/dist/es/policyArbiters/passThroughPolicyArbiter.js.map +1 -0
- package/dist/es/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.js +146 -0
- package/dist/es/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.js.map +1 -0
- package/dist/es/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.js +54 -0
- package/dist/es/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.js.map +1 -0
- package/dist/es/policyExecutionActions/automationPolicyExecutionAction.js +76 -0
- package/dist/es/policyExecutionActions/automationPolicyExecutionAction.js.map +1 -0
- package/dist/es/policyExecutionActions/loggingPolicyExecutionAction.js +16 -15
- package/dist/es/policyExecutionActions/loggingPolicyExecutionAction.js.map +1 -1
- package/dist/es/policyInformationSources/identityPolicyInformationSource.js +43 -31
- package/dist/es/policyInformationSources/identityPolicyInformationSource.js.map +1 -1
- package/dist/es/policyInformationSources/staticPolicyInformationSource.js +17 -15
- package/dist/es/policyInformationSources/staticPolicyInformationSource.js.map +1 -1
- package/dist/es/policyNegotiators/passThroughPolicyNegotiator.js +88 -0
- package/dist/es/policyNegotiators/passThroughPolicyNegotiator.js.map +1 -0
- package/dist/es/policyObligationEnforcers/passThroughPolicyObligationEnforcer.js +56 -0
- package/dist/es/policyObligationEnforcers/passThroughPolicyObligationEnforcer.js.map +1 -0
- package/dist/es/policyRequesters/passThroughPolicyRequester.js +103 -0
- package/dist/es/policyRequesters/passThroughPolicyRequester.js.map +1 -0
- package/dist/types/index.d.ts +18 -10
- package/dist/types/models/IAutomationPolicyExecutionActionConfig.d.ts +9 -0
- package/dist/types/models/IAutomationPolicyExecutionActionConstructorOptions.d.ts +15 -0
- package/dist/types/models/IDefaultPolicyArbiterConfig.d.ts +10 -0
- package/dist/types/models/IDefaultPolicyArbiterConstructorOptions.d.ts +20 -0
- package/dist/types/models/{IExamplePolicyEnforcementProcessorConstructorOptions.d.ts → IDefaultPolicyEnforcementProcessorConstructorOptions.d.ts} +2 -2
- package/dist/types/models/ILoggingPolicyExecutionActionConfig.d.ts +2 -2
- package/dist/types/models/{IExamplePolicyArbiterConstructorOptions.d.ts → IPassThroughPolicyArbiterConstructorOptions.d.ts} +2 -2
- package/dist/types/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.d.ts +10 -0
- package/dist/types/models/{IExamplePolicyNegotiatorConstructorOptions.d.ts → IPassThroughPolicyNegotiatorConstructorOptions.d.ts} +2 -2
- package/dist/types/models/IPassThroughPolicyObligationEnforcerConstructorOptions.d.ts +10 -0
- package/dist/types/models/{IExamplePolicyRequesterConstructorOptions.d.ts → IPassThroughPolicyRequesterConstructorOptions.d.ts} +2 -2
- package/dist/types/models/IStaticPolicyInformationSource.d.ts +10 -4
- package/dist/types/policyArbiters/defaultPolicyArbiter.d.ts +41 -0
- package/dist/types/policyArbiters/passThroughPolicyArbiter.d.ts +35 -0
- package/dist/types/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.d.ts +40 -0
- package/dist/types/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.d.ts +32 -0
- package/dist/types/policyExecutionActions/automationPolicyExecutionAction.d.ts +49 -0
- package/dist/types/policyExecutionActions/loggingPolicyExecutionAction.d.ts +8 -8
- package/dist/types/policyInformationSources/identityPolicyInformationSource.d.ts +8 -6
- package/dist/types/policyInformationSources/staticPolicyInformationSource.d.ts +7 -5
- package/dist/types/policyNegotiators/passThroughPolicyNegotiator.d.ts +52 -0
- package/dist/types/policyObligationEnforcers/passThroughPolicyObligationEnforcer.d.ts +35 -0
- package/dist/types/policyRequesters/{examplePolicyRequester.d.ts → passThroughPolicyRequester.d.ts} +15 -20
- package/docs/changelog.md +813 -3
- package/docs/examples.md +295 -1
- package/docs/reference/classes/AutomationPolicyExecutionAction.md +143 -0
- package/docs/reference/classes/DefaultPolicyArbiter.md +111 -0
- package/docs/reference/classes/DefaultPolicyEnforcementProcessor.md +107 -0
- package/docs/reference/classes/IdentityPolicyInformationSource.md +14 -14
- package/docs/reference/classes/LoggingPolicyExecutionAction.md +20 -20
- package/docs/reference/classes/PassThroughPolicyArbiter.md +101 -0
- package/docs/reference/classes/PassThroughPolicyEnforcementProcessor.md +107 -0
- package/docs/reference/classes/{ExamplePolicyNegotiator.md → PassThroughPolicyNegotiator.md} +24 -22
- package/docs/reference/classes/PassThroughPolicyObligationEnforcer.md +107 -0
- package/docs/reference/classes/{ExamplePolicyRequester.md → PassThroughPolicyRequester.md} +20 -38
- package/docs/reference/classes/StaticPolicyInformationSource.md +15 -15
- package/docs/reference/index.md +18 -10
- package/docs/reference/interfaces/IAutomationPolicyExecutionActionConfig.md +11 -0
- package/docs/reference/interfaces/IAutomationPolicyExecutionActionConstructorOptions.md +25 -0
- package/docs/reference/interfaces/IDefaultPolicyArbiterConfig.md +17 -0
- package/docs/reference/interfaces/IDefaultPolicyArbiterConstructorOptions.md +39 -0
- package/docs/reference/interfaces/IDefaultPolicyEnforcementProcessorConstructorOptions.md +17 -0
- package/docs/reference/interfaces/IIdentityPolicyInformationSourceConstructorOptions.md +4 -4
- package/docs/reference/interfaces/ILoggingPolicyExecutionActionConfig.md +9 -9
- package/docs/reference/interfaces/ILoggingPolicyExecutionActionConstructorOptions.md +4 -4
- package/docs/reference/interfaces/IPassThroughPolicyArbiterConstructorOptions.md +17 -0
- package/docs/reference/interfaces/IPassThroughPolicyEnforcementProcessorConstructorOptions.md +17 -0
- package/docs/reference/interfaces/IPassThroughPolicyNegotiatorConstructorOptions.md +17 -0
- package/docs/reference/interfaces/IPassThroughPolicyObligationEnforcerConstructorOptions.md +17 -0
- package/docs/reference/interfaces/IPassThroughPolicyRequesterConstructorOptions.md +17 -0
- package/docs/reference/interfaces/IStaticPolicyInformationSource.md +25 -6
- package/docs/reference/interfaces/IStaticPolicyInformationSourceConfig.md +2 -2
- package/docs/reference/interfaces/IStaticPolicyInformationSourceConstructorOptions.md +4 -4
- package/locales/en.json +58 -6
- package/package.json +7 -5
- package/dist/es/dataAccessHandlers/examplePolicyRequester.js +0 -82
- package/dist/es/dataAccessHandlers/examplePolicyRequester.js.map +0 -1
- package/dist/es/models/IExampleDataAccessHandlerConstructorOptions.js.map +0 -1
- package/dist/es/models/IExamplePolicyArbiterConstructorOptions.js.map +0 -1
- package/dist/es/models/IExamplePolicyEnforcementProcessorConstructorOptions.js.map +0 -1
- package/dist/es/models/IExamplePolicyNegotiatorConstructorOptions.js.map +0 -1
- package/dist/es/models/IExamplePolicyRequesterConstructorOptions.js.map +0 -1
- package/dist/es/policyArbiters/examplePolicyArbiter.js +0 -50
- package/dist/es/policyArbiters/examplePolicyArbiter.js.map +0 -1
- package/dist/es/policyEnforcementProcessor/examplePolicyEnforcementProcessor.js +0 -42
- package/dist/es/policyEnforcementProcessor/examplePolicyEnforcementProcessor.js.map +0 -1
- package/dist/es/policyNegotiators/examplePolicyNegotiator.js +0 -61
- package/dist/es/policyNegotiators/examplePolicyNegotiator.js.map +0 -1
- package/dist/es/policyRequesters/examplePolicyRequester.js +0 -69
- package/dist/es/policyRequesters/examplePolicyRequester.js.map +0 -1
- package/dist/types/dataAccessHandlers/examplePolicyRequester.d.ts +0 -68
- package/dist/types/models/IExampleDataAccessHandlerConstructorOptions.d.ts +0 -10
- package/dist/types/policyArbiters/examplePolicyArbiter.d.ts +0 -36
- package/dist/types/policyEnforcementProcessor/examplePolicyEnforcementProcessor.d.ts +0 -29
- package/dist/types/policyNegotiators/examplePolicyNegotiator.d.ts +0 -45
- package/docs/reference/classes/ExampleDataAccessHandler.md +0 -243
- package/docs/reference/classes/ExamplePolicyArbiter.md +0 -121
- package/docs/reference/classes/ExamplePolicyEnforcementProcessor.md +0 -101
- package/docs/reference/interfaces/IExampleDataAccessHandlerConstructorOptions.md +0 -17
- package/docs/reference/interfaces/IExamplePolicyArbiterConstructorOptions.md +0 -17
- package/docs/reference/interfaces/IExamplePolicyEnforcementProcessorConstructorOptions.md +0 -17
- package/docs/reference/interfaces/IExamplePolicyNegotiatorConstructorOptions.md +0 -17
- package/docs/reference/interfaces/IExamplePolicyRequesterConstructorOptions.md +0 -17
|
@@ -0,0 +1,1657 @@
|
|
|
1
|
+
// Copyright 2025 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { ArrayHelper, Coerce, ComponentFactory, GeneralError, Guards, Is, ObjectHelper, StringHelper } from "@twin.org/core";
|
|
4
|
+
import { JsonPathHelper } from "@twin.org/data-json-path";
|
|
5
|
+
import { OdrlPolicyHelper, OdrlProfiles, PolicyDecision, PolicyObligationEnforcerFactory } from "@twin.org/rights-management-models";
|
|
6
|
+
import { OdrlConflictStrategyType, OdrlLogicalConstraintType, OdrlOperatorType, OdrlTypes } 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
|
+
* ODRL profiles whose custom vocabulary this arbiter understands and supports.
|
|
17
|
+
* Any policy declaring a profile not in this set will be rejected.
|
|
18
|
+
* Add a new entry here when support for an additional profile is implemented.
|
|
19
|
+
*/
|
|
20
|
+
static SUPPORTED_PROFILES = new Set([OdrlProfiles.Twin]);
|
|
21
|
+
/**
|
|
22
|
+
* Default maximum inheritance depth.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
static _DEFAULT_MAX_INHERITANCE_DEPTH = 10;
|
|
26
|
+
/**
|
|
27
|
+
* Datasource key for the primary JSON path data source.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
static _DATA_SOURCE_KEY = "data";
|
|
31
|
+
/**
|
|
32
|
+
* Datasource key for the information source used in JSON path expressions.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
static _INFORMATION_SOURCE_KEY = "information";
|
|
36
|
+
/**
|
|
37
|
+
* TWIN prefix JSONPath canonical alias.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
static _TWIN_JSONPATH = "twin:jsonPath";
|
|
41
|
+
/**
|
|
42
|
+
* Canonical jsonPath expression property.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
static _TWIN_JSONPATH_EXPRESSION = "twin:jsonPathExpression";
|
|
46
|
+
/**
|
|
47
|
+
* Optional data source key property for canonical twin:jsonPath targets.
|
|
48
|
+
* When absent, defaults to the primary data source.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
static _TWIN_JSONPATH_DATA_SOURCE = "twin:jsonPathDataSource";
|
|
52
|
+
/**
|
|
53
|
+
* The logging component.
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
_logging;
|
|
57
|
+
/**
|
|
58
|
+
* The policy administration point component.
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
_policyAdministrationPoint;
|
|
62
|
+
/**
|
|
63
|
+
* The maximum depth to traverse when resolving inherited policies.
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
_maxInheritanceDepth;
|
|
67
|
+
/**
|
|
68
|
+
* Create a new instance of DefaultPolicyArbiter.
|
|
69
|
+
* @param options The options for the default policy arbiter.
|
|
70
|
+
*/
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this._logging = ComponentFactory.get(options?.loggingComponentType ?? "logging");
|
|
73
|
+
this._policyAdministrationPoint = ComponentFactory.get(options?.policyAdministrationPointComponentType ?? "policy-administration-point");
|
|
74
|
+
this._maxInheritanceDepth =
|
|
75
|
+
Coerce.integer(options?.config?.maxInheritanceDepth) ??
|
|
76
|
+
DefaultPolicyArbiter._DEFAULT_MAX_INHERITANCE_DEPTH;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Normalises a profile IRI for comparison against the supported-profiles allowlist.
|
|
80
|
+
* Per RFC 3986: scheme and host are case-insensitive (the URL constructor folds them to
|
|
81
|
+
* lowercase automatically); a trailing slash on the terminal path segment is treated as
|
|
82
|
+
* equivalent to its absence; repeated slashes in the path are collapsed to a single slash.
|
|
83
|
+
* Non-URL strings are returned unchanged.
|
|
84
|
+
* @param iri The IRI to normalise.
|
|
85
|
+
* @returns The normalised IRI.
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
static normalizeProfileIri(iri) {
|
|
89
|
+
try {
|
|
90
|
+
const url = new URL(iri);
|
|
91
|
+
const pathname = url.pathname.replace(/\/+/g, "/");
|
|
92
|
+
return `${url.protocol}//${url.host}${StringHelper.trimTrailingSlashes(pathname)}`;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return iri;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Returns the class name of the component.
|
|
100
|
+
* @returns The class name of the component.
|
|
101
|
+
*/
|
|
102
|
+
className() {
|
|
103
|
+
return DefaultPolicyArbiter.CLASS_NAME;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Makes decisions regarding policy access to data.
|
|
107
|
+
* @param agreement The agreement to evaluate.
|
|
108
|
+
* @param information Information provided by the requester to determine if a policy can be created.
|
|
109
|
+
* @param data The data to make a decision on.
|
|
110
|
+
* @param action Optional action to make a decision on, if not provided, the arbiter will evaluate all actions in the agreement.
|
|
111
|
+
* @returns The decisions about access to the data.
|
|
112
|
+
*/
|
|
113
|
+
async decide(agreement, information, data, action) {
|
|
114
|
+
Guards.object(DefaultPolicyArbiter.CLASS_NAME, "agreement", agreement);
|
|
115
|
+
// ODRL policy profiles extend the vocabulary with additional semantics (e.g. custom
|
|
116
|
+
// operators, left operands). Without profile-aware evaluation logic the arbiter
|
|
117
|
+
// cannot guarantee correctness, so any policy that declares an unknown profile is
|
|
118
|
+
// rejected. The TWIN platform profile is explicitly supported.
|
|
119
|
+
//
|
|
120
|
+
// Empty-string profile values (e.g. from over-eager schema defaults or serialization
|
|
121
|
+
// round-trips) are treated as "no profile declared" and filtered out before comparison.
|
|
122
|
+
//
|
|
123
|
+
// Profile IRIs are normalized before lookup: scheme and host are case-folded to
|
|
124
|
+
// lowercase and a trailing slash on the last path segment is stripped. This accepts
|
|
125
|
+
// common IRI variants (HTTPS://, uppercase host, trailing slash) instead of silently
|
|
126
|
+
// rejecting valid policies authored by IRI-aware tooling.
|
|
127
|
+
//
|
|
128
|
+
// `every` (conjunction) is intentional: a policy declaring ["TWIN", "unknown"] is
|
|
129
|
+
// rejected — the arbiter refuses to evaluate rules from a profile whose semantics it
|
|
130
|
+
// does not understand, even if other declared profiles are known.
|
|
131
|
+
const declaredProfiles = ArrayHelper.fromObjectOrArray(agreement.profile ?? [])
|
|
132
|
+
.filter(p => Is.stringValue(p))
|
|
133
|
+
.map(p => DefaultPolicyArbiter.normalizeProfileIri(p));
|
|
134
|
+
if (Is.arrayValue(declaredProfiles) &&
|
|
135
|
+
!declaredProfiles.every(p => DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p))) {
|
|
136
|
+
const unsupportedProfile = declaredProfiles.find(p => !DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p));
|
|
137
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "policyProfileNotSupported", {
|
|
138
|
+
policyId: OdrlPolicyHelper.getUid(agreement) ?? "",
|
|
139
|
+
unsupportedProfile
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
await this._logging.log({
|
|
143
|
+
level: "info",
|
|
144
|
+
source: DefaultPolicyArbiter.CLASS_NAME,
|
|
145
|
+
ts: Date.now(),
|
|
146
|
+
message: "decidingPolicy",
|
|
147
|
+
data: {
|
|
148
|
+
policyId: OdrlPolicyHelper.getUid(agreement) ?? ""
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
// Resolve and merge inherited policies.
|
|
152
|
+
const mergedPolicy = await this.mergeInheritedPolicies(agreement);
|
|
153
|
+
const expandedPolicy = this.expandCompactPolicyRules(mergedPolicy);
|
|
154
|
+
const dataSources = {
|
|
155
|
+
[DefaultPolicyArbiter._DATA_SOURCE_KEY]: data,
|
|
156
|
+
[DefaultPolicyArbiter._INFORMATION_SOURCE_KEY]: information
|
|
157
|
+
};
|
|
158
|
+
// Extract agreement parties once for use in rule evaluation
|
|
159
|
+
const agreementAssigner = OdrlPolicyHelper.getPartyIds(agreement.assigner);
|
|
160
|
+
const agreementAssignee = OdrlPolicyHelper.getPartyIds(agreement.assignee);
|
|
161
|
+
const obligationsFulfilled = await this.evaluatePolicyObligations(agreementAssigner, agreementAssignee, expandedPolicy, dataSources);
|
|
162
|
+
// ODRL-style rule evaluation grouped by decision target:
|
|
163
|
+
// - Permission rules authorize if ANY applicable permission on the same target matches.
|
|
164
|
+
// - Default to denied when no permission applies on a target (closed-world for access control).
|
|
165
|
+
// - Conflict strategy controls how applicable permissions/prohibitions are resolved per target.
|
|
166
|
+
const targetStates = Object.create(null);
|
|
167
|
+
const permissions = ArrayHelper.fromObjectOrArray(expandedPolicy.permission ?? []);
|
|
168
|
+
for (const permission of permissions) {
|
|
169
|
+
const decisionTargets = this.resolveRuleDecisionTargets(permission, dataSources);
|
|
170
|
+
for (const decisionTarget of decisionTargets) {
|
|
171
|
+
const state = this.getOrCreateTargetState(targetStates, decisionTarget.target);
|
|
172
|
+
if (await this.evaluatePermission(agreementAssigner, agreementAssignee, expandedPolicy, permission, decisionTarget.refinements, dataSources, action, decisionTarget.target)) {
|
|
173
|
+
state.permissionApplies = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const prohibitions = ArrayHelper.fromObjectOrArray(expandedPolicy.prohibition ?? []);
|
|
178
|
+
for (const prohibition of prohibitions) {
|
|
179
|
+
const decisionTargets = this.resolveRuleDecisionTargets(prohibition, dataSources);
|
|
180
|
+
for (const decisionTarget of decisionTargets) {
|
|
181
|
+
if (await this.evaluateProhibition(expandedPolicy, agreementAssigner, agreementAssignee, prohibition, decisionTarget.refinements, dataSources, action, decisionTarget.target)) {
|
|
182
|
+
const state = this.getOrCreateTargetState(targetStates, decisionTarget.target);
|
|
183
|
+
state.prohibitionApplies = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const conflictStrategy = expandedPolicy.conflict ?? OdrlConflictStrategyType.Invalid;
|
|
188
|
+
if (Object.keys(targetStates).length === 0) {
|
|
189
|
+
// Closed-world fallback when the policy has no rules at all.
|
|
190
|
+
return [{ decision: PolicyDecision.Denied, target: "$" }];
|
|
191
|
+
}
|
|
192
|
+
const decisions = [];
|
|
193
|
+
for (const [target, state] of Object.entries(targetStates)) {
|
|
194
|
+
let decision;
|
|
195
|
+
if (state.permissionApplies && state.prohibitionApplies) {
|
|
196
|
+
switch (conflictStrategy) {
|
|
197
|
+
case OdrlConflictStrategyType.Perm:
|
|
198
|
+
decision = PolicyDecision.Granted;
|
|
199
|
+
break;
|
|
200
|
+
case OdrlConflictStrategyType.Prohibit:
|
|
201
|
+
case OdrlConflictStrategyType.Invalid:
|
|
202
|
+
default:
|
|
203
|
+
decision = PolicyDecision.Denied;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
decision = state.permissionApplies ? PolicyDecision.Granted : PolicyDecision.Denied;
|
|
209
|
+
}
|
|
210
|
+
if (!obligationsFulfilled) {
|
|
211
|
+
decision = PolicyDecision.Denied;
|
|
212
|
+
}
|
|
213
|
+
decisions.push({
|
|
214
|
+
decision,
|
|
215
|
+
target
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return decisions;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Expand compact/compound policy rule forms into atomic rules.
|
|
222
|
+
* ODRL 2.7 allows compact forms where rule properties can be arrays.
|
|
223
|
+
* Evaluation in this arbiter is performed on expanded atomic rules.
|
|
224
|
+
* @param policy The policy to expand.
|
|
225
|
+
* @returns A policy with expanded rule arrays.
|
|
226
|
+
* @internal
|
|
227
|
+
*/
|
|
228
|
+
expandCompactPolicyRules(policy) {
|
|
229
|
+
const expandedPermissions = this.expandRules(ArrayHelper.fromObjectOrArray(policy.permission ?? []));
|
|
230
|
+
const expandedProhibitions = this.expandRules(ArrayHelper.fromObjectOrArray(policy.prohibition ?? []));
|
|
231
|
+
const expandedObligations = this.expandRules(ArrayHelper.fromObjectOrArray(policy.obligation ?? []));
|
|
232
|
+
return {
|
|
233
|
+
...policy,
|
|
234
|
+
permission: expandedPermissions.length > 0 ? expandedPermissions : undefined,
|
|
235
|
+
prohibition: expandedProhibitions.length > 0 ? expandedProhibitions : undefined,
|
|
236
|
+
obligation: expandedObligations.length > 0 ? expandedObligations : undefined
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Expand a rule list into atomic rules.
|
|
241
|
+
* @param rules The rules to expand.
|
|
242
|
+
* @returns Expanded atomic rules.
|
|
243
|
+
* @internal
|
|
244
|
+
*/
|
|
245
|
+
expandRules(rules) {
|
|
246
|
+
const expanded = [];
|
|
247
|
+
for (const rule of rules) {
|
|
248
|
+
expanded.push(...this.expandRule(rule));
|
|
249
|
+
}
|
|
250
|
+
return expanded;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Expand a single compact/compound rule into atomic rules.
|
|
254
|
+
* @param rule The rule to expand.
|
|
255
|
+
* @returns Expanded atomic rules.
|
|
256
|
+
* @internal
|
|
257
|
+
*/
|
|
258
|
+
expandRule(rule) {
|
|
259
|
+
const targets = this.normalizeRuleField(rule.target);
|
|
260
|
+
const actions = this.normalizeRuleField(rule.action);
|
|
261
|
+
const assigners = this.normalizeRuleField(rule.assigner);
|
|
262
|
+
const assignees = this.normalizeRuleField(rule.assignee);
|
|
263
|
+
const expanded = [];
|
|
264
|
+
for (const target of targets) {
|
|
265
|
+
for (const action of actions) {
|
|
266
|
+
for (const assigner of assigners) {
|
|
267
|
+
for (const assignee of assignees) {
|
|
268
|
+
const atomicRule = { ...rule };
|
|
269
|
+
atomicRule.target = target;
|
|
270
|
+
atomicRule.action = action;
|
|
271
|
+
atomicRule.assigner = assigner;
|
|
272
|
+
atomicRule.assignee = assignee;
|
|
273
|
+
expanded.push(atomicRule);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return expanded;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Normalize a potentially compact rule field into an array for expansion.
|
|
282
|
+
* @param value The field value.
|
|
283
|
+
* @returns Normalized values (or a single undefined when not provided).
|
|
284
|
+
* @internal
|
|
285
|
+
*/
|
|
286
|
+
normalizeRuleField(value) {
|
|
287
|
+
if (Is.undefined(value)) {
|
|
288
|
+
return [undefined];
|
|
289
|
+
}
|
|
290
|
+
const values = ArrayHelper.fromObjectOrArray(value);
|
|
291
|
+
return Is.arrayValue(values) ? values : [undefined];
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get an existing target state or create an initial state if it doesn't exist.
|
|
295
|
+
* @param targetStates The dictionary of target states.
|
|
296
|
+
* @param target The target key.
|
|
297
|
+
* @returns The target state.
|
|
298
|
+
* @internal
|
|
299
|
+
*/
|
|
300
|
+
getOrCreateTargetState(targetStates, target) {
|
|
301
|
+
let state = targetStates[target];
|
|
302
|
+
if (Is.undefined(state)) {
|
|
303
|
+
state = {
|
|
304
|
+
permissionApplies: false,
|
|
305
|
+
prohibitionApplies: false
|
|
306
|
+
};
|
|
307
|
+
targetStates[target] = state;
|
|
308
|
+
}
|
|
309
|
+
return state;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Evaluate whether a prohibition applies.
|
|
313
|
+
* @param policy The policy containing the prohibition.
|
|
314
|
+
* @param agreementAssigner The assigner ID from the agreement.
|
|
315
|
+
* @param agreementAssignee The assignee ID from the agreement.
|
|
316
|
+
* @param prohibition The prohibition to evaluate.
|
|
317
|
+
* @param targetRefinements Additional constraints from target refinement.
|
|
318
|
+
* @param dataSources The operand lookup sources.
|
|
319
|
+
* @param action Optional action to check against the prohibition's applicable actions.
|
|
320
|
+
* @returns True if the prohibition applies.
|
|
321
|
+
* @internal
|
|
322
|
+
*/
|
|
323
|
+
async evaluateProhibition(policy, agreementAssigner, agreementAssignee, prohibition, targetRefinements, dataSources, action, decisionTarget) {
|
|
324
|
+
if (!this.isRuleApplicableToParties(prohibition, agreementAssigner, agreementAssignee, dataSources)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
// Check if the prohibition's action(s) match the requested action
|
|
328
|
+
if (!this.isActionApplicable(prohibition.action, action, dataSources)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
// ODRL semantics: a rule without constraints is unconditional.
|
|
332
|
+
const constraints = [
|
|
333
|
+
...ArrayHelper.fromObjectOrArray(prohibition.constraint ?? []),
|
|
334
|
+
...targetRefinements
|
|
335
|
+
];
|
|
336
|
+
if (constraints.length > 0 &&
|
|
337
|
+
!constraints.every(c => this.evaluateConstraint(c, dataSources))) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
const prohibitionTargetLookup = this.tryResolveTargetDataSource(this.buildRuleDataContextTargetId(this.getRuleDataContextTargetId(prohibition.target), decisionTarget), dataSources, true);
|
|
341
|
+
const ruleDataContext = prohibitionTargetLookup.value;
|
|
342
|
+
const remedies = ArrayHelper.fromObjectOrArray(prohibition.remedy ?? []);
|
|
343
|
+
if (remedies.length === 0) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
for (const remedy of remedies) {
|
|
347
|
+
if (!(await this.enforceDuty(policy, remedy, dataSources, ruleDataContext))) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Remedies satisfied: prohibition is treated as no longer infringed.
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Evaluate all policy-level obligations.
|
|
356
|
+
* @param agreementAssigner The assigner ID from the agreement.
|
|
357
|
+
* @param agreementAssignee The assignee ID from the agreement.
|
|
358
|
+
* @param policy The policy containing obligations.
|
|
359
|
+
* @param dataSources The operand lookup sources.
|
|
360
|
+
* @returns True if all applicable obligations are fulfilled.
|
|
361
|
+
* @internal
|
|
362
|
+
*/
|
|
363
|
+
async evaluatePolicyObligations(agreementAssigner, agreementAssignee, policy, dataSources) {
|
|
364
|
+
const obligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []);
|
|
365
|
+
for (const obligation of obligations) {
|
|
366
|
+
if (!(await this.evaluateObligation(agreementAssigner, agreementAssignee, policy, obligation, dataSources))) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Evaluate whether a policy-level obligation is fulfilled.
|
|
374
|
+
* @param agreementAssigner The assigner ID from the agreement.
|
|
375
|
+
* @param agreementAssignee The assignee ID from the agreement.
|
|
376
|
+
* @param policy The policy containing the obligation.
|
|
377
|
+
* @param obligation The obligation to evaluate.
|
|
378
|
+
* @param dataSources The operand lookup sources.
|
|
379
|
+
* @returns True if the obligation is not applicable or is fulfilled.
|
|
380
|
+
* @internal
|
|
381
|
+
*/
|
|
382
|
+
async evaluateObligation(agreementAssigner, agreementAssignee, policy, obligation, dataSources) {
|
|
383
|
+
if (!this.isRuleApplicableToParties(obligation, agreementAssigner, agreementAssignee, dataSources)) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const { refinements } = this.resolveRuleTarget(obligation, dataSources);
|
|
387
|
+
const obligationTargetLookup = this.tryResolveTargetDataSource(this.getTargetId(obligation.target), dataSources, true);
|
|
388
|
+
const ruleDataContext = obligationTargetLookup.value;
|
|
389
|
+
const constraints = [
|
|
390
|
+
...ArrayHelper.fromObjectOrArray(obligation.constraint ?? []),
|
|
391
|
+
...refinements
|
|
392
|
+
];
|
|
393
|
+
if (constraints.length > 0 &&
|
|
394
|
+
!constraints.every(c => this.evaluateConstraint(c, dataSources))) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
return this.enforceDuty(policy, obligation, dataSources, ruleDataContext);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Merge inherited policies into the current policy.
|
|
401
|
+
* Inherited policies' permissions, prohibitions, and obligations are combined with the current policy's rules.
|
|
402
|
+
* @param policy The policy to evaluate.
|
|
403
|
+
* @returns A new policy with merged rules from all ancestors.
|
|
404
|
+
* @internal
|
|
405
|
+
*/
|
|
406
|
+
async mergeInheritedPolicies(policy) {
|
|
407
|
+
const visitedPolicyIds = [];
|
|
408
|
+
visitedPolicyIds.push(OdrlPolicyHelper.getUid(policy) ?? "");
|
|
409
|
+
const inheritedPolicies = await this.resolveInheritedPolicies(policy, visitedPolicyIds, 0);
|
|
410
|
+
const conflictStrategies = new Set();
|
|
411
|
+
if (Is.stringValue(policy.conflict)) {
|
|
412
|
+
conflictStrategies.add(policy.conflict);
|
|
413
|
+
}
|
|
414
|
+
// Start with copies of the current policy's rules
|
|
415
|
+
const mergedPermissions = ArrayHelper.fromObjectOrArray(policy.permission ?? []).map(permission => this.applyPolicyDefaultsToRule(policy, permission));
|
|
416
|
+
const mergedProhibitions = ArrayHelper.fromObjectOrArray(policy.prohibition ?? []).map(prohibition => this.applyPolicyDefaultsToRule(policy, prohibition));
|
|
417
|
+
const mergedObligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []).map(obligation => this.applyPolicyDefaultsToRule(policy, obligation));
|
|
418
|
+
// Merge rules from each inherited policy, applying the same profile guard as the
|
|
419
|
+
// top-level policy. A parent that declares an unsupported profile may carry rules
|
|
420
|
+
// whose semantics the arbiter cannot guarantee, so it is rejected.
|
|
421
|
+
for (const inheritedPolicy of inheritedPolicies) {
|
|
422
|
+
const inheritedProfiles = ArrayHelper.fromObjectOrArray(inheritedPolicy.profile ?? [])
|
|
423
|
+
.filter(p => Is.stringValue(p))
|
|
424
|
+
.map(p => DefaultPolicyArbiter.normalizeProfileIri(p));
|
|
425
|
+
if (Is.arrayValue(inheritedProfiles) &&
|
|
426
|
+
!inheritedProfiles.every(p => DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p))) {
|
|
427
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "inheritedPolicyProfileNotSupported", { policyId: OdrlPolicyHelper.getUid(inheritedPolicy) ?? "" });
|
|
428
|
+
}
|
|
429
|
+
if (Is.stringValue(inheritedPolicy.conflict)) {
|
|
430
|
+
conflictStrategies.add(inheritedPolicy.conflict);
|
|
431
|
+
}
|
|
432
|
+
const inheritedPermissions = ArrayHelper.fromObjectOrArray(inheritedPolicy.permission ?? []);
|
|
433
|
+
if (inheritedPermissions.length > 0) {
|
|
434
|
+
mergedPermissions.push(...inheritedPermissions.map(permission => this.applyPolicyDefaultsToRule(inheritedPolicy, permission)));
|
|
435
|
+
}
|
|
436
|
+
const inheritedProhibitions = ArrayHelper.fromObjectOrArray(inheritedPolicy.prohibition ?? []);
|
|
437
|
+
if (inheritedProhibitions.length > 0) {
|
|
438
|
+
mergedProhibitions.push(...inheritedProhibitions.map(prohibition => this.applyPolicyDefaultsToRule(inheritedPolicy, prohibition)));
|
|
439
|
+
}
|
|
440
|
+
const inheritedObligations = ArrayHelper.fromObjectOrArray(inheritedPolicy.obligation ?? []);
|
|
441
|
+
if (inheritedObligations.length > 0) {
|
|
442
|
+
mergedObligations.push(...inheritedObligations.map(obligation => this.applyPolicyDefaultsToRule(inheritedPolicy, obligation)));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
let mergedConflict;
|
|
446
|
+
if (conflictStrategies.size === 1) {
|
|
447
|
+
mergedConflict = Array.from(conflictStrategies)[0];
|
|
448
|
+
}
|
|
449
|
+
else if (conflictStrategies.size > 1) {
|
|
450
|
+
mergedConflict = OdrlConflictStrategyType.Invalid;
|
|
451
|
+
}
|
|
452
|
+
// Return a new policy with merged rules
|
|
453
|
+
return {
|
|
454
|
+
...policy,
|
|
455
|
+
conflict: mergedConflict,
|
|
456
|
+
permission: mergedPermissions.length > 0 ? mergedPermissions : undefined,
|
|
457
|
+
prohibition: mergedProhibitions.length > 0 ? mergedProhibitions : undefined,
|
|
458
|
+
obligation: mergedObligations.length > 0 ? mergedObligations : undefined
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Resolve inherited policies by their UIDs from the Policy Administration Point.
|
|
463
|
+
* Policies can inherit from other policies via the inheritFrom property.
|
|
464
|
+
* @param policy The policy that may have inheritFrom references.
|
|
465
|
+
* @param visitedPolicyIds Array of policy UIDs already visited in this inheritance chain.
|
|
466
|
+
* @returns Array of inherited policies fetched from the PAP.
|
|
467
|
+
* @internal
|
|
468
|
+
*/
|
|
469
|
+
async resolveInheritedPolicies(policy, visitedPolicyIds, currentDepth) {
|
|
470
|
+
const inheritedPolicies = [];
|
|
471
|
+
// If policy has no inheritFrom, return empty array
|
|
472
|
+
if (Is.empty(policy.inheritFrom)) {
|
|
473
|
+
return inheritedPolicies;
|
|
474
|
+
}
|
|
475
|
+
const inheritFromIds = ArrayHelper.fromObjectOrArray(policy.inheritFrom) ?? [];
|
|
476
|
+
for (const inheritFromId of inheritFromIds) {
|
|
477
|
+
const nextDepth = currentDepth + 1;
|
|
478
|
+
if (nextDepth > this._maxInheritanceDepth) {
|
|
479
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "maxInheritanceDepthExceeded", {
|
|
480
|
+
policyId: OdrlPolicyHelper.getUid(policy) ?? "",
|
|
481
|
+
inheritFromId,
|
|
482
|
+
maxInheritanceDepth: this._maxInheritanceDepth
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
// Check for circular inheritance
|
|
486
|
+
if (visitedPolicyIds.includes(inheritFromId)) {
|
|
487
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "circularInheritanceDetected", {
|
|
488
|
+
policyId: OdrlPolicyHelper.getUid(policy) ?? "",
|
|
489
|
+
inheritFromId
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
// Fetch the inherited policy from the PAP
|
|
493
|
+
const inheritedPolicy = await this._policyAdministrationPoint.get(inheritFromId);
|
|
494
|
+
if (Is.empty(inheritedPolicy)) {
|
|
495
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "inheritedPolicyNotFound", {
|
|
496
|
+
inheritFromId
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
// Mark this policy as visited
|
|
500
|
+
visitedPolicyIds.push(inheritFromId);
|
|
501
|
+
inheritedPolicies.push(inheritedPolicy);
|
|
502
|
+
// Recursively resolve inherited policies of the parent
|
|
503
|
+
const grandparentPolicies = await this.resolveInheritedPolicies(inheritedPolicy, visitedPolicyIds, nextDepth);
|
|
504
|
+
inheritedPolicies.push(...grandparentPolicies);
|
|
505
|
+
}
|
|
506
|
+
return inheritedPolicies;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Apply policy-level default values (assigner, assignee, target, action) to rules that don't override them.
|
|
510
|
+
* @param policy The policy providing defaults.
|
|
511
|
+
* @param rule The rule to apply defaults to.
|
|
512
|
+
* @returns The rule with policy-level defaults applied.
|
|
513
|
+
* @internal
|
|
514
|
+
*/
|
|
515
|
+
applyPolicyDefaultsToRule(policy, rule) {
|
|
516
|
+
const assigner = Is.empty(rule.assigner) ? policy.assigner : rule.assigner;
|
|
517
|
+
const assignee = Is.empty(rule.assignee) ? policy.assignee : rule.assignee;
|
|
518
|
+
const target = Is.empty(rule.target) ? policy.target : rule.target;
|
|
519
|
+
const action = Is.empty(rule.action) ? policy.action : rule.action;
|
|
520
|
+
const assignerIds = OdrlPolicyHelper.getPartyIds(assigner);
|
|
521
|
+
const ruleAssignerIds = OdrlPolicyHelper.getPartyIds(rule.assigner);
|
|
522
|
+
const assigneeIds = OdrlPolicyHelper.getPartyIds(assignee);
|
|
523
|
+
const ruleAssigneeIds = OdrlPolicyHelper.getPartyIds(rule.assignee);
|
|
524
|
+
let assignerEqual = false;
|
|
525
|
+
if (Is.empty(assignerIds) && Is.empty(ruleAssignerIds)) {
|
|
526
|
+
assignerEqual = true;
|
|
527
|
+
}
|
|
528
|
+
else if (!Is.empty(assignerIds) && !Is.empty(ruleAssignerIds)) {
|
|
529
|
+
assignerEqual =
|
|
530
|
+
assignerIds.length === ruleAssignerIds.length &&
|
|
531
|
+
assignerIds.every(id => ruleAssignerIds.includes(id));
|
|
532
|
+
}
|
|
533
|
+
let assigneeEqual = false;
|
|
534
|
+
if (Is.empty(assigneeIds) && Is.empty(ruleAssigneeIds)) {
|
|
535
|
+
assigneeEqual = true;
|
|
536
|
+
}
|
|
537
|
+
else if (!Is.empty(assigneeIds) && !Is.empty(ruleAssigneeIds)) {
|
|
538
|
+
assigneeEqual =
|
|
539
|
+
assigneeIds.length === ruleAssigneeIds.length &&
|
|
540
|
+
assigneeIds.every(id => ruleAssigneeIds.includes(id));
|
|
541
|
+
}
|
|
542
|
+
const targetEqual = target === rule.target;
|
|
543
|
+
const actionEqual = action === rule.action;
|
|
544
|
+
if (assignerEqual && assigneeEqual && targetEqual && actionEqual) {
|
|
545
|
+
return rule;
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
...rule,
|
|
549
|
+
assigner,
|
|
550
|
+
assignee,
|
|
551
|
+
target,
|
|
552
|
+
action
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Determine whether a rule applies to the agreement parties based on assigner/assignee.
|
|
557
|
+
* @param rule The rule to evaluate.
|
|
558
|
+
* @param agreementAssigner The assigner ID from the agreement.
|
|
559
|
+
* @param agreementAssignee The assignee ID from the agreement.
|
|
560
|
+
* @returns True if the rule is applicable to the agreement parties.
|
|
561
|
+
* @internal
|
|
562
|
+
*/
|
|
563
|
+
isRuleApplicableToParties(rule, agreementAssigner, agreementAssignee, dataSources) {
|
|
564
|
+
const assignerContext = this.resolveRulePartyContext(rule.assigner);
|
|
565
|
+
const assigneeContext = this.resolveRulePartyContext(rule.assignee);
|
|
566
|
+
if (!this.isPartyApplicable(assignerContext.partyIds, agreementAssigner)) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
if (assignerContext.refinements.length > 0 &&
|
|
570
|
+
!assignerContext.refinements.every(c => this.evaluateConstraint(c, dataSources))) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
if (!this.isPartyApplicable(assigneeContext.partyIds, agreementAssignee)) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
if (assigneeContext.refinements.length > 0 &&
|
|
577
|
+
!assigneeContext.refinements.every(c => this.evaluateConstraint(c, dataSources))) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Resolve rule party identifiers and refinements.
|
|
584
|
+
* PartyCollection source values are currently not supported for party matching.
|
|
585
|
+
* @param party The rule party value.
|
|
586
|
+
* @returns Resolved party identifiers and refinement constraints.
|
|
587
|
+
* @throws GeneralError if PartyCollection source has a value.
|
|
588
|
+
* @internal
|
|
589
|
+
*/
|
|
590
|
+
resolveRulePartyContext(party) {
|
|
591
|
+
const partyIds = [];
|
|
592
|
+
const refinements = [];
|
|
593
|
+
const parties = ArrayHelper.fromObjectOrArray(party ?? []);
|
|
594
|
+
for (const partyEntry of parties) {
|
|
595
|
+
if (Is.stringValue(partyEntry)) {
|
|
596
|
+
partyIds.push(partyEntry);
|
|
597
|
+
}
|
|
598
|
+
else if (Is.object(partyEntry)) {
|
|
599
|
+
// Guard against unsupported ODRL party properties
|
|
600
|
+
if (Is.notEmpty(partyEntry.assignerOf)) {
|
|
601
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyAssignerOfNotSupported");
|
|
602
|
+
}
|
|
603
|
+
if (Is.notEmpty(partyEntry.assigneeOf)) {
|
|
604
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyAssigneeOfNotSupported");
|
|
605
|
+
}
|
|
606
|
+
if (Is.notEmpty(partyEntry.partOf)) {
|
|
607
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyPartOfNotSupported");
|
|
608
|
+
}
|
|
609
|
+
if (OdrlPolicyHelper.getType(partyEntry) === OdrlTypes.PartyCollection) {
|
|
610
|
+
const partyCollectionEntry = partyEntry;
|
|
611
|
+
if (Is.stringValue(partyCollectionEntry.source)) {
|
|
612
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyCollectionSourceNotSupported", {
|
|
613
|
+
source: partyCollectionEntry.source ?? ""
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
refinements.push(...ArrayHelper.fromObjectOrArray(partyCollectionEntry.refinement ?? []));
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
const partyId = OdrlPolicyHelper.getUid(partyEntry);
|
|
620
|
+
if (Is.stringValue(partyId)) {
|
|
621
|
+
partyIds.push(partyId);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
partyIds,
|
|
628
|
+
refinements
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Determine whether a rule party constraint applies to the agreement parties.
|
|
633
|
+
* Rule party is treated as a constraint: if specified, it must match at least one agreement party.
|
|
634
|
+
* @param rulePartyIds The party ids specified on the rule (if any).
|
|
635
|
+
* @param agreementPartyIds The party ids extracted from the agreement.
|
|
636
|
+
* @returns True if the rule party constraint is satisfied.
|
|
637
|
+
* @internal
|
|
638
|
+
*/
|
|
639
|
+
isPartyApplicable(rulePartyIds, agreementPartyIds) {
|
|
640
|
+
// No party specified on rule means it applies to any agreement party.
|
|
641
|
+
if (Is.empty(rulePartyIds)) {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
// Rule specifies party/parties, but agreement doesn't provide any.
|
|
645
|
+
if (Is.empty(agreementPartyIds)) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
for (const rulePartyId of rulePartyIds) {
|
|
649
|
+
if (agreementPartyIds.includes(rulePartyId)) {
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Determine whether a rule's action(s) match the requested action.
|
|
657
|
+
* Supports exact match, includedIn hierarchy, and implies relationships.
|
|
658
|
+
* If no action is specified in the rule, it applies to all actions (action-agnostic).
|
|
659
|
+
* If an action is specified in the rule and a specific action is requested, they must match.
|
|
660
|
+
* If an action is specified in the rule but no specific action is requested, the rule applies (general evaluation).
|
|
661
|
+
* @param ruleActions The actions defined in the rule (can be string, object, or array).
|
|
662
|
+
* @param requestedAction The action being requested (optional).
|
|
663
|
+
* @returns True if the rule's action(s) apply.
|
|
664
|
+
* @internal
|
|
665
|
+
*/
|
|
666
|
+
isActionApplicable(ruleActions, requestedAction, dataSources) {
|
|
667
|
+
// If the rule has no action specified, it applies to all actions
|
|
668
|
+
if (Is.empty(ruleActions)) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
// If the rule has actions but no specific action is requested,
|
|
672
|
+
// the rule applies (we're evaluating permissions in general)
|
|
673
|
+
if (Is.empty(requestedAction)) {
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
const requestedActionId = Is.string(requestedAction)
|
|
677
|
+
? requestedAction
|
|
678
|
+
: OdrlPolicyHelper.getUid(requestedAction);
|
|
679
|
+
if (!Is.stringValue(requestedActionId)) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
const ruleActionArray = ArrayHelper.fromObjectOrArray(ruleActions) ?? [];
|
|
683
|
+
for (const ruleAction of ruleActionArray) {
|
|
684
|
+
if (this.ruleActionCoversRequested(ruleAction, requestedActionId, dataSources)) {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Determine whether a single rule action covers a requested action.
|
|
692
|
+
* Covers exact match plus ODRL action hierarchy semantics.
|
|
693
|
+
* - includedIn: the rule action is a sub-action of a broader parent.
|
|
694
|
+
* A rule naming the narrower action also covers requests for the parent.
|
|
695
|
+
* E.g. rule action "print" with includedIn "reproduce" covers a request for "reproduce".
|
|
696
|
+
* - implies: the rule action entails another action.
|
|
697
|
+
* A rule granting action X also covers action Y when X implies Y.
|
|
698
|
+
* E.g. rule action "distribute" implying "reproduce" covers a request for "reproduce".
|
|
699
|
+
* @param ruleAction The action specified in the rule.
|
|
700
|
+
* @param requestedActionId The requested action identifier.
|
|
701
|
+
* @returns True if the rule action covers the requested action.
|
|
702
|
+
* @internal
|
|
703
|
+
*/
|
|
704
|
+
ruleActionCoversRequested(ruleAction, requestedActionId, dataSources) {
|
|
705
|
+
// Extract the rule action ID — support both @id and rdf:value forms
|
|
706
|
+
let ruleActionId;
|
|
707
|
+
if (Is.string(ruleAction)) {
|
|
708
|
+
ruleActionId = ruleAction;
|
|
709
|
+
}
|
|
710
|
+
else if (Is.object(ruleAction)) {
|
|
711
|
+
ruleActionId = ruleAction["rdf:value"]?.["@id"] ?? OdrlPolicyHelper.getUid(ruleAction);
|
|
712
|
+
}
|
|
713
|
+
// Determine whether this rule action covers the requested action via any semantic path.
|
|
714
|
+
let covers = false;
|
|
715
|
+
if (Is.stringValue(ruleActionId) && ruleActionId === requestedActionId) {
|
|
716
|
+
// Exact match
|
|
717
|
+
covers = true;
|
|
718
|
+
}
|
|
719
|
+
else if (Is.object(ruleAction)) {
|
|
720
|
+
// includedIn: rule action A includedIn B means A is a sub-type of B.
|
|
721
|
+
// A rule that names the narrower action A with includedIn B also covers requests for B.
|
|
722
|
+
if (Is.stringValue(ruleAction.includedIn) && ruleAction.includedIn === requestedActionId) {
|
|
723
|
+
covers = true;
|
|
724
|
+
}
|
|
725
|
+
// implies: rule action A implies B means exercising A also entails B.
|
|
726
|
+
// A rule granting A therefore also grants each implied action.
|
|
727
|
+
if (!covers && (ruleAction.implies ?? []).includes(requestedActionId)) {
|
|
728
|
+
covers = true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (!covers) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
// If the action specifies refinements, all must be satisfied for the action to apply.
|
|
735
|
+
// Refinements constrain the manner in which the action is exercised (e.g. print count <= 5).
|
|
736
|
+
if (Is.object(ruleAction) && Is.notEmpty(ruleAction.refinement)) {
|
|
737
|
+
const refinements = ArrayHelper.fromObjectOrArray(ruleAction.refinement ?? []);
|
|
738
|
+
return refinements.every((refinement) => this.evaluateConstraint(refinement, dataSources));
|
|
739
|
+
}
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Apply a permission and create decisions based on that information.
|
|
744
|
+
* @param agreementAssigner The assigner ID from the agreement.
|
|
745
|
+
* @param agreementAssignee The assignee ID from the agreement.
|
|
746
|
+
* @param policy The policy containing the permission.
|
|
747
|
+
* @param permission The permission to apply.
|
|
748
|
+
* @param targetRefinements Additional constraints from target refinement.
|
|
749
|
+
* @param dataSources The operand lookup sources.
|
|
750
|
+
* @returns True if the permission applies.
|
|
751
|
+
* @internal
|
|
752
|
+
*/
|
|
753
|
+
async evaluatePermission(agreementAssigner, agreementAssignee, policy, permission, targetRefinements, dataSources, action, decisionTarget) {
|
|
754
|
+
if (!this.isRuleApplicableToParties(permission, agreementAssigner, agreementAssignee, dataSources)) {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
// Check if the permission's action(s) match the requested action
|
|
758
|
+
if (!this.isActionApplicable(permission.action, action, dataSources)) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
// ODRL semantics: a Permission without constraints is unconditional.
|
|
762
|
+
const constraints = [
|
|
763
|
+
...ArrayHelper.fromObjectOrArray(permission.constraint ?? []),
|
|
764
|
+
...targetRefinements
|
|
765
|
+
];
|
|
766
|
+
const permissionTargetLookup = this.tryResolveTargetDataSource(this.buildRuleDataContextTargetId(this.getRuleDataContextTargetId(permission.target), decisionTarget), dataSources, true);
|
|
767
|
+
const ruleDataContext = permissionTargetLookup.value;
|
|
768
|
+
if (constraints.length === 0) {
|
|
769
|
+
return this.enforcePermissionDuties(policy, permission, dataSources, ruleDataContext);
|
|
770
|
+
}
|
|
771
|
+
// All constraints must be satisfied for the permission to apply.
|
|
772
|
+
const constraintsSatisfied = constraints.every(c => this.evaluateConstraint(c, dataSources));
|
|
773
|
+
if (!constraintsSatisfied) {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
return this.enforcePermissionDuties(policy, permission, dataSources, ruleDataContext);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Enforce duties attached to a permission.
|
|
780
|
+
* @param policy The policy being evaluated.
|
|
781
|
+
* @param permission The permission being evaluated.
|
|
782
|
+
* @param dataSources The operand lookup sources.
|
|
783
|
+
* @param ruleDataContext The target-scoped data context passed to enforcers.
|
|
784
|
+
* @returns True if all duties are enforced or none are present.
|
|
785
|
+
* @internal
|
|
786
|
+
*/
|
|
787
|
+
async enforcePermissionDuties(policy, permission, dataSources, ruleDataContext) {
|
|
788
|
+
const duties = ArrayHelper.fromObjectOrArray(permission.duty ?? []);
|
|
789
|
+
if (duties.length === 0) {
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
for (const duty of duties) {
|
|
793
|
+
const enforced = await this.enforceDuty(policy, duty, dataSources, ruleDataContext);
|
|
794
|
+
if (!enforced) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Enforce a single duty using registered obligation enforcers.
|
|
802
|
+
* @param policy The policy being evaluated.
|
|
803
|
+
* @param duty The duty to enforce.
|
|
804
|
+
* @param dataSources The operand lookup sources.
|
|
805
|
+
* @param ruleDataContext The target-scoped data context passed to enforcers.
|
|
806
|
+
* @returns True if any enforcer succeeds.
|
|
807
|
+
* @internal
|
|
808
|
+
*/
|
|
809
|
+
async enforceDuty(policy, duty, dataSources, ruleDataContext) {
|
|
810
|
+
const enforcerNames = PolicyObligationEnforcerFactory.names();
|
|
811
|
+
const information = dataSources[DefaultPolicyArbiter._INFORMATION_SOURCE_KEY];
|
|
812
|
+
if (enforcerNames.length === 0) {
|
|
813
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "noObligationEnforcersRegistered");
|
|
814
|
+
}
|
|
815
|
+
for (const enforcerName of enforcerNames) {
|
|
816
|
+
const enforcer = PolicyObligationEnforcerFactory.get(enforcerName);
|
|
817
|
+
if (await enforcer.enforce(policy, duty, information, ruleDataContext)) {
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const consequences = ArrayHelper.fromObjectOrArray(duty.consequence ?? []);
|
|
822
|
+
if (consequences.length === 0) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
for (const consequence of consequences) {
|
|
826
|
+
if (!(await this.enforceDuty(policy, consequence, dataSources, ruleDataContext))) {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Resolve a target string to a matching datasource prefix and remaining target value.
|
|
834
|
+
* @param targetId The target identifier to resolve.
|
|
835
|
+
* @param dataSources The available lookup sources.
|
|
836
|
+
* @param resolveValue True to resolve an item from the target path/key.
|
|
837
|
+
* @returns The matching prefix, source, remaining target and optional resolved value.
|
|
838
|
+
* @internal
|
|
839
|
+
*/
|
|
840
|
+
tryResolveTargetDataSource(targetId, dataSources, resolveValue = false) {
|
|
841
|
+
// If there is no target id, default to the data datasource
|
|
842
|
+
if (Is.empty(targetId)) {
|
|
843
|
+
return {
|
|
844
|
+
source: dataSources[DefaultPolicyArbiter._DATA_SOURCE_KEY],
|
|
845
|
+
target: "$",
|
|
846
|
+
value: dataSources[DefaultPolicyArbiter._DATA_SOURCE_KEY]
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
// Handle twin:jsonPath:<datasource>:<expression> format.
|
|
850
|
+
// The datasource segment is optional; when absent the expression starts with "$"
|
|
851
|
+
// and falls back to the primary data source.
|
|
852
|
+
if (targetId.startsWith(`${DefaultPolicyArbiter._TWIN_JSONPATH}:`)) {
|
|
853
|
+
const rest = targetId.slice(DefaultPolicyArbiter._TWIN_JSONPATH.length + 1);
|
|
854
|
+
const matchingKey = Object.keys(dataSources).find(k => rest.startsWith(`${k}:`));
|
|
855
|
+
const sourceKey = matchingKey ?? DefaultPolicyArbiter._DATA_SOURCE_KEY;
|
|
856
|
+
const expression = matchingKey ? rest.slice(matchingKey.length + 1) : rest;
|
|
857
|
+
return this.resolveDataSourceByKey(sourceKey, expression, dataSources, resolveValue);
|
|
858
|
+
}
|
|
859
|
+
// Handle <datasource>:<expression> format
|
|
860
|
+
const matchingKey = Object.keys(dataSources).find(k => targetId.startsWith(`${k}:`));
|
|
861
|
+
if (matchingKey) {
|
|
862
|
+
const expression = targetId.slice(matchingKey.length + 1);
|
|
863
|
+
return this.resolveDataSourceByKey(matchingKey, expression, dataSources, resolveValue);
|
|
864
|
+
}
|
|
865
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
|
|
866
|
+
target: targetId
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Resolve a datasource by key and evaluate a JSONPath expression against it.
|
|
871
|
+
* @param sourceKey The datasource key.
|
|
872
|
+
* @param expression The JSONPath expression.
|
|
873
|
+
* @param dataSources The available datasources.
|
|
874
|
+
* @param resolveValue Whether to evaluate the expression and return the matched value.
|
|
875
|
+
* @returns The resolved source, target expression, and optionally the matched value.
|
|
876
|
+
* @internal
|
|
877
|
+
*/
|
|
878
|
+
resolveDataSourceByKey(sourceKey, expression, dataSources, resolveValue = false) {
|
|
879
|
+
if (!Is.stringValue(expression)) {
|
|
880
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
|
|
881
|
+
operand: "target"
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (!expression.startsWith("$")) {
|
|
885
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
|
|
886
|
+
target: `${sourceKey}:${expression}`
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
const source = dataSources[sourceKey];
|
|
890
|
+
if (!resolveValue) {
|
|
891
|
+
return { source, target: expression };
|
|
892
|
+
}
|
|
893
|
+
const matches = JsonPathHelper.query(expression, source);
|
|
894
|
+
if (matches.length === 0) {
|
|
895
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
|
|
896
|
+
target: `${sourceKey}:${expression}`
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
return {
|
|
900
|
+
source,
|
|
901
|
+
target: expression,
|
|
902
|
+
value: matches.length === 1 ? matches[0].value : matches.map(m => m.value)
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Extract the target id from the permission.
|
|
907
|
+
* @param target The permission target.
|
|
908
|
+
* @returns The information key, or undefined when the target is not an information reference.
|
|
909
|
+
* @internal
|
|
910
|
+
*/
|
|
911
|
+
getTargetId(target) {
|
|
912
|
+
if (Is.undefined(target)) {
|
|
913
|
+
return undefined;
|
|
914
|
+
}
|
|
915
|
+
if (Is.array(target) && target.length > 1) {
|
|
916
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "multipleTargetsNotSupported");
|
|
917
|
+
}
|
|
918
|
+
const arr = ArrayHelper.fromObjectOrArray(target ?? []);
|
|
919
|
+
if (arr.length === 0) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
const first = arr[0];
|
|
923
|
+
if (Is.string(first)) {
|
|
924
|
+
return first;
|
|
925
|
+
}
|
|
926
|
+
if (this.isTwinJsonPathTarget(first)) {
|
|
927
|
+
const t = first;
|
|
928
|
+
const expression = t[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
|
|
929
|
+
if (!Is.stringValue(expression)) {
|
|
930
|
+
return undefined;
|
|
931
|
+
}
|
|
932
|
+
const dataSource = t[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
|
|
933
|
+
const sourceKey = Is.stringValue(dataSource)
|
|
934
|
+
? dataSource
|
|
935
|
+
: DefaultPolicyArbiter._DATA_SOURCE_KEY;
|
|
936
|
+
return `${sourceKey}:${expression}`;
|
|
937
|
+
}
|
|
938
|
+
return OdrlPolicyHelper.getUid(first);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Resolve the target identifier used for rule data context lookup.
|
|
942
|
+
* For AssetCollection targets the `source` property is used because the collection has no `uid`.
|
|
943
|
+
* Falls back to `getTargetId` for all other target forms.
|
|
944
|
+
* @param target The rule target field value.
|
|
945
|
+
* @returns The prefixed target string for data context resolution.
|
|
946
|
+
* @internal
|
|
947
|
+
*/
|
|
948
|
+
getRuleDataContextTargetId(target) {
|
|
949
|
+
const arr = ArrayHelper.fromObjectOrArray(target ?? []);
|
|
950
|
+
if (arr.length === 1) {
|
|
951
|
+
const firstTarget = arr[0];
|
|
952
|
+
if (Is.object(firstTarget) &&
|
|
953
|
+
OdrlPolicyHelper.getType(firstTarget) === OdrlTypes.AssetCollection &&
|
|
954
|
+
firstTarget.source === DefaultPolicyArbiter._TWIN_JSONPATH) {
|
|
955
|
+
const ctx = firstTarget;
|
|
956
|
+
const expression = ctx[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
|
|
957
|
+
if (Is.stringValue(expression)) {
|
|
958
|
+
const dataSource = ctx[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
|
|
959
|
+
const sourceKey = Is.stringValue(dataSource)
|
|
960
|
+
? dataSource
|
|
961
|
+
: DefaultPolicyArbiter._DATA_SOURCE_KEY;
|
|
962
|
+
return `${DefaultPolicyArbiter._TWIN_JSONPATH}:${sourceKey}:${expression}`;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return this.getTargetId(target);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Build a concrete prefixed target id for rule data-context lookup.
|
|
970
|
+
* @param baseTargetId The original prefixed target id from the rule.
|
|
971
|
+
* @param decisionTarget The concrete decision JSONPath target.
|
|
972
|
+
* @returns The concrete prefixed target id.
|
|
973
|
+
* @internal
|
|
974
|
+
*/
|
|
975
|
+
buildRuleDataContextTargetId(baseTargetId, decisionTarget) {
|
|
976
|
+
if (!Is.stringValue(decisionTarget) || decisionTarget === "$") {
|
|
977
|
+
return baseTargetId;
|
|
978
|
+
}
|
|
979
|
+
if (!Is.stringValue(baseTargetId)) {
|
|
980
|
+
return undefined;
|
|
981
|
+
}
|
|
982
|
+
const pathStartIndex = baseTargetId.indexOf(":$");
|
|
983
|
+
if (pathStartIndex < 0) {
|
|
984
|
+
return baseTargetId;
|
|
985
|
+
}
|
|
986
|
+
return `${baseTargetId.slice(0, pathStartIndex)}:${decisionTarget}`;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Resolve a rule target into a policy-decision JSONPath target and extracted refinements.
|
|
990
|
+
* For AssetCollection targets, `source` is treated as the decision target and `refinement`
|
|
991
|
+
* constraints are applied as additional rule constraints.
|
|
992
|
+
* @param rule The rule to resolve the target for.
|
|
993
|
+
* @returns The decision target and target refinements.
|
|
994
|
+
* @throws GeneralError if target is invalid or unsupported.
|
|
995
|
+
* @internal
|
|
996
|
+
*/
|
|
997
|
+
resolveRuleTarget(rule, dataSources) {
|
|
998
|
+
const arr = ArrayHelper.fromObjectOrArray(rule.target ?? []);
|
|
999
|
+
if (arr.length === 0) {
|
|
1000
|
+
return {
|
|
1001
|
+
target: "$",
|
|
1002
|
+
refinements: []
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (arr.length > 1) {
|
|
1006
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "multipleTargetsNotSupported");
|
|
1007
|
+
}
|
|
1008
|
+
const firstTarget = arr[0];
|
|
1009
|
+
if (this.isTwinJsonPathTarget(firstTarget)) {
|
|
1010
|
+
const t = firstTarget;
|
|
1011
|
+
const resolved = this.resolveDataSourceByKey(t[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE] ?? DefaultPolicyArbiter._DATA_SOURCE_KEY, t[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION], dataSources);
|
|
1012
|
+
return {
|
|
1013
|
+
target: resolved.target,
|
|
1014
|
+
refinements: []
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
if (Is.object(firstTarget)) {
|
|
1018
|
+
// Guard against unsupported ODRL asset properties
|
|
1019
|
+
if (Is.notEmpty(firstTarget.hasPolicy)) {
|
|
1020
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetHasPolicyNotSupported");
|
|
1021
|
+
}
|
|
1022
|
+
if (Is.notEmpty(firstTarget.partOf)) {
|
|
1023
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetPartOfNotSupported");
|
|
1024
|
+
}
|
|
1025
|
+
if (Is.object(firstTarget) &&
|
|
1026
|
+
OdrlPolicyHelper.getType(firstTarget) === OdrlTypes.AssetCollection) {
|
|
1027
|
+
if (firstTarget.source !== DefaultPolicyArbiter._TWIN_JSONPATH) {
|
|
1028
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetCollectionSourceNotSupported", {
|
|
1029
|
+
source: firstTarget.source ?? ""
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
const ctx = firstTarget;
|
|
1033
|
+
const dataSource = ctx[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
|
|
1034
|
+
const sourceKey = Is.stringValue(dataSource)
|
|
1035
|
+
? dataSource
|
|
1036
|
+
: DefaultPolicyArbiter._DATA_SOURCE_KEY;
|
|
1037
|
+
let sourceLookup;
|
|
1038
|
+
try {
|
|
1039
|
+
sourceLookup = this.resolveDataSourceByKey(sourceKey, ctx[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION], dataSources);
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetCollectionSourceNotSupported", {
|
|
1043
|
+
source: ctx[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION] ?? ""
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
target: sourceLookup.target,
|
|
1048
|
+
refinements: ArrayHelper.fromObjectOrArray(firstTarget.refinement ?? [])
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const targetId = Is.string(firstTarget) ? firstTarget : OdrlPolicyHelper.getUid(firstTarget);
|
|
1053
|
+
if (!Is.stringValue(targetId)) {
|
|
1054
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
|
|
1055
|
+
target: ""
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
const targetLookup = this.tryResolveTargetDataSource(targetId, dataSources);
|
|
1059
|
+
return {
|
|
1060
|
+
target: targetLookup.target,
|
|
1061
|
+
refinements: []
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Resolve decision targets for a rule.
|
|
1066
|
+
* AssetCollection wildcard targets with refinements are expanded to per-item targets.
|
|
1067
|
+
* @param rule The rule being evaluated.
|
|
1068
|
+
* @param dataSources The operand lookup sources.
|
|
1069
|
+
* @returns The decision targets and scoped refinements.
|
|
1070
|
+
* @internal
|
|
1071
|
+
*/
|
|
1072
|
+
resolveRuleDecisionTargets(rule, dataSources) {
|
|
1073
|
+
const resolvedTarget = this.resolveRuleTarget(rule, dataSources);
|
|
1074
|
+
if (!this.shouldExpandToPerItemTargets(rule, resolvedTarget)) {
|
|
1075
|
+
return [resolvedTarget];
|
|
1076
|
+
}
|
|
1077
|
+
const sourceLookup = this.tryResolveTargetDataSource(this.getRuleDataContextTargetId(rule.target), dataSources);
|
|
1078
|
+
const matches = JsonPathHelper.query(sourceLookup.target, sourceLookup.source);
|
|
1079
|
+
if (matches.length === 0) {
|
|
1080
|
+
return [resolvedTarget];
|
|
1081
|
+
}
|
|
1082
|
+
return matches.map(match => {
|
|
1083
|
+
const itemTarget = this.normalizeDecisionTargetPath(match.path ?? resolvedTarget.target);
|
|
1084
|
+
return {
|
|
1085
|
+
target: itemTarget,
|
|
1086
|
+
refinements: resolvedTarget.refinements.map(refinement => this.rewriteRefinementForDecisionTarget(refinement, resolvedTarget.target, itemTarget))
|
|
1087
|
+
};
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Determine if a rule should be expanded to per-item targets.
|
|
1092
|
+
* @param rule The rule.
|
|
1093
|
+
* @param resolvedTarget The resolved target details.
|
|
1094
|
+
* @returns True if the rule should emit per-item decisions.
|
|
1095
|
+
* @internal
|
|
1096
|
+
*/
|
|
1097
|
+
shouldExpandToPerItemTargets(rule, resolvedTarget) {
|
|
1098
|
+
if (resolvedTarget.refinements.length === 0 || !resolvedTarget.target.includes("[*]")) {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
const targets = ArrayHelper.fromObjectOrArray(rule.target ?? []);
|
|
1102
|
+
if (targets.length !== 1 || !Is.object(targets[0])) {
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
return OdrlPolicyHelper.getType(targets[0]) === OdrlTypes.AssetCollection;
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Rewrite a refinement so wildcard paths are scoped to a concrete item target.
|
|
1109
|
+
* @param refinement The refinement to rewrite.
|
|
1110
|
+
* @param sourceTarget The wildcard source target.
|
|
1111
|
+
* @param itemTarget The concrete item target.
|
|
1112
|
+
* @returns The rewritten refinement.
|
|
1113
|
+
* @internal
|
|
1114
|
+
*/
|
|
1115
|
+
rewriteRefinementForDecisionTarget(refinement, sourceTarget, itemTarget) {
|
|
1116
|
+
const logicalConstraint = this.getLogicalConstraintOperands(refinement);
|
|
1117
|
+
if (logicalConstraint) {
|
|
1118
|
+
return {
|
|
1119
|
+
...refinement,
|
|
1120
|
+
[logicalConstraint.operator]: logicalConstraint.constraints.map(item => this.rewriteRefinementForDecisionTarget(item, sourceTarget, itemTarget))
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
const regularConstraint = refinement;
|
|
1124
|
+
const constraintWithExtensions = regularConstraint;
|
|
1125
|
+
const canonicalExpression = constraintWithExtensions[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
|
|
1126
|
+
const rewrittenConstraint = {
|
|
1127
|
+
...regularConstraint,
|
|
1128
|
+
leftOperand: this.rewriteOperandForDecisionTarget(regularConstraint.leftOperand, sourceTarget, itemTarget),
|
|
1129
|
+
rightOperand: this.rewriteOperandForDecisionTarget(regularConstraint.rightOperand, sourceTarget, itemTarget)
|
|
1130
|
+
};
|
|
1131
|
+
if (Is.stringValue(canonicalExpression)) {
|
|
1132
|
+
rewrittenConstraint[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION] =
|
|
1133
|
+
this.rewriteWildcardPath(canonicalExpression, sourceTarget, itemTarget);
|
|
1134
|
+
}
|
|
1135
|
+
return rewrittenConstraint;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Rewrite JSONPath-based operands from wildcard source to concrete item target.
|
|
1139
|
+
* @param operand The operand to rewrite.
|
|
1140
|
+
* @param sourceTarget The wildcard source target.
|
|
1141
|
+
* @param itemTarget The concrete item target.
|
|
1142
|
+
* @returns The rewritten operand.
|
|
1143
|
+
* @internal
|
|
1144
|
+
*/
|
|
1145
|
+
rewriteOperandForDecisionTarget(operand, sourceTarget, itemTarget) {
|
|
1146
|
+
if (Is.object(operand)) {
|
|
1147
|
+
const typedOperand = {
|
|
1148
|
+
...operand
|
|
1149
|
+
};
|
|
1150
|
+
if (this.isTwinJsonPathOperandType(typedOperand["@type"])) {
|
|
1151
|
+
if (Is.stringValue(typedOperand["@value"])) {
|
|
1152
|
+
typedOperand["@value"] = this.rewriteWildcardPath(typedOperand["@value"], sourceTarget, itemTarget);
|
|
1153
|
+
}
|
|
1154
|
+
const canonicalExpression = typedOperand[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
|
|
1155
|
+
if (Is.stringValue(canonicalExpression)) {
|
|
1156
|
+
typedOperand[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION] = this.rewriteWildcardPath(canonicalExpression, sourceTarget, itemTarget);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return typedOperand;
|
|
1160
|
+
}
|
|
1161
|
+
// Legacy string form: "twin:information:$.foo". Extract the prefix, rewrite the
|
|
1162
|
+
// JSONPath expression to scope to the per-item target, then re-assemble. Without
|
|
1163
|
+
// this, the leftOperand stays at the wildcard
|
|
1164
|
+
// `$.itemList.itemListElement[*].unloadingLocation.id` for every item, returning
|
|
1165
|
+
// the full array of all ids per check — so all items pass the equality test.
|
|
1166
|
+
if (Is.stringValue(operand) &&
|
|
1167
|
+
operand.startsWith(`twin:${DefaultPolicyArbiter._INFORMATION_SOURCE_KEY}:`)) {
|
|
1168
|
+
const prefix = `twin:${DefaultPolicyArbiter._INFORMATION_SOURCE_KEY}:`;
|
|
1169
|
+
const expression = operand.slice(prefix.length);
|
|
1170
|
+
const rewritten = this.rewriteWildcardPath(expression, sourceTarget, itemTarget);
|
|
1171
|
+
return `${prefix}${rewritten}`;
|
|
1172
|
+
}
|
|
1173
|
+
return operand;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Rewrite wildcard source JSONPath segments to a concrete item JSONPath.
|
|
1177
|
+
* @param valuePath The operand path.
|
|
1178
|
+
* @param sourceTarget The wildcard source path.
|
|
1179
|
+
* @param itemTarget The concrete item path.
|
|
1180
|
+
* @returns The rewritten path.
|
|
1181
|
+
* @internal
|
|
1182
|
+
*/
|
|
1183
|
+
rewriteWildcardPath(valuePath, sourceTarget, itemTarget) {
|
|
1184
|
+
if (!sourceTarget.includes("[*]") || !valuePath.includes("[*]")) {
|
|
1185
|
+
return valuePath;
|
|
1186
|
+
}
|
|
1187
|
+
if (valuePath.startsWith(sourceTarget)) {
|
|
1188
|
+
return `${itemTarget}${valuePath.slice(sourceTarget.length)}`;
|
|
1189
|
+
}
|
|
1190
|
+
return valuePath;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Normalize JSONPath strings to dot notation for stable decision targets.
|
|
1194
|
+
* @param path The JSONPath to normalize.
|
|
1195
|
+
* @returns The normalized path.
|
|
1196
|
+
* @internal
|
|
1197
|
+
*/
|
|
1198
|
+
normalizeDecisionTargetPath(path) {
|
|
1199
|
+
return path.replace(/\['([^']+)']/g, ".$1");
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Evaluate a single ODRL constraint against the available context.
|
|
1203
|
+
* Supports logical constraint composition through nested refinements.
|
|
1204
|
+
* @param constraint The constraint to evaluate.
|
|
1205
|
+
* @param dataSources The operand lookup sources.
|
|
1206
|
+
* @returns True if the constraint is satisfied.
|
|
1207
|
+
* @internal
|
|
1208
|
+
*/
|
|
1209
|
+
evaluateConstraint(constraint, dataSources) {
|
|
1210
|
+
const logicalConstraint = this.getLogicalConstraintOperands(constraint);
|
|
1211
|
+
if (logicalConstraint) {
|
|
1212
|
+
return this.evaluateLogicalConstraint(logicalConstraint, dataSources);
|
|
1213
|
+
}
|
|
1214
|
+
// Must be a regular constraint beyond this point
|
|
1215
|
+
const regularConstraint = constraint;
|
|
1216
|
+
// rightOperandReference is not supported — it requires an external IRI lookup that
|
|
1217
|
+
// is outside the scope of the local evaluation engine.
|
|
1218
|
+
if (Is.notEmpty(regularConstraint.rightOperandReference)) {
|
|
1219
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "rightOperandReferenceNotSupported");
|
|
1220
|
+
}
|
|
1221
|
+
// dataType specifies how the rightOperand value should be coerced before comparison.
|
|
1222
|
+
// Without dataType-aware coercion logic the comparison may produce incorrect results.
|
|
1223
|
+
if (Is.notEmpty(regularConstraint.dataType)) {
|
|
1224
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintDataTypeNotSupported");
|
|
1225
|
+
}
|
|
1226
|
+
// unit specifies the measurement unit for the right operand (e.g. currency, length).
|
|
1227
|
+
// Unit-aware comparison is not implemented.
|
|
1228
|
+
if (Is.notEmpty(regularConstraint.unit)) {
|
|
1229
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintUnitNotSupported");
|
|
1230
|
+
}
|
|
1231
|
+
// status represents a state-based evaluation operand (e.g. odrl:policyUsage).
|
|
1232
|
+
// State-based evaluation is not implemented.
|
|
1233
|
+
if (Is.notEmpty(regularConstraint.status)) {
|
|
1234
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintStatusNotSupported");
|
|
1235
|
+
}
|
|
1236
|
+
// Evaluate the main constraint condition
|
|
1237
|
+
const leftValue = this.calculateOperandValue(regularConstraint.leftOperand, dataSources, regularConstraint);
|
|
1238
|
+
const rightValue = this.calculateOperandValue(regularConstraint.rightOperand, dataSources, regularConstraint);
|
|
1239
|
+
const mainSatisfied = this.evaluateOperator(regularConstraint.operator, leftValue, rightValue);
|
|
1240
|
+
// If main constraint is not satisfied, the overall constraint fails
|
|
1241
|
+
return mainSatisfied;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Extract logical constraint operands when present.
|
|
1245
|
+
* @param constraint The constraint to inspect.
|
|
1246
|
+
* @returns The logical operator and its operands, or undefined when not logical.
|
|
1247
|
+
* @throws GeneralError if logical constraint operands are not unique.
|
|
1248
|
+
* @internal
|
|
1249
|
+
*/
|
|
1250
|
+
getLogicalConstraintOperands(constraint) {
|
|
1251
|
+
const logicalConstraint = constraint;
|
|
1252
|
+
const operators = Object.values(OdrlLogicalConstraintType);
|
|
1253
|
+
for (const operator of operators) {
|
|
1254
|
+
const value = logicalConstraint[operator];
|
|
1255
|
+
if (!Is.undefined(value)) {
|
|
1256
|
+
const constraints = this.normalizeLogicalConstraintOperands(value);
|
|
1257
|
+
this.validateLogicalConstraintOperandUniqueness(constraints, operator);
|
|
1258
|
+
return {
|
|
1259
|
+
operator,
|
|
1260
|
+
constraints
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return undefined;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Normalize logical constraint operands into a constraint array.
|
|
1268
|
+
* Handles both IOdrlConstraint and IOdrlLogicalConstraint types.
|
|
1269
|
+
* @param raw The raw operand value.
|
|
1270
|
+
* @returns The constraint array.
|
|
1271
|
+
* @internal
|
|
1272
|
+
*/
|
|
1273
|
+
normalizeLogicalConstraintOperands(raw) {
|
|
1274
|
+
let normalized = raw;
|
|
1275
|
+
if (Is.object(normalized) &&
|
|
1276
|
+
!Is.undefined(normalized["@list"])) {
|
|
1277
|
+
normalized = normalized["@list"];
|
|
1278
|
+
}
|
|
1279
|
+
return (ArrayHelper.fromObjectOrArray(normalized) ?? [])
|
|
1280
|
+
.filter(item => Is.object(item))
|
|
1281
|
+
.map(item => item);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Validate that all operands in a logical constraint are unique.
|
|
1285
|
+
* ODRL spec 2.5.2 requires that all operand values MUST be unique Constraint instances.
|
|
1286
|
+
* Uniqueness is checked by uid property and id property.
|
|
1287
|
+
* @param constraints The constraint operands to validate.
|
|
1288
|
+
* @param operator The logical operator type (for error messaging).
|
|
1289
|
+
* @throws GeneralError if duplicate constraints are found.
|
|
1290
|
+
* @internal
|
|
1291
|
+
*/
|
|
1292
|
+
validateLogicalConstraintOperandUniqueness(constraints, operator) {
|
|
1293
|
+
const seenIdentifiers = new Set();
|
|
1294
|
+
for (let i = 0; i < constraints.length; i++) {
|
|
1295
|
+
const constraint = constraints[i];
|
|
1296
|
+
const identifier = OdrlPolicyHelper.getUid(constraint);
|
|
1297
|
+
// If we have an identifier, check for duplicates
|
|
1298
|
+
if (Is.stringValue(identifier)) {
|
|
1299
|
+
if (seenIdentifiers.has(identifier)) {
|
|
1300
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "logicalConstraintOperandNotUnique", {
|
|
1301
|
+
operator,
|
|
1302
|
+
identifier,
|
|
1303
|
+
index: i
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
seenIdentifiers.add(identifier);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Evaluate a logical constraint operator against its operands.
|
|
1312
|
+
* @param logicalConstraint The operator and operand list.
|
|
1313
|
+
* @param dataSources The operand lookup sources.
|
|
1314
|
+
* @returns True if the logical constraint is satisfied.
|
|
1315
|
+
* @internal
|
|
1316
|
+
*/
|
|
1317
|
+
evaluateLogicalConstraint(logicalConstraint, dataSources) {
|
|
1318
|
+
const { operator, constraints } = logicalConstraint;
|
|
1319
|
+
if (constraints.length === 0) {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
switch (operator) {
|
|
1323
|
+
case OdrlLogicalConstraintType.And:
|
|
1324
|
+
return constraints.every(item => this.evaluateConstraint(item, dataSources));
|
|
1325
|
+
case OdrlLogicalConstraintType.AndSequence: {
|
|
1326
|
+
for (const item of constraints) {
|
|
1327
|
+
if (!this.evaluateConstraint(item, dataSources)) {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
case OdrlLogicalConstraintType.Or:
|
|
1334
|
+
return constraints.some(item => this.evaluateConstraint(item, dataSources));
|
|
1335
|
+
case OdrlLogicalConstraintType.Xone: {
|
|
1336
|
+
let satisfied = 0;
|
|
1337
|
+
for (const item of constraints) {
|
|
1338
|
+
if (this.evaluateConstraint(item, dataSources)) {
|
|
1339
|
+
satisfied += 1;
|
|
1340
|
+
if (satisfied > 1) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return satisfied === 1;
|
|
1346
|
+
}
|
|
1347
|
+
default:
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Resolve a prefixed operand to its source object and JSONPath expression.
|
|
1353
|
+
* Prefix matching is dictionary-driven so additional operand namespaces can be
|
|
1354
|
+
* added in one place by extending the lookup sources map.
|
|
1355
|
+
* @param operandTypeOrValue The string operand value or typed operand namespace.
|
|
1356
|
+
* @param operandValue The JSONPath expression.
|
|
1357
|
+
* @param dataSources The available lookup sources.
|
|
1358
|
+
* @returns The resolved source and JSONPath, or undefined when not namespaced.
|
|
1359
|
+
* @throws GeneralError if a twin: prefixed operand doesn't resolve to any available datasource key.
|
|
1360
|
+
* @internal
|
|
1361
|
+
*/
|
|
1362
|
+
tryResolveOperandLookup(operandTypeOrValue, operandValue, dataSources) {
|
|
1363
|
+
let sourceKey = this.normalizeTwinJsonPathOperandAlias(operandTypeOrValue);
|
|
1364
|
+
let value = operandValue;
|
|
1365
|
+
// Combined form: operandTypeOrValue is a full "<prefix>:<expression>" string
|
|
1366
|
+
// like "twin:jsonPath:$.foo" rather than the canonical separation of @type +
|
|
1367
|
+
// @value/expression. Detect by walking the registered datasource keys for a prefix
|
|
1368
|
+
// match; on hit, split into key + expression so resolveDataSourceByKey can evaluate.
|
|
1369
|
+
if (!dataSources[sourceKey]) {
|
|
1370
|
+
const matchingKey = Object.keys(dataSources).find(k => sourceKey.startsWith(`${k}:`));
|
|
1371
|
+
if (matchingKey) {
|
|
1372
|
+
value = sourceKey.slice(matchingKey.length + 1);
|
|
1373
|
+
sourceKey = matchingKey;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (!dataSources[sourceKey] || !Is.stringValue(value)) {
|
|
1377
|
+
return undefined;
|
|
1378
|
+
}
|
|
1379
|
+
const resolved = this.resolveDataSourceByKey(sourceKey, value, dataSources);
|
|
1380
|
+
return { source: resolved.source, jsonPath: resolved.target };
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Calculate an operand value.
|
|
1384
|
+
* @param operand The operand.
|
|
1385
|
+
* @param dataSources The available prefixed operand sources.
|
|
1386
|
+
* @returns The resolved operand value.
|
|
1387
|
+
* @internal
|
|
1388
|
+
*/
|
|
1389
|
+
calculateOperandValue(operand, dataSources, constraint) {
|
|
1390
|
+
let jsonPath;
|
|
1391
|
+
let operandRoot;
|
|
1392
|
+
if (Is.stringValue(operand)) {
|
|
1393
|
+
const expression = this.extractJsonPathExpressionFromConstraint(constraint, operand);
|
|
1394
|
+
let resolvedOperand = operand;
|
|
1395
|
+
if (operand === DefaultPolicyArbiter._TWIN_JSONPATH &&
|
|
1396
|
+
Is.object(constraint)) {
|
|
1397
|
+
const dataSourceOverride = constraint[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
|
|
1398
|
+
if (Is.stringValue(dataSourceOverride)) {
|
|
1399
|
+
resolvedOperand = dataSourceOverride;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const lookup = this.tryResolveOperandLookup(resolvedOperand, expression ?? operand, dataSources);
|
|
1403
|
+
if (lookup) {
|
|
1404
|
+
jsonPath = lookup.jsonPath;
|
|
1405
|
+
operandRoot = lookup.source;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
else if (Is.object(operand)) {
|
|
1409
|
+
const typedOperand = operand;
|
|
1410
|
+
// Is this an object { "@value": "18", "@type": "xsd:integer" } ?
|
|
1411
|
+
const value = this.extractJsonPathExpressionFromTypedOperand(typedOperand) ??
|
|
1412
|
+
typedOperand["@value"];
|
|
1413
|
+
const type = typedOperand["@type"];
|
|
1414
|
+
if (Is.stringValue(type)) {
|
|
1415
|
+
let resolvedType = type;
|
|
1416
|
+
if (this.isTwinJsonPathOperandType(type)) {
|
|
1417
|
+
const dataSourceOverride = typedOperand[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
|
|
1418
|
+
if (Is.stringValue(dataSourceOverride)) {
|
|
1419
|
+
resolvedType = dataSourceOverride;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const lookup = this.tryResolveOperandLookup(resolvedType, value, dataSources);
|
|
1423
|
+
if (lookup) {
|
|
1424
|
+
jsonPath = lookup.jsonPath;
|
|
1425
|
+
operandRoot = lookup.source;
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
const xsdValue = this.coerceXsdType(value, type);
|
|
1429
|
+
if (!Is.undefined(xsdValue)) {
|
|
1430
|
+
return xsdValue;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
// We have a JSON Path to resolve
|
|
1436
|
+
if (Is.stringValue(jsonPath)) {
|
|
1437
|
+
const jsonPaths = JsonPathHelper.query(jsonPath, operandRoot);
|
|
1438
|
+
if (jsonPaths.length === 0) {
|
|
1439
|
+
// No matches
|
|
1440
|
+
return undefined;
|
|
1441
|
+
}
|
|
1442
|
+
else if (jsonPaths.length === 1) {
|
|
1443
|
+
// Single match - return the value directly
|
|
1444
|
+
return jsonPaths[0].value;
|
|
1445
|
+
}
|
|
1446
|
+
// Multiple matches - return array of values
|
|
1447
|
+
return jsonPaths.map(p => p.value);
|
|
1448
|
+
}
|
|
1449
|
+
// Not JSON Path or object value so return as is
|
|
1450
|
+
return operand;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Determine if a target object uses the canonical twin:jsonPath object format.
|
|
1454
|
+
* @param target The target value.
|
|
1455
|
+
* @returns True if the target is a canonical twin:jsonPath object.
|
|
1456
|
+
* @internal
|
|
1457
|
+
*/
|
|
1458
|
+
isTwinJsonPathTarget(target) {
|
|
1459
|
+
return Is.object(target) && this.isTwinJsonPathOperandType(OdrlPolicyHelper.getType(target));
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Determine if the operand type is a jsonPath namespace.
|
|
1463
|
+
* @param type The operand type.
|
|
1464
|
+
* @returns True if the type is twin:jsonPath.
|
|
1465
|
+
* @internal
|
|
1466
|
+
*/
|
|
1467
|
+
isTwinJsonPathOperandType(type) {
|
|
1468
|
+
return type === DefaultPolicyArbiter._TWIN_JSONPATH;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Normalize canonical jsonPath alias to the legacy jsonpath namespace key.
|
|
1472
|
+
* @param operandTypeOrValue The raw operand type or value.
|
|
1473
|
+
* @returns The normalized operand type/value.
|
|
1474
|
+
* @internal
|
|
1475
|
+
*/
|
|
1476
|
+
normalizeTwinJsonPathOperandAlias(operandTypeOrValue) {
|
|
1477
|
+
if (operandTypeOrValue === DefaultPolicyArbiter._TWIN_JSONPATH ||
|
|
1478
|
+
operandTypeOrValue.startsWith(`${DefaultPolicyArbiter._TWIN_JSONPATH}:`)) {
|
|
1479
|
+
const suffix = operandTypeOrValue.slice(DefaultPolicyArbiter._TWIN_JSONPATH.length);
|
|
1480
|
+
return `${DefaultPolicyArbiter._DATA_SOURCE_KEY}${suffix}`;
|
|
1481
|
+
}
|
|
1482
|
+
return operandTypeOrValue;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Extract canonical jsonPath expression from typed operand object.
|
|
1486
|
+
* @param operand The typed operand.
|
|
1487
|
+
* @returns The jsonPath expression if present.
|
|
1488
|
+
* @internal
|
|
1489
|
+
*/
|
|
1490
|
+
extractJsonPathExpressionFromTypedOperand(operand) {
|
|
1491
|
+
if (!this.isTwinJsonPathOperandType(operand["@type"])) {
|
|
1492
|
+
return undefined;
|
|
1493
|
+
}
|
|
1494
|
+
const expression = operand[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
|
|
1495
|
+
if (Is.stringValue(expression)) {
|
|
1496
|
+
return expression;
|
|
1497
|
+
}
|
|
1498
|
+
const type = operand["@type"];
|
|
1499
|
+
if (type === DefaultPolicyArbiter._TWIN_JSONPATH) {
|
|
1500
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
|
|
1501
|
+
operand: "rightOperand"
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Extract canonical jsonPath expression from constraint for leftOperand aliases.
|
|
1507
|
+
* @param constraint The constraint containing the left operand.
|
|
1508
|
+
* @param leftOperand The left operand.
|
|
1509
|
+
* @returns The expression if canonical form is used.
|
|
1510
|
+
* @internal
|
|
1511
|
+
*/
|
|
1512
|
+
extractJsonPathExpressionFromConstraint(constraint, leftOperand) {
|
|
1513
|
+
if (!Is.object(constraint)) {
|
|
1514
|
+
return undefined;
|
|
1515
|
+
}
|
|
1516
|
+
if (constraint.leftOperand !== leftOperand) {
|
|
1517
|
+
return undefined;
|
|
1518
|
+
}
|
|
1519
|
+
if (leftOperand.startsWith(`${DefaultPolicyArbiter._TWIN_JSONPATH}:`)) {
|
|
1520
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
|
|
1521
|
+
operand: "leftOperand"
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
if (leftOperand !== DefaultPolicyArbiter._TWIN_JSONPATH) {
|
|
1525
|
+
return undefined;
|
|
1526
|
+
}
|
|
1527
|
+
const expression = constraint[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
|
|
1528
|
+
if (Is.stringValue(expression)) {
|
|
1529
|
+
return expression;
|
|
1530
|
+
}
|
|
1531
|
+
throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
|
|
1532
|
+
operand: "leftOperand"
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Evaluate an ODRL operator against resolved operands.
|
|
1537
|
+
* @param operator The operator.
|
|
1538
|
+
* @param left The resolved left operand.
|
|
1539
|
+
* @param right The resolved right operand.
|
|
1540
|
+
* @returns True if the comparison is satisfied.
|
|
1541
|
+
* @internal
|
|
1542
|
+
*/
|
|
1543
|
+
evaluateOperator(operator, left, right) {
|
|
1544
|
+
// Handle array/collection left values (e.g. JSONPath returning multiple matches).
|
|
1545
|
+
const leftValues = ArrayHelper.fromObjectOrArray(left ?? []);
|
|
1546
|
+
switch (operator) {
|
|
1547
|
+
case OdrlOperatorType.Eq:
|
|
1548
|
+
return leftValues.some(v => ObjectHelper.equal(v, right, false));
|
|
1549
|
+
case OdrlOperatorType.Neq:
|
|
1550
|
+
return leftValues.every(v => !ObjectHelper.equal(v, right, false));
|
|
1551
|
+
case OdrlOperatorType.Gt:
|
|
1552
|
+
return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a > b));
|
|
1553
|
+
case OdrlOperatorType.Gteq:
|
|
1554
|
+
return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a >= b));
|
|
1555
|
+
case OdrlOperatorType.Lt:
|
|
1556
|
+
return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a < b));
|
|
1557
|
+
case OdrlOperatorType.Lteq:
|
|
1558
|
+
return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a <= b));
|
|
1559
|
+
case OdrlOperatorType.IsAnyOf: {
|
|
1560
|
+
return leftValues.some(v => {
|
|
1561
|
+
const stringValue = typeof v === "string" || typeof v === "number" || typeof v === "boolean"
|
|
1562
|
+
? String(v)
|
|
1563
|
+
: JSON.stringify(v);
|
|
1564
|
+
return (ArrayHelper.fromObjectOrArray(right) ?? []).includes(stringValue);
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
case OdrlOperatorType.IsAllOf: {
|
|
1568
|
+
return ObjectHelper.equal(leftValues, ArrayHelper.fromObjectOrArray(right) ?? [], false);
|
|
1569
|
+
}
|
|
1570
|
+
case OdrlOperatorType.IsNoneOf: {
|
|
1571
|
+
return leftValues.every(v => !(ArrayHelper.fromObjectOrArray(right) ?? []).includes(v));
|
|
1572
|
+
}
|
|
1573
|
+
case OdrlOperatorType.LocTimeEq:
|
|
1574
|
+
return leftValues.some(v => ObjectHelper.equal(v, right, false));
|
|
1575
|
+
case OdrlOperatorType.LocTimeGteq:
|
|
1576
|
+
return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a >= b));
|
|
1577
|
+
case OdrlOperatorType.IsA:
|
|
1578
|
+
case OdrlOperatorType.HasPart:
|
|
1579
|
+
case OdrlOperatorType.IsPartOf:
|
|
1580
|
+
// For now, treat these as simple equality/ordering semantics where meaningful.
|
|
1581
|
+
// Profiles can introduce richer semantics via additional arbiters.
|
|
1582
|
+
return leftValues.some(v => ObjectHelper.equal(v, right, false));
|
|
1583
|
+
default:
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Compare values with numeric/date/string coercion.
|
|
1589
|
+
* @param left The left value.
|
|
1590
|
+
* @param right The right value.
|
|
1591
|
+
* @param compare Comparison operator.
|
|
1592
|
+
* @returns True if ordered comparison passes.
|
|
1593
|
+
* @internal
|
|
1594
|
+
*/
|
|
1595
|
+
compareOrdered(left, right, compare) {
|
|
1596
|
+
const leftNum = Coerce.number(left);
|
|
1597
|
+
const rightNum = Coerce.number(right);
|
|
1598
|
+
if (!Is.undefined(leftNum) && !Is.undefined(rightNum)) {
|
|
1599
|
+
return compare(leftNum, rightNum);
|
|
1600
|
+
}
|
|
1601
|
+
const leftDate = Coerce.dateTime(left);
|
|
1602
|
+
const rightDate = Coerce.dateTime(right);
|
|
1603
|
+
if (!Is.undefined(leftDate) && !Is.undefined(rightDate)) {
|
|
1604
|
+
return compare(leftDate.getTime(), rightDate.getTime());
|
|
1605
|
+
}
|
|
1606
|
+
// Only use string ordering when both operands are actual strings.
|
|
1607
|
+
// Avoid coercing other types into strings, as that can cause
|
|
1608
|
+
// unintended comparisons like 18 >= "$.minAge" evaluating to true.
|
|
1609
|
+
if (Is.string(left) && Is.string(right)) {
|
|
1610
|
+
return compare(left.localeCompare(right), 0);
|
|
1611
|
+
}
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Coerce a value to a specific XSD type.
|
|
1616
|
+
* @param value The value to coerce.
|
|
1617
|
+
* @param type The XSD type.
|
|
1618
|
+
* @returns The coerced value, or undefined when coercion is not possible.
|
|
1619
|
+
* @internal
|
|
1620
|
+
*/
|
|
1621
|
+
coerceXsdType(value, type) {
|
|
1622
|
+
if ([
|
|
1623
|
+
"xsd:string",
|
|
1624
|
+
"xsd:normalizedString",
|
|
1625
|
+
"xsd:token",
|
|
1626
|
+
"xsd:anyURI",
|
|
1627
|
+
"xsd:QName",
|
|
1628
|
+
"xsd:NOTATION"
|
|
1629
|
+
].includes(type)) {
|
|
1630
|
+
// Handle standard xsd types
|
|
1631
|
+
return Coerce.string(value);
|
|
1632
|
+
}
|
|
1633
|
+
else if ([
|
|
1634
|
+
"xsd:integer",
|
|
1635
|
+
"xsd:decimal",
|
|
1636
|
+
"xsd:float",
|
|
1637
|
+
"xsd:double",
|
|
1638
|
+
"xsd:long",
|
|
1639
|
+
"xsd:int",
|
|
1640
|
+
"xsd:short",
|
|
1641
|
+
"xsd:byte"
|
|
1642
|
+
].includes(type)) {
|
|
1643
|
+
// Handle standard xsd types
|
|
1644
|
+
return Coerce.number(value);
|
|
1645
|
+
}
|
|
1646
|
+
else if (type === "xsd:boolean") {
|
|
1647
|
+
// Handle standard xsd types
|
|
1648
|
+
return Coerce.boolean(value);
|
|
1649
|
+
}
|
|
1650
|
+
else if (["xsd:date", "xsd:dateTime", "xsd:time"].includes(type)) {
|
|
1651
|
+
// Handle standard xsd types
|
|
1652
|
+
return Coerce.dateTime(value);
|
|
1653
|
+
}
|
|
1654
|
+
return undefined;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
//# sourceMappingURL=defaultPolicyArbiter.js.map
|