Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/uma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
"logform": "^2.6.0",
"ms": "^2.1.3",
"n3": "^1.17.2",
"odrl-evaluator": "^0.5.0",
"odrl-evaluator": "^0.6.0",
"policy-conflict-resolver": "^0.0.2",
"rdf-vocabulary": "^1.0.1",
"uri-template-lite": "^23.4.0",
"winston": "^3.11.0"
Expand Down
2 changes: 2 additions & 0 deletions packages/uma/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export * from './util/http/validate/RequestValidator';

// UCP
export * from './ucp/policy/ODRL';
export * from './ucp/policy/Strategy'
export * from './ucp/policy/PrioritizeProhibitionStrategy'
export * from './ucp/policy/UsageControlPolicy';
export * from './ucp/storage/ContainerUCRulesStorage';
export * from './ucp/storage/DirectoryUCRulesStorage';
Expand Down
224 changes: 43 additions & 181 deletions packages/uma/src/policies/authorizers/OdrlAuthorizer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { BadRequestHttpError, DC, NotImplementedHttpError, RDF } from '@solid/community-server';
import { BadRequestHttpError, NotImplementedHttpError, RDF } from '@solid/community-server';
import { getLoggerFor } from 'global-logger-factory';
import { DataFactory, Literal, NamedNode, Quad, Quad_Subject, Store } from 'n3';
import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator'
import { createVocabulary } from 'rdf-vocabulary';
import { DataFactory, Quad, Store } from 'n3';
import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator';
import { CLIENTID, WEBID } from '../../credentials/Claims';
import { ClaimSet } from '../../credentials/ClaimSet';
import { Requirements } from '../../credentials/Requirements';
import { basicPolicy } from '../../ucp/policy/ODRL';
import { PrioritizeProhibitionStrategy } from '../../ucp/policy/PrioritizeProhibitionStrategy';
import { Strategy } from '../../ucp/policy/Strategy';
import { UCPPolicy } from '../../ucp/policy/UsageControlPolicy';
import { UCRulesStorage } from '../../ucp/storage/UCRulesStorage';
import { ODRL } from '../../ucp/util/Vocabularies';
Expand Down Expand Up @@ -34,6 +35,7 @@ const { quad, namedNode, literal, blankNode } = DataFactory
export class OdrlAuthorizer implements Authorizer {
protected readonly logger = getLoggerFor(this);
private readonly odrlEvaluator: ODRLEvaluator;
private readonly strategy: Strategy;

/**
* Creates a OdrlAuthorizer enforcing policies using ODRL with the ODRL Evaluator.
Expand All @@ -47,13 +49,14 @@ export class OdrlAuthorizer implements Authorizer {
eyePath?: string,
) {
const engine = eyePath ?
new ODRLEngineMultipleSteps({reasoner: new EyeReasoner(eyePath, ["--quiet", "--nope", "--pass-only-new"])}) :
new ODRLEngineMultipleSteps();
new ODRLEngineMultipleSteps({ reasoner: new EyeReasoner(eyePath, ["--quiet", "--nope", "--pass-only-new"]) }) :
new ODRLEngineMultipleSteps();
this.odrlEvaluator = new ODRLEvaluator(engine);
this.strategy = new PrioritizeProhibitionStrategy();
}

public async permissions(claims: ClaimSet, query?: Permission[]): Promise<Permission[]> {
this.logger.info(`Calculating permissions. ${JSON.stringify({claims, query})}`);
this.logger.info(`Calculating permissions. ${JSON.stringify({ claims, query })}`);
if (!query) {
this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit queries.')
return [];
Expand All @@ -68,9 +71,9 @@ export class OdrlAuthorizer implements Authorizer {
// prepare sotw
const sotw = new Store();
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"))),
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"))),
);

const subject = typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous';
Expand All @@ -92,7 +95,7 @@ export class OdrlAuthorizer implements Authorizer {
// });
}

for (const {resource_id, resource_scopes} of query) {
for (const { resource_id, resource_scopes } of query) {
grantedPermissions[resource_id] = [];
const actions = transformActionsCssToOdrl(resource_scopes);
for (const action of actions) {
Expand All @@ -112,9 +115,9 @@ export class OdrlAuthorizer implements Authorizer {
// Adding context triples for the client identifier, if there is one
if (clientQuads.length > 0) {
requestStore.addQuad(quad(
namedNode(request.ruleIRIs[0]),
namedNode('https://w3id.org/force/sotw#context'),
clientSubject,
namedNode(request.ruleIRIs[0]),
namedNode('https://w3id.org/force/sotw#context'),
clientSubject,
));
requestStore.addQuads(clientQuads);
}
Expand All @@ -124,19 +127,19 @@ export class OdrlAuthorizer implements Authorizer {
[...policyStore],
[...requestStore],
[...sotw]);
const reportStore = new Store(reports);

// TODO: handle multiple reports -> possible to be generated
// NOTE: current strategy, add all actions of active reports generated by the request
// fetch active and attempted
const PolicyReportNodes = reportStore.getSubjects(RDF.type, CR.PolicyReport, null);
for (const policyReportNode of PolicyReportNodes) {
const policyReport = parseComplianceReport(policyReportNode, reportStore)
const activeReports = policyReport.ruleReport.filter(
(report) => report.activationState === ActivationState.Active);
if (activeReports.length > 0 && activeReports[0].type === RuleReportType.PermissionReport) {
grantedPermissions[resource_id].push(action);
}
// handle potential conflicts with a strategy
const allowed = await this.strategy.handleSafe({
request: {
request: [...requestStore],
identifier: namedNode(request.policyIRI)
},
policies: [...policyStore],
reports: reports
})

if (allowed) {
grantedPermissions[resource_id].push(action);
}
}
}
Expand All @@ -145,7 +148,7 @@ export class OdrlAuthorizer implements Authorizer {
resource_id => permissions.push({
resource_id,
resource_scopes: transformActionsOdrlToCss(grantedPermissions[resource_id])
}) );
}));
return permissions;
}

Expand All @@ -155,13 +158,13 @@ export class OdrlAuthorizer implements Authorizer {

}
const scopeCssToOdrl: Map<string, string> = new Map();
scopeCssToOdrl.set('urn:example:css:modes:read','http://www.w3.org/ns/odrl/2/read');
scopeCssToOdrl.set('urn:example:css:modes:append','http://www.w3.org/ns/odrl/2/append');
scopeCssToOdrl.set('urn:example:css:modes:create','http://www.w3.org/ns/odrl/2/create');
scopeCssToOdrl.set('urn:example:css:modes:delete','http://www.w3.org/ns/odrl/2/delete');
scopeCssToOdrl.set('urn:example:css:modes:write','http://www.w3.org/ns/odrl/2/write');
scopeCssToOdrl.set('urn:example:css:modes:read', 'http://www.w3.org/ns/odrl/2/read');
scopeCssToOdrl.set('urn:example:css:modes:append', 'http://www.w3.org/ns/odrl/2/append');
scopeCssToOdrl.set('urn:example:css:modes:create', 'http://www.w3.org/ns/odrl/2/create');
scopeCssToOdrl.set('urn:example:css:modes:delete', 'http://www.w3.org/ns/odrl/2/delete');
scopeCssToOdrl.set('urn:example:css:modes:write', 'http://www.w3.org/ns/odrl/2/write');

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

/**
* Transform the Actions enforced by the Community Solid Server to equivalent ODRL Actions
Expand All @@ -173,11 +176,11 @@ function transformActionsCssToOdrl(actions: string[]): string[] {
// in UMAPermissionReader, only the last part of the URN will be used, divided by a colon
// again, see CSS package
return actions.map(action => {
const result = scopeCssToOdrl.get(action);
if (!result) {
throw new BadRequestHttpError(`Unsupported action ${action}`);
}
return result;
const result = scopeCssToOdrl.get(action);
if (!result) {
throw new BadRequestHttpError(`Unsupported action ${action}`);
}
return result;
});
}
/**
Expand All @@ -187,151 +190,10 @@ function transformActionsCssToOdrl(actions: string[]): string[] {
function transformActionsOdrlToCss(actions: string[]): string[] {
const cssActions = []
for (const action of actions) {
if (action === 'http://www.w3.org/ns/odrl/2/use'){
if (action === 'http://www.w3.org/ns/odrl/2/use') {
return Array.from(scopeCssToOdrl.keys());
}
cssActions.push(scopeOdrlToCss.get(action)!);
}
return cssActions;
}

type PolicyReport = {
id: NamedNode;
created: Literal;
request: NamedNode;
policy: NamedNode;
ruleReport: RuleReport[];
}
type RuleReport = {
id: NamedNode;
type: RuleReportType;
activationState: ActivationState
rule: NamedNode;
requestedRule: NamedNode;
premiseReport: PremiseReport[]
}

type PremiseReport = {
id: NamedNode;
type:PremiseReportType;
premiseReport: PremiseReport[];
satisfactionState: SatisfactionState
}

// is it possible to just use CR.namespace + "term"?
// https://github.com/microsoft/TypeScript/issues/40793
enum RuleReportType {
PermissionReport= 'https://w3id.org/force/compliance-report#PermissionReport',
ProhibitionReport= 'https://w3id.org/force/compliance-report#ProhibitionReport',
ObligationReport= 'https://w3id.org/force/compliance-report#ObligationReport',
}
enum SatisfactionState {
Satisfied= 'https://w3id.org/force/compliance-report#Satisfied',
Unsatisfied= 'https://w3id.org/force/compliance-report#Unsatisfied',
}

enum PremiseReportType {
ConstraintReport = 'https://w3id.org/force/compliance-report#ConstraintReport',
PartyReport = 'https://w3id.org/force/compliance-report#PartyReport',
TargetReport = 'https://w3id.org/force/compliance-report#TargetReport',
ActionReport = 'https://w3id.org/force/compliance-report#ActionReport',
}

enum ActivationState {
Active= 'https://w3id.org/force/compliance-report#Active',
Inactive= 'https://w3id.org/force/compliance-report#Inactive',
}

/**
* Parses an ODRL Compliance Report Model into a {@link PolicyReport}.
* @param identifier
* @param store
*/
function parseComplianceReport(identifier: Quad_Subject, store: Store): PolicyReport {
const exists = store.getQuads(identifier,RDF.type,CR.PolicyReport, null).length === 1;
if (!exists) { throw Error(`No Policy Report found with: ${identifier}.`); }
const ruleReportNodes = store.getObjects(identifier, CR.ruleReport, null) as NamedNode[];

return {
id: identifier as NamedNode,
created: store.getObjects(identifier, DC.namespace+"created", null)[0] as Literal,
policy: store.getObjects(identifier, CR.policy, null)[0] as NamedNode,
request: store.getObjects(identifier, CR.policyRequest, null)[0] as NamedNode,
ruleReport: ruleReportNodes.map(ruleReportNode => parseRuleReport(ruleReportNode, store))
}
}

/**
* Parses Rule Reports from a Compliance Report, including its premises
* @param identifier
* @param store
*/
function parseRuleReport(identifier: Quad_Subject, store: Store): RuleReport {
const premiseNodes = store.getObjects(identifier,CR.premiseReport, null) as NamedNode[];
return {
id: identifier as NamedNode,
type: store.getObjects(identifier, RDF.type, null)[0].value as RuleReportType,
activationState: store.getObjects(identifier, CR.activationState, null)[0].value as ActivationState,
requestedRule: store.getObjects(identifier, CR.ruleRequest, null)[0] as NamedNode,
rule: store.getObjects(identifier, CR.rule, null)[0] as NamedNode,
premiseReport: premiseNodes.map((prem) => parsePremiseReport(prem, store))
}
}

/**
* Parses Premise Reports, including premises of a Premise Report itself.
* Note that if for some reason there are circular premise reports, this will result into an infinite loop
* @param identifier
* @param store
*/
function parsePremiseReport(identifier: Quad_Subject, store: Store): PremiseReport {
const nestedPremises = store.getObjects(identifier, CR.PremiseReport, null) as NamedNode[];
return {
id: identifier as NamedNode,
type: store.getObjects(identifier, RDF.type, null)[0].value as PremiseReportType,
premiseReport: nestedPremises.map((prem) => parsePremiseReport(prem, store)),
satisfactionState: store.getObjects(identifier, CR.satisfactionState, null)[0].value as SatisfactionState
}
}
const CR = createVocabulary('https://w3id.org/force/compliance-report#',
'PolicyReport',
'RuleReport',
'PermissionReport',
'ProhibitionReport',
'DutyReport',
'PremiseReport',
'ConstraintReport',
'PartyReport',
'ActionReport',
'TargetReport',
'ActivationState',
'Active',
'Inactive',
'AttemptState',
'Attempted',
'NotAttempted',
'PerformanceState',
'Performed',
'Unperformed',
'Unknown',
'DeonticState',
'NonSet',
'Violated',
'Fulfilled',
'SatisfactionState',
'Satisfied',
'Unsatisfied',
'policy',
'policyRequest',
'ruleReport',
'conditionReport',
'premiseReport',
'rule',
'ruleRequest',
'activationState',
'attemptState',
'performanceState',
'deonticState',
'constraint',
'satisfactionState',
)
}
52 changes: 52 additions & 0 deletions packages/uma/src/ucp/policy/PrioritizeProhibitionStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Store } from 'n3';
import { parseComplianceReport, RDF, REPORT, serializeComplianceReport } from 'odrl-evaluator';
import { ActiveConflictResolver, ConflictResolverInput, DenyConflictResolver, FORCE } from 'policy-conflict-resolver';
import { ConflictResolutionStrategyInput, Strategy } from './Strategy';

