Skip to content

Commit 30ac98a

Browse files
woutslabbinckjoachimvh
authored andcommitted
feat: introduce the prohibition over permission + default deny strategy in the ODRL authorizer
1 parent 5ed6150 commit 30ac98a

8 files changed

Lines changed: 374 additions & 197 deletions

File tree

packages/uma/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@
7373
"logform": "^2.6.0",
7474
"ms": "^2.1.3",
7575
"n3": "^1.17.2",
76-
"odrl-evaluator": "^0.5.0",
76+
"odrl-evaluator": "^0.6.0",
77+
"policy-conflict-resolver": "^0.0.2",
7778
"rdf-string": "^2.0.1",
7879
"rdf-vocabulary": "^1.0.1",
7980
"uri-template-lite": "^23.4.0",

packages/uma/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export * from './util/http/validate/RequestValidator';
8686

8787
// UCP
8888
export * from './ucp/policy/ODRL';
89+
export * from './ucp/policy/Strategy'
90+
export * from './ucp/policy/PrioritizeProhibitionStrategy'
8991
export * from './ucp/policy/UsageControlPolicy';
9092
export * from './ucp/storage/ContainerUCRulesStorage';
9193
export * from './ucp/storage/DirectoryUCRulesStorage';
Lines changed: 35 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { DC, RDF } from '@solid/community-server';
1+
import { BadRequestHttpError, DC, RDF } from '@solid/community-server';
22
import { getLoggerFor } from 'global-logger-factory';
3-
import { DataFactory, Literal, NamedNode, Quad, Quad_Subject, Store, Writer } from 'n3';
4-
import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator'
5-
import { createVocabulary } from 'rdf-vocabulary';
3+
import { DataFactory, Quad, Store, Writer } from 'n3';
4+
import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator';
65
import { CLIENTID, WEBID } from '../../credentials/Claims';
76
import { ClaimSet } from '../../credentials/ClaimSet';
87
import { basicPolicy } from '../../ucp/policy/ODRL';
8+
import { PrioritizeProhibitionStrategy } from '../../ucp/policy/PrioritizeProhibitionStrategy';
9+
import { Strategy } from '../../ucp/policy/Strategy';
910
import { UCPPolicy } from '../../ucp/policy/UsageControlPolicy';
1011
import { UCRulesStorage } from '../../ucp/storage/UCRulesStorage';
1112
import { ODRL } from '../../ucp/util/Vocabularies';
@@ -33,6 +34,7 @@ const { quad, namedNode, literal, blankNode } = DataFactory
3334
export class OdrlAuthorizer implements Authorizer {
3435
protected readonly logger = getLoggerFor(this);
3536
private readonly odrlEvaluator: ODRLEvaluator;
37+
private readonly strategy: Strategy;
3638

3739
/**
3840
* Creates a OdrlAuthorizer enforcing policies using ODRL with the ODRL Evaluator.
@@ -46,13 +48,14 @@ export class OdrlAuthorizer implements Authorizer {
4648
eyePath?: string,
4749
) {
4850
const engine = eyePath ?
49-
new ODRLEngineMultipleSteps({reasoner: new EyeReasoner(eyePath, ["--quiet", "--nope", "--pass-only-new"])}) :
50-
new ODRLEngineMultipleSteps();
51+
new ODRLEngineMultipleSteps({ reasoner: new EyeReasoner(eyePath, ["--quiet", "--nope", "--pass-only-new"]) }) :
52+
new ODRLEngineMultipleSteps();
5153
this.odrlEvaluator = new ODRLEvaluator(engine);
54+
this.strategy = new PrioritizeProhibitionStrategy();
5255
}
5356

5457
public async permissions(claims: ClaimSet, query?: Permission[]): Promise<Permission[]> {
55-
this.logger.info(`Calculating permissions. ${JSON.stringify({claims, query})}`);
58+
this.logger.info(`Calculating permissions. ${JSON.stringify({ claims, query })}`);
5659
if (!query) {
5760
this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit queries.')
5861
return [];
@@ -67,9 +70,9 @@ export class OdrlAuthorizer implements Authorizer {
6770
// prepare sotw
6871
const sotw = new Store();
6972
sotw.add(quad(
70-
namedNode('http://example.com/request/currentTime'),
71-
namedNode('http://purl.org/dc/terms/issued'),
72-
literal(new Date().toISOString(), namedNode("http://www.w3.org/2001/XMLSchema#dateTime"))),
73+
namedNode('http://example.com/request/currentTime'),
74+
namedNode('http://purl.org/dc/terms/issued'),
75+
literal(new Date().toISOString(), namedNode("http://www.w3.org/2001/XMLSchema#dateTime"))),
7376
);
7477

7578
const subject = typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous';
@@ -91,7 +94,7 @@ export class OdrlAuthorizer implements Authorizer {
9194
// });
9295
}
9396

94-
for (const {resource_id, resource_scopes} of query) {
97+
for (const { resource_id, resource_scopes } of query) {
9598
grantedPermissions[resource_id] = [];
9699
for (const scope of resource_scopes) {
97100
// TODO: why is this transformation happening (here)?
@@ -114,9 +117,9 @@ export class OdrlAuthorizer implements Authorizer {
114117
// Adding context triples for the client identifier, if there is one
115118
if (clientQuads.length > 0) {
116119
requestStore.addQuad(quad(
117-
namedNode(request.ruleIRIs[0]),
118-
namedNode('https://w3id.org/force/sotw#context'),
119-
clientSubject,
120+
namedNode(request.ruleIRIs[0]),
121+
namedNode('https://w3id.org/force/sotw#context'),
122+
clientSubject,
120123
));
121124
requestStore.addQuads(clientQuads);
122125
}
@@ -126,19 +129,19 @@ export class OdrlAuthorizer implements Authorizer {
126129
[...policyStore],
127130
[...requestStore],
128131
[...sotw]);
129-
const reportStore = new Store(reports);
130132

131-
// TODO: handle multiple reports -> possible to be generated
132-
// NOTE: current strategy, add all actions of active reports generated by the request
133-
// fetch active and attempted
134-
const PolicyReportNodes = reportStore.getSubjects(RDF.type, CR.PolicyReport, null);
135-
for (const policyReportNode of PolicyReportNodes) {
136-
const policyReport = parseComplianceReport(policyReportNode, reportStore)
137-
const activeReports = policyReport.ruleReport.filter(
138-
(report) => report.activationState === ActivationState.Active);
139-
if (activeReports.length > 0 && activeReports[0].type === RuleReportType.PermissionReport) {
140-
grantedPermissions[resource_id].push(scope);
141-
}
133+
// handle potential conflicts with a strategy
134+
const allowed = await this.strategy.handleSafe({
135+
request: {
136+
request: [...requestStore],
137+
identifier: namedNode(request.policyIRI)
138+
},
139+
policies: [...policyStore],
140+
reports: reports
141+
})
142+
143+
if (allowed) {
144+
grantedPermissions[resource_id].push(scope);
142145
}
143146
}
144147
}
@@ -147,156 +150,13 @@ export class OdrlAuthorizer implements Authorizer {
147150
resource_id => permissions.push({
148151
resource_id,
149152
resource_scopes: grantedPermissions[resource_id],
150-
}) );
153+
}));
151154
return permissions;
152155
}
153156
}
154157
const scopeCssToOdrl: Map<string, string> = new Map();
155-
scopeCssToOdrl.set('urn:example:css:modes:read','http://www.w3.org/ns/odrl/2/read');
156-
scopeCssToOdrl.set('urn:example:css:modes:append','http://www.w3.org/ns/odrl/2/append');
157-
scopeCssToOdrl.set('urn:example:css:modes:create','http://www.w3.org/ns/odrl/2/create');
158-
scopeCssToOdrl.set('urn:example:css:modes:delete','http://www.w3.org/ns/odrl/2/delete');
159-
scopeCssToOdrl.set('urn:example:css:modes:write','http://www.w3.org/ns/odrl/2/write');
160-
161-
const scopeOdrlToCss : Map<string, string> = new Map(Array.from(scopeCssToOdrl, entry => [entry[1], entry[0]]));
162-
163-
type PolicyReport = {
164-
id: NamedNode;
165-
created: Literal;
166-
request: NamedNode;
167-
policy: NamedNode;
168-
ruleReport: RuleReport[];
169-
}
170-
type RuleReport = {
171-
id: NamedNode;
172-
type: RuleReportType;
173-
activationState: ActivationState
174-
rule: NamedNode;
175-
requestedRule: NamedNode;
176-
premiseReport: PremiseReport[]
177-
}
178-
179-
type PremiseReport = {
180-
id: NamedNode;
181-
type:PremiseReportType;
182-
premiseReport: PremiseReport[];
183-
satisfactionState: SatisfactionState
184-
}
185-
186-
// is it possible to just use CR.namespace + "term"?
187-
// https://github.com/microsoft/TypeScript/issues/40793
188-
enum RuleReportType {
189-
PermissionReport= 'https://w3id.org/force/compliance-report#PermissionReport',
190-
ProhibitionReport= 'https://w3id.org/force/compliance-report#ProhibitionReport',
191-
ObligationReport= 'https://w3id.org/force/compliance-report#ObligationReport',
192-
}
193-
enum SatisfactionState {
194-
Satisfied= 'https://w3id.org/force/compliance-report#Satisfied',
195-
Unsatisfied= 'https://w3id.org/force/compliance-report#Unsatisfied',
196-
}
197-
198-
enum PremiseReportType {
199-
ConstraintReport = 'https://w3id.org/force/compliance-report#ConstraintReport',
200-
PartyReport = 'https://w3id.org/force/compliance-report#PartyReport',
201-
TargetReport = 'https://w3id.org/force/compliance-report#TargetReport',
202-
ActionReport = 'https://w3id.org/force/compliance-report#ActionReport',
203-
}
204-
205-
enum ActivationState {
206-
Active= 'https://w3id.org/force/compliance-report#Active',
207-
Inactive= 'https://w3id.org/force/compliance-report#Inactive',
208-
}
209-
210-
/**
211-
* Parses an ODRL Compliance Report Model into a {@link PolicyReport}.
212-
* @param identifier
213-
* @param store
214-
*/
215-
function parseComplianceReport(identifier: Quad_Subject, store: Store): PolicyReport {
216-
const exists = store.getQuads(identifier,RDF.type,CR.PolicyReport, null).length === 1;
217-
if (!exists) { throw Error(`No Policy Report found with: ${identifier}.`); }
218-
const ruleReportNodes = store.getObjects(identifier, CR.ruleReport, null) as NamedNode[];
219-
220-
return {
221-
id: identifier as NamedNode,
222-
created: store.getObjects(identifier, DC.namespace+"created", null)[0] as Literal,
223-
policy: store.getObjects(identifier, CR.policy, null)[0] as NamedNode,
224-
request: store.getObjects(identifier, CR.policyRequest, null)[0] as NamedNode,
225-
ruleReport: ruleReportNodes.map(ruleReportNode => parseRuleReport(ruleReportNode, store))
226-
}
227-
}
228-
229-
/**
230-
* Parses Rule Reports from a Compliance Report, including its premises
231-
* @param identifier
232-
* @param store
233-
*/
234-
function parseRuleReport(identifier: Quad_Subject, store: Store): RuleReport {
235-
const premiseNodes = store.getObjects(identifier,CR.premiseReport, null) as NamedNode[];
236-
return {
237-
id: identifier as NamedNode,
238-
type: store.getObjects(identifier, RDF.type, null)[0].value as RuleReportType,
239-
activationState: store.getObjects(identifier, CR.activationState, null)[0].value as ActivationState,
240-
requestedRule: store.getObjects(identifier, CR.ruleRequest, null)[0] as NamedNode,
241-
rule: store.getObjects(identifier, CR.rule, null)[0] as NamedNode,
242-
premiseReport: premiseNodes.map((prem) => parsePremiseReport(prem, store))
243-
}
244-
}
245-
246-
/**
247-
* Parses Premise Reports, including premises of a Premise Report itself.
248-
* Note that if for some reason there are circular premise reports, this will result into an infinite loop
249-
* @param identifier
250-
* @param store
251-
*/
252-
function parsePremiseReport(identifier: Quad_Subject, store: Store): PremiseReport {
253-
const nestedPremises = store.getObjects(identifier, CR.PremiseReport, null) as NamedNode[];
254-
return {
255-
id: identifier as NamedNode,
256-
type: store.getObjects(identifier, RDF.type, null)[0].value as PremiseReportType,
257-
premiseReport: nestedPremises.map((prem) => parsePremiseReport(prem, store)),
258-
satisfactionState: store.getObjects(identifier, CR.satisfactionState, null)[0].value as SatisfactionState
259-
}
260-
}
261-
const CR = createVocabulary('https://w3id.org/force/compliance-report#',
262-
'PolicyReport',
263-
'RuleReport',
264-
'PermissionReport',
265-
'ProhibitionReport',
266-
'DutyReport',
267-
'PremiseReport',
268-
'ConstraintReport',
269-
'PartyReport',
270-
'ActionReport',
271-
'TargetReport',
272-
'ActivationState',
273-
'Active',
274-
'Inactive',
275-
'AttemptState',
276-
'Attempted',
277-
'NotAttempted',
278-
'PerformanceState',
279-
'Performed',
280-
'Unperformed',
281-
'Unknown',
282-
'DeonticState',
283-
'NonSet',
284-
'Violated',
285-
'Fulfilled',
286-
'SatisfactionState',
287-
'Satisfied',
288-
'Unsatisfied',
289-
'policy',
290-
'policyRequest',
291-
'ruleReport',
292-
'conditionReport',
293-
'premiseReport',
294-
'rule',
295-
'ruleRequest',
296-
'activationState',
297-
'attemptState',
298-
'performanceState',
299-
'deonticState',
300-
'constraint',
301-
'satisfactionState',
302-
)
158+
scopeCssToOdrl.set('urn:example:css:modes:read', 'http://www.w3.org/ns/odrl/2/read');
159+
scopeCssToOdrl.set('urn:example:css:modes:append', 'http://www.w3.org/ns/odrl/2/append');
160+
scopeCssToOdrl.set('urn:example:css:modes:create', 'http://www.w3.org/ns/odrl/2/create');
161+
scopeCssToOdrl.set('urn:example:css:modes:delete', 'http://www.w3.org/ns/odrl/2/delete');
162+
scopeCssToOdrl.set('urn:example:css:modes:write', 'http://www.w3.org/ns/odrl/2/write');
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Store } from 'n3';
2+
import { parseComplianceReport, RDF, REPORT, serializeComplianceReport } from 'odrl-evaluator';
3+
import { ActiveConflictResolver, ConflictResolverInput, DenyConflictResolver, FORCE } from 'policy-conflict-resolver';
4+
import { ConflictResolutionStrategyInput, Strategy } from './Strategy';
5+
6+
/**
7+
* A strategy for ODRL evaluations that combines two strategies:
8+
* - default deny: If there is no active permission, the action is not allowed -> There must be at least one permission.
9+
* - prohibition over permissions: The action is allowed if there is no prohibition and at least one permission.
10+
*
11+
* The stronger of the two is that there must be at least one permission and no prohibitions for that given request.
12+
*
13+
* It works for one request at a time to determine whether the action is allowed on the resource or not.
14+
*/
15+
export class PrioritizeProhibitionStrategy extends Strategy {
16+
public constructor() {
17+
super(new ActiveConflictResolver(new DenyConflictResolver()));
18+
}
19+
20+
async handle(input: ConflictResolutionStrategyInput): Promise<boolean> {
21+
const reportStore = new Store(input.reports)
22+
const policyReportNodes = reportStore.getSubjects(RDF.type, REPORT.PolicyReport, null);
23+
const conflictResolverInput: ConflictResolverInput = { reports: [] }
24+
25+
for (const policyReportNode of policyReportNodes) {
26+
const parsedReport = parseComplianceReport(policyReportNode, reportStore);
27+
28+
if (parsedReport.request.value !== input.request.identifier.value) {
29+
// Ignore this compliance report as it pertains to another request
30+
continue;
31+
}
32+
// NOTE: on rule level of the compliance, this does not get checked.
33+
// In theory it is possible to have a compliance report that has a different requested rule than to the top level.
34+
// In practice, that should not happen.
35+
36+
conflictResolverInput.reports.push(
37+
{
38+
report: serializeComplianceReport(parsedReport),
39+
policy: input.policies
40+
})
41+
42+
}
43+
const result = await this.resolver.handleSafe(conflictResolverInput);
44+
const resultStore = new Store(result.report);
45+
const status = resultStore.getObjects(result.identifier, FORCE.conclusion, null);
46+
if (status.length < 1) {
47+
return false;
48+
}
49+
50+
return status[0].value === FORCE.Allow;
51+
}
52+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Quad, Quad_Subject } from '@rdfjs/types';
2+
import { AsyncHandler } from 'asynchronous-handlers';
3+
import { ConflictResolver } from 'policy-conflict-resolver';
4+
5+
export interface ConflictResolutionStrategyInput {
6+
/**
7+
* The Evaluation Request to which an ODRL Evaluation has occurred.
8+
*/
9+
request: {
10+
identifier: Quad_Subject;
11+
request: Quad[];
12+
}
13+
/**
14+
* A set of ODRL policies reports serialized as a list of quads
15+
*/
16+
policies: Quad[];
17+
/**
18+
* A set of policy compliance reports serialized as a list of quads
19+
*/
20+
reports: Quad[];
21+
}
22+
23+
/**
24+
* The strategy employed for ODRL Evaluations.
25+
*
26+
* This is necessary to deal with multiple rule reports that might conflict with each other.
27+
* It also contains the logic to encode the strategy what happens when there is no information to be gained from the compliance report.
28+
* I.e. what access control decision is employed when no active Permission Rule Reports are present.
29+
*/
30+
export abstract class Strategy extends AsyncHandler<ConflictResolutionStrategyInput, boolean> {
31+
protected resolver: ConflictResolver;
32+
33+
constructor(conflictResolver: ConflictResolver) {
34+
super();
35+
this.resolver = conflictResolver;
36+
}
37+
}

0 commit comments

Comments
 (0)