Skip to content

Commit 611deaa

Browse files
woutslabbinckjoachimvh
authored andcommitted
feat: clean up ODRL Authorizer
1 parent 929bd80 commit 611deaa

1 file changed

Lines changed: 122 additions & 88 deletions

File tree

packages/uma/src/policies/authorizers/OdrlAuthorizer.ts

Lines changed: 122 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createVocabulary, DC, getLoggerFor, RDF } from '@solid/community-server';
2-
import { ODRL, UconRequest, UCRulesStorage } from '@solidlab/ucp';
3-
import { generate_uuid } from 'koreografeye';
2+
import { basicPolicy, ODRL, UCPPolicy, UCRulesStorage } from '@solidlab/ucp';
43
import { DataFactory, Literal, NamedNode, Quad_Subject, Store } from 'n3';
54
import { ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator'
65
import { WEBID } from '../../credentials/Claims';
@@ -11,13 +10,30 @@ import { Authorizer } from './Authorizer';
1110

1211
const {quad, namedNode, literal} = DataFactory
1312

13+
/**
14+
* Permission evaluation is performed as follows:
15+
*
16+
* 1. Conversion of Permission queries to ODRL Requests.
17+
* - A translation is performed to transform CSS actions to ODRL actions.
18+
* - One ODRL Request per Action and target Resource.
19+
*
20+
* 2. ODRL Evaluator performs ODRL Evaluation
21+
* - No policy selection is performed (all policies are inserted rather than all relevant).
22+
* - No conflict resolution strategy is present (Prohibition policies are ignored).
23+
* - No duties are checked.
24+
*
25+
* 3. Conversion from ODRL Policy Compliance Reports to Permissions
26+
* - Selecting the ODRL actions from Active Permission Reports
27+
* - Translation from ODRL actions to CSS actions
28+
*/
1429
export class OdrlAuthorizer implements Authorizer {
1530
protected readonly logger = getLoggerFor(this);
1631
private readonly odrlEvaluator: ODRLEvaluator;
1732

1833
/**
1934
* Creates a OdrlAuthorizer enforcing policies using ODRL with the ODRL Evaluator.
2035
*
36+
*
2137
* @param policies - A store containing the ODRL policy rules.
2238
*/
2339
constructor(
@@ -35,79 +51,66 @@ export class OdrlAuthorizer implements Authorizer {
3551
return [];
3652
}
3753

38-
const requests: UconRequest[] = [];
39-
for (const {resource_id, resource_scopes} of query) {
40-
41-
if (!resource_id) {
42-
this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit resources.');
43-
continue;
44-
}
45-
46-
// ODRL can only handle odrl actions
47-
requests.push({
48-
subject: typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous',
49-
resource: resource_id,
50-
action: resource_scopes ? transformActionsCssToOdrl(resource_scopes) : ["http://www.w3.org/ns/odrl/2/use"],
51-
claims
52-
});
53-
}
54-
const permissions: Permission[] = await Promise.all(requests.map(
55-
async (request) => {
56-
const scopes_permitted = [];
57-
58-
// prepare sotw
59-
const sotw = new Store();
60-
61-
sotw.add(quad(namedNode('http://example.com/request/currentTime'), namedNode('http://purl.org/dc/terms/issued'), literal(new Date().toISOString(), namedNode("http://www.w3.org/2001/XMLSchema#dateTime"))));
62-
63-
// prepare policy
64-
const policyStore = (await this.policies.getStore())
54+
// key value store for building the permissions to be granted on a resource
55+
const grantedPermissions: { [key: string]: string[] } = {};
6556

57+
// prepare policy
58+
const policyStore = (await this.policies.getStore())
6659

67-
for (const action of request.action) {
68-
// prepare request
69-
const req = new Store();
70-
const requestNode = generate_uuid();
71-
const permissionNode = generate_uuid();
72-
req.add(quad(requestNode, RDF.terms.type, ODRL.terms.Request));
73-
req.add(quad(requestNode, ODRL.terms.permission, permissionNode));
60+
// prepare sotw
61+
const sotw = new Store();
62+
sotw.add(quad(namedNode('http://example.com/request/currentTime'), namedNode('http://purl.org/dc/terms/issued'), literal(new Date().toISOString(), namedNode("http://www.w3.org/2001/XMLSchema#dateTime"))));
7463

75-
req.add(quad(permissionNode, RDF.terms.type, ODRL.terms.Permission));
76-
req.add(quad(permissionNode, ODRL.terms.assignee, namedNode(request.subject)));
77-
req.add(quad(permissionNode, ODRL.terms.target, namedNode(request.resource)));
78-
req.add(quad(permissionNode, ODRL.terms.action, namedNode(action)));
64+
const subject = typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous';
7965

80-
// evaluate policies
81-
const reports = await this.odrlEvaluator.evaluate([...policyStore], [...req], [...sotw]);
82-
const reportStore = new Store(reports)
8366

84-
// TODO: handle multiple reports -> possible to be generated
85-
// NOTE: current strategy, add all actions of active reports generated by the request
86-
// fetch active and attempted
87-
// console.log(new Writer().quadsToString([...req]))
67+
for (const {resource_id, resource_scopes} of query) {
68+
if (!resource_id) {
69+
this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit resources.');
70+
continue;
71+
}
8872

89-
const PolicyReportNodes = reportStore.getSubjects(RDF.type, CR.PolicyReport, null);
90-
for (const policyReportNode of PolicyReportNodes) {
91-
const policyReport = parseComplianceReport(policyReportNode, reportStore)
92-
// console.log(new Writer().quadsToString([...policyStore]))
93-
// console.log(new Writer().quadsToString([...sotw]))
94-
// console.log(new Writer().quadsToString(reports))
95-
// console.log(policyReport)
96-
if (policyReport.ruleReport.activationState === ActivationState.Active &&
97-
policyReport.ruleReport.type === RuleReportType.PermissionReport) {
98-
scopes_permitted.push(action)
73+
grantedPermissions[resource_id] = [];
74+
const actions = resource_scopes ? transformActionsCssToOdrl(resource_scopes) : ["http://www.w3.org/ns/odrl/2/use"]
75+
for (const action of actions) {
76+
this.logger.info(`Evaluating Request [S R AR]: [${subject} ${resource_id} ${action}]`);
77+
const requestPolicy: UCPPolicy = {
78+
type: ODRL.Request,
79+
rules: [
80+
{
81+
action: action,
82+
resource: resource_id,
83+
requestingParty: subject
9984
}
100-
}
101-
85+
]
10286
}
103-
// extract allowed scopes
104-
return {
105-
resource_id: request.resource,
106-
resource_scopes: transformActionsOdrlToCss(scopes_permitted)
87+
const requestStore = basicPolicy(requestPolicy).representation
88+
// evaluate policies
89+
const reports = await this.odrlEvaluator.evaluate(
90+
[...policyStore],
91+
[...requestStore],
92+
[...sotw]);
93+
const reportStore = new Store(reports);
94+
95+
// TODO: handle multiple reports -> possible to be generated
96+
// NOTE: current strategy, add all actions of active reports generated by the request
97+
// fetch active and attempted
98+
const PolicyReportNodes = reportStore.getSubjects(RDF.type, CR.PolicyReport, null);
99+
for (const policyReportNode of PolicyReportNodes) {
100+
const policyReport = parseComplianceReport(policyReportNode, reportStore)
101+
if (policyReport.ruleReport[0].activationState === ActivationState.Active &&
102+
policyReport.ruleReport[0].type === RuleReportType.PermissionReport) {
103+
grantedPermissions[resource_id].push(action);
104+
}
107105
}
108106
}
109-
));
110-
// console.log(permissions)
107+
}
108+
const permissions: Permission[] = []
109+
Object.keys(grantedPermissions).forEach(
110+
resource_id => permissions.push({
111+
resource_id,
112+
resource_scopes: transformActionsOdrlToCss(grantedPermissions[resource_id])
113+
}) );
111114
return permissions;
112115
}
113116

@@ -125,14 +128,21 @@ scopeCssToOdrl.set('urn:example:css:modes:write','http://www.w3.org/ns/odrl/2/wr
125128

126129
const scopeOdrlToCss : Map<string, string> = new Map(Array.from(scopeCssToOdrl, entry => [entry[1], entry[0]]));
127130

131+
/**
132+
* Transform the Actions enforced by the Community Solid Server to equivalent ODRL Actions
133+
* @param actions
134+
*/
128135
function transformActionsCssToOdrl(actions: string[]): string[] {
129136
// scopes come from UmaClient.ts -> see CSS package
130137

131138
// in UMAPermissionReader, only the last part of the URN will be used, divided by a colon
132139
// again, see CSS package
133140
return actions.map(action => scopeCssToOdrl.get(action)!);
134141
}
135-
142+
/**
143+
* Transform ODRL Actions to equivalent Actions enforced by the Community Solid Server
144+
* @param actions
145+
*/
136146
function transformActionsOdrlToCss(actions: string[]): string[] {
137147
const cssActions = []
138148
for (const action of actions) {
@@ -149,13 +159,14 @@ type PolicyReport = {
149159
created: Literal;
150160
request: NamedNode;
151161
policy: NamedNode;
152-
ruleReport: RuleReport;
162+
ruleReport: RuleReport[];
153163
}
154164
type RuleReport = {
155165
id: NamedNode;
156166
type: RuleReportType;
157167
activationState: ActivationState
158-
// TODO: others
168+
rule: NamedNode;
169+
requestedRule: NamedNode;
159170
premiseReport: PremiseReport[]
160171
}
161172

@@ -189,35 +200,58 @@ enum ActivationState {
189200
Active= 'http://example.com/report/temp/Active',
190201
Inactive= 'http://example.com/report/temp/Inactive',
191202
}
192-
function parseComplianceReport(identifier: Quad_Subject, store: Store):PolicyReport{
193-
const exists = store.getQuads(identifier,RDF.type,CR.PolicyReport, null).length ===1;
203+
204+
/**
205+
* Parses an ODRL Compliance Report Model into a {@link PolicyReport}.
206+
* @param identifier
207+
* @param store
208+
*/
209+
function parseComplianceReport(identifier: Quad_Subject, store: Store): PolicyReport {
210+
const exists = store.getQuads(identifier,RDF.type,CR.PolicyReport, null).length === 1;
194211
if (!exists) { throw Error(`No Policy Report found with: ${identifier}.`); }
195-
const ruleReportNode = store.getQuads(identifier,CR.ruleReport, null, null)[0].object as NamedNode;
196-
const premises :PremiseReport[] = [];
197-
const premiseNodes = store.getObjects(ruleReportNode,CR.premiseReport, null) as NamedNode[];
198-
for (const premiseNode of premiseNodes) {
199-
premises.push({
200-
id: premiseNode,
201-
type: store.getObjects(premiseNode, RDF.type, null)[0].value as PremiseReportType,
202-
premiseReport:[], // Note: won't get nested premises this way
203-
satisfactionState: store.getObjects(premiseNode, CR.satisfactionState, null)[0].value as SatisfactionState
204-
})
205-
}
206-
const ruleReport :RuleReport = {
207-
id: ruleReportNode,
208-
type: store.getObjects(ruleReportNode, RDF.type, null)[0].value as RuleReportType,
209-
activationState: store.getObjects(ruleReportNode, CR.activationState, null)[0].value as ActivationState,
210-
premiseReport: premises
211-
}
212+
const ruleReportNodes = store.getObjects(identifier, CR.ruleReport, null) as NamedNode[];
213+
212214
return {
213215
id: identifier as NamedNode,
214216
created: store.getObjects(identifier, DC.namespace+"created", null)[0] as Literal,
215217
policy: store.getObjects(identifier, CR.policy, null)[0] as NamedNode,
216218
request: store.getObjects(identifier, CR.policyRequest, null)[0] as NamedNode,
217-
ruleReport: ruleReport
219+
ruleReport: ruleReportNodes.map(ruleReportNode => parseRuleReport(ruleReportNode, store))
218220
}
219221
}
220222

223+
/**
224+
* Parses Rule Reports from a Compliance Report, including its premises
225+
* @param identifier
226+
* @param store
227+
*/
228+
function parseRuleReport(identifier: Quad_Subject, store: Store): RuleReport {
229+
const premiseNodes = store.getObjects(identifier,CR.premiseReport, null) as NamedNode[];
230+
return {
231+
id: identifier as NamedNode,
232+
type: store.getObjects(identifier, RDF.type, null)[0].value as RuleReportType,
233+
activationState: store.getObjects(identifier, CR.activationState, null)[0].value as ActivationState,
234+
requestedRule: store.getObjects(identifier, CR.ruleRequest, null)[0] as NamedNode,
235+
rule: store.getObjects(identifier, CR.rule, null)[0] as NamedNode,
236+
premiseReport: premiseNodes.map((prem) => parsePremiseReport(prem, store))
237+
}
238+
}
239+
240+
/**
241+
* Parses Premise Reports, including premises of a Premise Report itself.
242+
* Note that if for some reason there are circular premise reports, this will result into an infinite loop
243+
* @param identifier
244+
* @param store
245+
*/
246+
function parsePremiseReport(identifier: Quad_Subject, store: Store): PremiseReport {
247+
const nestedPremises = store.getObjects(identifier, CR.PremiseReport, null) as NamedNode[];
248+
return {
249+
id: identifier as NamedNode,
250+
type: store.getObjects(identifier, RDF.type, null)[0].value as PremiseReportType,
251+
premiseReport: nestedPremises.map((prem) => parsePremiseReport(prem, store)),
252+
satisfactionState: store.getObjects(identifier, CR.satisfactionState, null)[0].value as SatisfactionState
253+
}
254+
}
221255
const CR = createVocabulary('http://example.com/report/temp/',
222256
'PolicyReport',
223257
'RuleReport',

0 commit comments

Comments
 (0)