/**
* A strategy for ODRL evaluations that combines two strategies:
* - default deny: If there is no active permission, the action is not allowed -> There must be at least one permission.
* - prohibition over permissions: The action is allowed if there is no prohibition and at least one permission.
*
* The stronger of the two is that there must be at least one permission and no prohibitions for that given request.
*
* It works for one request at a time to determine whether the action is allowed on the resource or not.
*/
export class PrioritizeProhibitionStrategy extends Strategy {
public constructor() {
super(new ActiveConflictResolver(new DenyConflictResolver()));
}

async handle(input: ConflictResolutionStrategyInput): Promise<boolean> {
const reportStore = new Store(input.reports)
const policyReportNodes = reportStore.getSubjects(RDF.type, REPORT.PolicyReport, null);
const conflictResolverInput: ConflictResolverInput = { reports: [] }

for (const policyReportNode of policyReportNodes) {
const parsedReport = parseComplianceReport(policyReportNode, reportStore);

if (parsedReport.request.value !== input.request.identifier.value) {
// Ignore this compliance report as it pertains to another request
continue;
}
// NOTE: on rule level of the compliance, this does not get checked.
// In theory it is possible to have a compliance report that has a different requested rule than to the top level.
// In practice, that should not happen.

conflictResolverInput.reports.push(
{
report: serializeComplianceReport(parsedReport),
policy: input.policies
})

}
const result = await this.resolver.handleSafe(conflictResolverInput);
const resultStore = new Store(result.report);
const status = resultStore.getObjects(result.identifier, FORCE.conclusion, null);
if (status.length < 1) {
return false;
}

return status[0].value === FORCE.Allow;
}
}
Loading
Loading