From 6fadfa5661047426e50e41c585796f4d44a67b92 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Thu, 22 Jan 2026 15:55:28 +0100 Subject: [PATCH 1/3] feat: introduce the prohibition over permission + default deny strategy in the ODRL authorizer --- packages/uma/package.json | 3 +- packages/uma/src/index.ts | 2 + .../policies/authorizers/OdrlAuthorizer.ts | 210 +++--------------- .../policy/PrioritizeProhibitionStrategy.ts | 52 +++++ packages/uma/src/ucp/policy/Strategy.ts | 37 +++ .../authorizers/OdrlAuthorizer.test.ts | 28 ++- .../PrioritizeProhibitionStrategy.test.ts | 165 ++++++++++++++ yarn.lock | 74 ++++-- 8 files changed, 374 insertions(+), 197 deletions(-) create mode 100644 packages/uma/src/ucp/policy/PrioritizeProhibitionStrategy.ts create mode 100644 packages/uma/src/ucp/policy/Strategy.ts create mode 100644 packages/uma/test/unit/ucp/policy/PrioritizeProhibitionStrategy.test.ts diff --git a/packages/uma/package.json b/packages/uma/package.json index 7b52bdd..0f1f257 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -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-string": "^2.0.1", "rdf-vocabulary": "^1.0.1", "uri-template-lite": "^23.4.0", diff --git a/packages/uma/src/index.ts b/packages/uma/src/index.ts index 090b23a..699fb39 100644 --- a/packages/uma/src/index.ts +++ b/packages/uma/src/index.ts @@ -85,6 +85,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'; diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index dab4449..6f9e925 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -1,11 +1,12 @@ -import { DC, RDF } from '@solid/community-server'; +import { BadRequestHttpError, DC, RDF } from '@solid/community-server'; import { getLoggerFor } from 'global-logger-factory'; -import { DataFactory, Literal, NamedNode, Quad, Quad_Subject, Store, Writer } from 'n3'; -import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator' -import { createVocabulary } from 'rdf-vocabulary'; +import { DataFactory, Quad, Store, Writer } from 'n3'; +import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator'; import { CLIENTID, WEBID } from '../../credentials/Claims'; import { ClaimSet } from '../../credentials/ClaimSet'; 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'; @@ -33,6 +34,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. @@ -46,13 +48,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 { - 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 []; @@ -67,9 +70,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'; @@ -91,7 +94,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] = []; for (const scope of resource_scopes) { // TODO: why is this transformation happening (here)? @@ -114,9 +117,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); } @@ -126,19 +129,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(scope); - } + // 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(scope); } } } @@ -147,156 +150,13 @@ export class OdrlAuthorizer implements Authorizer { resource_id => permissions.push({ resource_id, resource_scopes: grantedPermissions[resource_id], - }) ); + })); return permissions; } } const scopeCssToOdrl: Map = 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'); - -const scopeOdrlToCss : Map = new Map(Array.from(scopeCssToOdrl, entry => [entry[1], entry[0]])); - -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', - ) +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'); diff --git a/packages/uma/src/ucp/policy/PrioritizeProhibitionStrategy.ts b/packages/uma/src/ucp/policy/PrioritizeProhibitionStrategy.ts new file mode 100644 index 0000000..a25f0fc --- /dev/null +++ b/packages/uma/src/ucp/policy/PrioritizeProhibitionStrategy.ts @@ -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 { + 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; + } +} diff --git a/packages/uma/src/ucp/policy/Strategy.ts b/packages/uma/src/ucp/policy/Strategy.ts new file mode 100644 index 0000000..a0c29de --- /dev/null +++ b/packages/uma/src/ucp/policy/Strategy.ts @@ -0,0 +1,37 @@ +import type { Quad, Quad_Subject } from '@rdfjs/types'; +import { AsyncHandler } from 'asynchronous-handlers'; +import { ConflictResolver } from 'policy-conflict-resolver'; + +export interface ConflictResolutionStrategyInput { + /** + * The Evaluation Request to which an ODRL Evaluation has occurred. + */ + request: { + identifier: Quad_Subject; + request: Quad[]; + } + /** + * A set of ODRL policies reports serialized as a list of quads + */ + policies: Quad[]; + /** + * A set of policy compliance reports serialized as a list of quads + */ + reports: Quad[]; +} + +/** + * The strategy employed for ODRL Evaluations. + * + * This is necessary to deal with multiple rule reports that might conflict with each other. + * It also contains the logic to encode the strategy what happens when there is no information to be gained from the compliance report. + * I.e. what access control decision is employed when no active Permission Rule Reports are present. + */ +export abstract class Strategy extends AsyncHandler { + protected resolver: ConflictResolver; + + constructor(conflictResolver: ConflictResolver) { + super(); + this.resolver = conflictResolver; + } +} diff --git a/packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts b/packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts index 9e614cb..6e24d1f 100644 --- a/packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts +++ b/packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts @@ -35,7 +35,7 @@ describe('OdrlAuthorizer', (): void => { vi.mocked(basicPolicy).mockReturnValue({ ruleIRIs:[], - policyIRI: '', + policyIRI: 'req', representation: new Store(requestQuads), }); @@ -105,8 +105,13 @@ describe('OdrlAuthorizer', (): void => { @prefix dc: . @prefix xsd: . a cr:PolicyReport ; + dc:created "2024-02-12T11:20:10.999Z"^^xsd:dateTime ; + cr:policyRequest ; + cr:policy ; cr:ruleReport . a cr:PermissionReport ; + cr:rule ; + cr:ruleRequest ; cr:activationState cr:Active ; cr:premiseReport . a cr:Target-Report ; @@ -128,8 +133,13 @@ describe('OdrlAuthorizer', (): void => { @prefix dc: . @prefix xsd: . a cr:PolicyReport ; + dc:created "2024-02-12T11:20:10.999Z"^^xsd:dateTime ; + cr:policyRequest ; + cr:policy ; cr:ruleReport . a cr:PermissionReport ; + cr:rule ; + cr:ruleRequest ; cr:activationState cr:Inactive ; cr:premiseReport . a cr:Target-Report ; @@ -151,8 +161,14 @@ describe('OdrlAuthorizer', (): void => { @prefix dc: . @prefix xsd: . a cr:PolicyReport ; + cr:policyRequest ; + dc:created "2024-02-12T11:20:10.999Z"^^xsd:dateTime ; + cr:policyRequest ; + cr:policy ; cr:ruleReport . a cr:ProhibitionReport ; + cr:rule ; + cr:ruleRequest ; cr:activationState cr:Active ; cr:premiseReport . a cr:Target-Report ; @@ -177,8 +193,13 @@ describe('OdrlAuthorizer', (): void => { @prefix dc: . @prefix xsd: . a cr:PolicyReport ; + dc:created "2024-02-12T11:20:10.999Z"^^xsd:dateTime ; + cr:policyRequest ; + cr:policy ; cr:ruleReport . a cr:PermissionReport ; + cr:rule ; + cr:ruleRequest ; cr:activationState cr:Active ; cr:premiseReport . a cr:Target-Report ; @@ -189,8 +210,13 @@ describe('OdrlAuthorizer', (): void => { @prefix dc: . @prefix xsd: . a cr:PolicyReport ; + dc:created "2024-02-12T11:20:10.999Z"^^xsd:dateTime ; + cr:policyRequest ; + cr:policy ; cr:ruleReport . a cr:ProhibitionReport ; + cr:rule ; + cr:ruleRequest ; cr:activationState cr:Active ; cr:premiseReport . a cr:Target-Report ; diff --git a/packages/uma/test/unit/ucp/policy/PrioritizeProhibitionStrategy.test.ts b/packages/uma/test/unit/ucp/policy/PrioritizeProhibitionStrategy.test.ts new file mode 100644 index 0000000..ebcafde --- /dev/null +++ b/packages/uma/test/unit/ucp/policy/PrioritizeProhibitionStrategy.test.ts @@ -0,0 +1,165 @@ +import { + PolicyReport, + RuleReportType, + PremiseReportType, + SatisfactionState, + ActivationState, + AttemptState, + serializeComplianceReport, + ODRL +} from "odrl-evaluator" +import { DataFactory } from 'n3'; +import type { Quad, Quad_Subject } from '@rdfjs/types' +import { PrioritizeProhibitionStrategy } from '../../../../src/ucp/policy/PrioritizeProhibitionStrategy' + +const { namedNode, literal, quad } = DataFactory; + +describe("PrioritizeProhibitionStrategy", (): void => { + let complianceReport: PolicyReport; + let strategy: PrioritizeProhibitionStrategy; + let request: { identifier: Quad_Subject, request: Quad[] }; + beforeEach(async (): Promise => { + let requestIdentifier = "urn:uuid:evaluation-request-1"; + const requestRule = "urn:uuid:requested-rule-xyz" + request = { + identifier: namedNode(requestIdentifier), + request: [ + quad(namedNode(requestIdentifier), ODRL.terms.permission, namedNode(requestRule)) + ] + } + complianceReport = { + id: namedNode("urn:uuid:policy-report-1"), + created: literal("2024-02-12T11:20:10.999Z", "http://www.w3.org/2001/XMLSchema#dateTime"), + request: namedNode(requestIdentifier), + policy: namedNode("urn:uuid:policy-123"), + ruleReport: [ + { + id: namedNode("urn:uuid:rule-report-1"), + type: RuleReportType.PermissionReport, + activationState: ActivationState.Active, + attemptState: AttemptState.Attempted, + performanceState: undefined, + deonticState: undefined, + rule: namedNode("urn:uuid:rule-abc"), + requestedRule: namedNode(requestRule), + premiseReport: [ + { + id: namedNode("urn:uuid:premise-1"), + type: PremiseReportType.TargetReport, + premiseReport: [], + satisfactionState: SatisfactionState.Satisfied + }, + { + id: namedNode("urn:uuid:premise-2"), + type: PremiseReportType.PartyReport, + premiseReport: [], + satisfactionState: SatisfactionState.Satisfied + }, + { + id: namedNode("urn:uuid:premise-3"), + type: PremiseReportType.ActionReport, + premiseReport: [], + satisfactionState: SatisfactionState.Satisfied + } + ], + conditionReport: [] + } + ] + } + strategy = new PrioritizeProhibitionStrategy(); + }); + + it('returns true when there are only active rule reports.', async (): Promise => { + const result = await strategy.handle({ + request: request, + reports: serializeComplianceReport(complianceReport), + policies: [] + }); + expect(result).toBe(true) + }); + + it('returns false when there no active permission rule reports.', async (): Promise => { + complianceReport.ruleReport[0].activationState = ActivationState.Inactive + const result = await strategy.handle({ + request: request, + reports: serializeComplianceReport(complianceReport), + policies: [] + }); + expect(result).toBe(false) + }); + + it('returns false when there is an active prohibition rule report.', async (): Promise => { + complianceReport.ruleReport[0].type = RuleReportType.ProhibitionReport; + const result = await strategy.handle({ + request: request, + reports: serializeComplianceReport(complianceReport), + policies: [] + }); + expect(result).toBe(false) + }); + + it('returns false when there is an active prohibition and active permission rule report in different compliance reports.', async (): Promise => { + let prohibitionComplianceReport: PolicyReport = { + id: namedNode("urn:uuid:policy-report-2"), + created: literal("2024-02-12T11:20:10.999Z", "http://www.w3.org/2001/XMLSchema#dateTime"), + request: complianceReport.request, + policy: namedNode("urn:uuid:policy-124"), + ruleReport: [ + { + id: namedNode("urn:uuid:rule-report-2"), + type: RuleReportType.ProhibitionReport, + activationState: ActivationState.Active, + attemptState: AttemptState.Attempted, + performanceState: undefined, + deonticState: undefined, + rule: namedNode("urn:uuid:rule-abc"), + requestedRule: complianceReport.ruleReport[0].requestedRule, + premiseReport: [ + ], + conditionReport: [] + } + ] + } + const result = await strategy.handle({ + request: request, + reports: [...serializeComplianceReport(complianceReport), ...serializeComplianceReport(prohibitionComplianceReport)], + policies: [] + }); + expect(result).toBe(false) + }); + + it('returns false when there is an active prohibition and active permission rule report in the same compliance report.', async (): Promise => { + complianceReport.ruleReport.push( + { + id: namedNode("urn:uuid:rule-report-2"), + type: RuleReportType.ProhibitionReport, + activationState: ActivationState.Active, + attemptState: AttemptState.Attempted, + performanceState: undefined, + deonticState: undefined, + rule: namedNode("urn:uuid:rule-abc"), + requestedRule: complianceReport.ruleReport[0].requestedRule, + premiseReport: [ + ], + conditionReport: [] + } + ) + const result = await strategy.handle({ + request: request, + reports: serializeComplianceReport(complianceReport), + policies: [] + }); + expect(result).toBe(false) + }); + + + it('returns false when there are no active permission rule reports for the given request.', async (): Promise => { + request.identifier = namedNode("random-request"); + const result = await strategy.handle({ + request: request, + reports: serializeComplianceReport(complianceReport), + policies: [] + }); + expect(result).toBe(false) + }) +}) diff --git a/yarn.lock b/yarn.lock index 120a8d0..d69148e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5656,7 +5656,8 @@ __metadata: logform: "npm:^2.6.0" ms: "npm:^2.1.3" n3: "npm:^1.17.2" - odrl-evaluator: "npm:^0.5.0" + odrl-evaluator: "npm:^0.6.0" + policy-conflict-resolver: "npm:^0.0.2" rdf-string: "npm:^2.0.1" rdf-vocabulary: "npm:^1.0.1" uri-template-lite: "npm:^23.4.0" @@ -5839,10 +5840,10 @@ __metadata: languageName: node linkType: hard -"@types/emscripten@npm:^1.39.13": - version: 1.40.1 - resolution: "@types/emscripten@npm:1.40.1" - checksum: 10c0/0d6cd29e551f85ba49a0e7d58de16c857960d40e57553e7cc2860b7d80c4210c992ed292998ec3fd3bdc3b41d96541e91d01a6c232106ac0ad79b4710e87f38d +"@types/emscripten@npm:^1.41.5": + version: 1.41.5 + resolution: "@types/emscripten@npm:1.41.5" + checksum: 10c0/ae816da716f896434e59df7a71b67c71ae7e85ca067a32aef1616572fc4757459515d42ade6f5b8fd8d69733a9dbd0cf23010fec5b2f41ce52c09501aa350e45 languageName: node linkType: hard @@ -8567,17 +8568,26 @@ __metadata: languageName: node linkType: hard -"eyereasoner@npm:^16.18.4": - version: 16.34.1 - resolution: "eyereasoner@npm:16.34.1" +"eyeling@npm:^1.10.6": + version: 1.10.14 + resolution: "eyeling@npm:1.10.14" + bin: + eyeling: eyeling.js + checksum: 10c0/fa6787a209b38029b14318d16ea667a8ec5e324570477bc4d688200416595b34762383a454f7610fbd06a8ea4f290d42dcc93aa5695c145691f50772baa1bf1a + languageName: node + linkType: hard + +"eyereasoner@npm:^19.0.1": + version: 19.0.1 + resolution: "eyereasoner@npm:19.0.1" dependencies: n3: "npm:^1.16.3" - swipl-wasm: "npm:4.0.13" + swipl-wasm: "npm:6.0.0" peerDependencies: "@rdfjs/types": ^1.1.0 bin: eyereasoner: dist/bin/index.js - checksum: 10c0/a1c78114edc20d94b7cb052305d97bc16e49ece53b7c221cda4309c6e92deb1ed485ba31fff728e4bd1195ee16421e1cf25f5da43225e52939ddac2bca9678ba + checksum: 10c0/320d4d40a9941d979861fb24a949485f3b0655bd85c9519a039e83a4c7da5162fc30bc9d39f43231075ff5557d083bb3317ee8aeb2630b54406ed2ac14eecb25 languageName: node linkType: hard @@ -10758,6 +10768,16 @@ __metadata: languageName: node linkType: hard +"n3@npm:^2.0.1": + version: 2.0.1 + resolution: "n3@npm:2.0.1" + dependencies: + buffer: "npm:^6.0.3" + readable-stream: "npm:^4.0.0" + checksum: 10c0/f8c6de2052004c83b8870445e844ce6022d34a91807003b407c696b12e56cc8d74c987fa4103fef05a3544b4a70b44b7219500f609808181207fde34faac0a32 + languageName: node + linkType: hard + "nanoid@npm:^3.3.11": version: 3.3.11 resolution: "nanoid@npm:3.3.11" @@ -10979,15 +10999,16 @@ __metadata: languageName: node linkType: hard -"odrl-evaluator@npm:^0.5.0": - version: 0.5.0 - resolution: "odrl-evaluator@npm:0.5.0" +"odrl-evaluator@npm:^0.6.0": + version: 0.6.0 + resolution: "odrl-evaluator@npm:0.6.0" dependencies: "@jeswr/pretty-turtle": "npm:^1.8.2" "@rdfjs/types": "npm:^1.1.0" "@types/n3": "npm:^1.16.3" commit-and-tag-version: "npm:^12.6.0" - eyereasoner: "npm:^16.18.4" + eyeling: "npm:^1.10.6" + eyereasoner: "npm:^19.0.1" n3: "npm:^1.20.4" odrl-atomizer: "npm:^0.1.2" rdf-isomorphic: "npm:^1.3.1" @@ -10998,7 +11019,7 @@ __metadata: streamify-string: "npm:^1.0.1" tmp: "npm:^0.2.3" uuidv4: "npm:^6.2.13" - checksum: 10c0/38058c3d7342a939a65bf93de6b23ee332497966c4043543246cf10f6238cec72d10115b19dbf647cad4a4fe55e43f9e18f3482cbaf31ae71ff6fc894e0c3ca5 + checksum: 10c0/0df8d6cb738d159e83ae742af1d70c704c2c865e244c2761f1ab45d7d63ce5f974909f77bfc3659271002b326e0bc4e894f1d8785ce54caf6b2d638b33e47a31 languageName: node linkType: hard @@ -11360,6 +11381,19 @@ __metadata: languageName: node linkType: hard +"policy-conflict-resolver@npm:^0.0.2": + version: 0.0.2 + resolution: "policy-conflict-resolver@npm:0.0.2" + dependencies: + "@rdfjs/types": "npm:^2.0.1" + "@types/n3": "npm:^1.21.1" + asynchronous-handlers: "npm:^1.0.2" + n3: "npm:^2.0.1" + rdf-vocabulary: "npm:^1.0.0" + checksum: 10c0/1d2d07b7d811907bd991507a6e83463550bcdc5eee308b29a587d805993c1e3ee152afac98affd420b132ecd5c3da4e3148cf807588bdafc3f41e49ef351192d + languageName: node + linkType: hard + "postcss@npm:^8.5.3": version: 8.5.6 resolution: "postcss@npm:8.5.6" @@ -12986,14 +13020,14 @@ __metadata: languageName: node linkType: hard -"swipl-wasm@npm:4.0.13": - version: 4.0.13 - resolution: "swipl-wasm@npm:4.0.13" +"swipl-wasm@npm:6.0.0": + version: 6.0.0 + resolution: "swipl-wasm@npm:6.0.0" dependencies: - "@types/emscripten": "npm:^1.39.13" + "@types/emscripten": "npm:^1.41.5" bin: swipl-generate: dist/bin/index.js - checksum: 10c0/ea61942ceb60883bddd213d029ce21bb7010860621b69dfd1e13209b17948c90bc3c422153244a8662f5eb89275135b3905be5274d827ce4f2effac6a383f038 + checksum: 10c0/89db476938488e4a24e417ebc813a427f204520b9829383995e8ca26a1796859fcc6f32481acbeaac95b78e7d6e377dbffb2096cefa6dc18870b71902ad1eeb8 languageName: node linkType: hard From 59b671367287fc672ce6d763449895ba7edd75af Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Mon, 26 Jan 2026 09:47:25 +0100 Subject: [PATCH 2/3] fix: update odrl-conflict-resolver package resulting in build working again --- packages/uma/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/uma/package.json b/packages/uma/package.json index 0f1f257..afcc6c7 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -74,7 +74,7 @@ "ms": "^2.1.3", "n3": "^1.17.2", "odrl-evaluator": "^0.6.0", - "policy-conflict-resolver": "^0.0.2", + "policy-conflict-resolver": "^0.0.3", "rdf-string": "^2.0.1", "rdf-vocabulary": "^1.0.1", "uri-template-lite": "^23.4.0", diff --git a/yarn.lock b/yarn.lock index d69148e..f5db91b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5657,7 +5657,7 @@ __metadata: ms: "npm:^2.1.3" n3: "npm:^1.17.2" odrl-evaluator: "npm:^0.6.0" - policy-conflict-resolver: "npm:^0.0.2" + policy-conflict-resolver: "npm:^0.0.3" rdf-string: "npm:^2.0.1" rdf-vocabulary: "npm:^1.0.1" uri-template-lite: "npm:^23.4.0" @@ -11381,16 +11381,16 @@ __metadata: languageName: node linkType: hard -"policy-conflict-resolver@npm:^0.0.2": - version: 0.0.2 - resolution: "policy-conflict-resolver@npm:0.0.2" +"policy-conflict-resolver@npm:^0.0.3": + version: 0.0.3 + resolution: "policy-conflict-resolver@npm:0.0.3" dependencies: "@rdfjs/types": "npm:^2.0.1" "@types/n3": "npm:^1.21.1" asynchronous-handlers: "npm:^1.0.2" n3: "npm:^2.0.1" rdf-vocabulary: "npm:^1.0.0" - checksum: 10c0/1d2d07b7d811907bd991507a6e83463550bcdc5eee308b29a587d805993c1e3ee152afac98affd420b132ecd5c3da4e3148cf807588bdafc3f41e49ef351192d + checksum: 10c0/e96677faf1a1e136ee41574d85ba7ce6953ce8bce7ad47376f1548183375e951f1abae2d52c1e44de8afb1902936578972faa469b90623a905becb285174ca96 languageName: node linkType: hard From 96d12867c6daf69d7dfd4864f5ec84509758ca86 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 16 Feb 2026 12:48:17 +0100 Subject: [PATCH 3/3] feat: Use eyeling for reasoning This removes the need for eye reasoner to be installed --- .github/workflows/test.yml | 11 ----------- Dockerfile | 7 ------- README.md | 3 +-- packages/uma/bin/demo.js | 1 - packages/uma/bin/main.js | 1 - packages/uma/bin/odrl.js | 1 - packages/uma/config/odrl.json | 1 - packages/uma/config/policies/authorizers/default.json | 1 - packages/uma/config/variables/default.json | 5 ----- .../uma/src/policies/authorizers/OdrlAuthorizer.ts | 9 ++------- test/integration/AccessRequests.test.ts | 1 - test/integration/Aggregation.test.ts | 1 - test/integration/Base.test.ts | 1 - test/integration/Collections.test.ts | 1 - test/integration/Demo.test.ts | 1 - test/integration/Odrl.test.ts | 1 - test/integration/Oidc.test.ts | 1 - test/integration/Policies.test.ts | 1 - 18 files changed, 3 insertions(+), 45 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7aee23..1514d5a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,17 +29,6 @@ jobs: steps: - - name: Install prolog - uses: logtalk-actions/setup-swi-prolog@master - - - name: Clone EYE repo - uses: actions/checkout@v4 - with: - repository: eyereasoner/eye - - - name: Build EYE - run: bash install.sh --prefix=$HOME/.local # This folder is available on $PATH already - - name: Checkout main branch uses: actions/checkout@v4 diff --git a/Dockerfile b/Dockerfile index 9343125..3b3971b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,6 @@ FROM node:22 ENV NODE_ENV=production -# Install EYE reasoner -RUN apt-get update \ - && apt-get install swi-prolog -y \ - && git clone https://github.com/eyereasoner/eye.git \ - && /eye/install.sh --prefix=/usr/local \ - && rm -r /eye - WORKDIR /usr/src/app COPY . . diff --git a/README.md b/README.md index 63c7b2c..c0e7543 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,7 @@ This repository contains SolidLab research artefacts on use of UMA in the Solid In order to run this project you need to perform the following steps. -1. Install the [eye reasoner](https://github.com/eyereasoner/eye/) and have `eye` available on your path. -2. Ensure that you are using Node.js 20 or higher, e.g. by running `nvm use`. (see [.nvmrc](./.nvmrc)) +1. Ensure that you are using Node.js 20 or higher, e.g. by running `nvm use`. (see [.nvmrc](./.nvmrc)) 2. Enable Node.js Corepack with `corepack enable`. 3. Run `yarn install` in the project root (this will automatically call `yarn build`). 4. Run `yarn start`. diff --git a/packages/uma/bin/demo.js b/packages/uma/bin/demo.js index 8c5072d..5984e22 100644 --- a/packages/uma/bin/demo.js +++ b/packages/uma/bin/demo.js @@ -17,7 +17,6 @@ const launch = async () => { // variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/policy'); variables['urn:uma:variables:policyContainer'] = 'http://localhost:3000/settings/policies/'; - variables['urn:uma:variables:eyePath'] = 'eye'; variables['urn:uma:variables:backupFilePath'] = ''; const configPath = path.join(rootDir, './config/demo.json'); diff --git a/packages/uma/bin/main.js b/packages/uma/bin/main.js index 0d54410..9a0e812 100644 --- a/packages/uma/bin/main.js +++ b/packages/uma/bin/main.js @@ -39,7 +39,6 @@ const launch = async () => { variables['urn:uma:variables:port'] = argv.port; variables['urn:uma:variables:baseUrl'] = argv.baseUrl ?? `http://localhost:${argv.port}/uma`; - variables['urn:uma:variables:eyePath'] = 'eye'; variables['urn:uma:variables:backupFilePath'] = argv.backupFilePath; const configPath = path.join(rootDir, './config/default.json'); diff --git a/packages/uma/bin/odrl.js b/packages/uma/bin/odrl.js index 91f2517..6abb1f1 100644 --- a/packages/uma/bin/odrl.js +++ b/packages/uma/bin/odrl.js @@ -17,7 +17,6 @@ const launch = async () => { variables['urn:uma:variables:policyBaseIRI'] = 'http://localhost:3000/'; variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/odrl'); - variables['urn:uma:variables:eyePath'] = 'eye'; variables['urn:uma:variables:backupFilePath'] = ''; const configPath = path.join(rootDir, './config/odrl.json'); diff --git a/packages/uma/config/odrl.json b/packages/uma/config/odrl.json index e193137..479a77a 100644 --- a/packages/uma/config/odrl.json +++ b/packages/uma/config/odrl.json @@ -12,7 +12,6 @@ "overrideInstance": { "@id": "urn:uma:default:Authorizer" }, "overrideParameters": { "@type": "OdrlAuthorizer", - "eyePath": { "@id": "urn:uma:variables:eyePath" }, "policies": { "@id": "urn:uma:default:RulesStorage" } diff --git a/packages/uma/config/policies/authorizers/default.json b/packages/uma/config/policies/authorizers/default.json index 19775a3..67f2799 100644 --- a/packages/uma/config/policies/authorizers/default.json +++ b/packages/uma/config/policies/authorizers/default.json @@ -52,7 +52,6 @@ "fallback": { "@id": "urn:uma:default:OdrlAuthorizer", "@type": "OdrlAuthorizer", - "eyePath": { "@id": "urn:uma:variables:eyePath" }, "policies": { "@id": "urn:uma:default:RulesStorage", "@type": "FileBackupUCRulesStorage", diff --git a/packages/uma/config/variables/default.json b/packages/uma/config/variables/default.json index c355cdb..63b9ddf 100644 --- a/packages/uma/config/variables/default.json +++ b/packages/uma/config/variables/default.json @@ -22,11 +22,6 @@ "@id": "urn:uma:variables:policyBaseIRI", "@type": "Variable" }, - { - "comment": "Path of the local eye reasoner.", - "@id": "urn:uma:variables:eyePath", - "@type": "Variable" - }, { "comment": "URL of container where policies are stored.", "@id": "urn:uma:variables:policyContainer", diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index 6f9e925..8e791e7 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -1,7 +1,7 @@ import { BadRequestHttpError, DC, RDF } from '@solid/community-server'; import { getLoggerFor } from 'global-logger-factory'; import { DataFactory, Quad, Store, Writer } from 'n3'; -import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator'; +import { EyelingReasoner, EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator'; import { CLIENTID, WEBID } from '../../credentials/Claims'; import { ClaimSet } from '../../credentials/ClaimSet'; import { basicPolicy } from '../../ucp/policy/ODRL'; @@ -41,16 +41,11 @@ export class OdrlAuthorizer implements Authorizer { * * * @param policies - A store containing the ODRL policy rules. - * @param eyePath - The path to run the local EYE reasoner, if there is one. */ constructor( private readonly policies: UCRulesStorage, - eyePath?: string, ) { - const engine = eyePath ? - new ODRLEngineMultipleSteps({ reasoner: new EyeReasoner(eyePath, ["--quiet", "--nope", "--pass-only-new"]) }) : - new ODRLEngineMultipleSteps(); - this.odrlEvaluator = new ODRLEvaluator(engine); + this.odrlEvaluator = new ODRLEvaluator(new ODRLEngineMultipleSteps({ reasoner: new EyelingReasoner()})); this.strategy = new PrioritizeProhibitionStrategy(); } diff --git a/test/integration/AccessRequests.test.ts b/test/integration/AccessRequests.test.ts index 72cd84a..4bb4c56 100644 --- a/test/integration/AccessRequests.test.ts +++ b/test/integration/AccessRequests.test.ts @@ -28,7 +28,6 @@ describe('An access request server setup', (): void => { { 'urn:uma:variables:port': umaPort, 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, - 'urn:uma:variables:eyePath': 'eye', 'urn:uma:variables:backupFilePath': '', } ) as App; diff --git a/test/integration/Aggregation.test.ts b/test/integration/Aggregation.test.ts index 80c5f1b..2296fb5 100644 --- a/test/integration/Aggregation.test.ts +++ b/test/integration/Aggregation.test.ts @@ -26,7 +26,6 @@ function getUmaApp(port: number): Promise { { 'urn:uma:variables:port': port, 'urn:uma:variables:baseUrl': `http://localhost:${port}/uma`, - 'urn:uma:variables:eyePath': 'eye', 'urn:uma:variables:backupFilePath': '', } ); diff --git a/test/integration/Base.test.ts b/test/integration/Base.test.ts index 7121171..dcf25d9 100644 --- a/test/integration/Base.test.ts +++ b/test/integration/Base.test.ts @@ -22,7 +22,6 @@ describe('A server setup', (): void => { { 'urn:uma:variables:port': umaPort, 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, - 'urn:uma:variables:eyePath': 'eye', 'urn:uma:variables:backupFilePath': '', } ); diff --git a/test/integration/Collections.test.ts b/test/integration/Collections.test.ts index b6e88db..cdc9a34 100644 --- a/test/integration/Collections.test.ts +++ b/test/integration/Collections.test.ts @@ -21,7 +21,6 @@ describe('A server with collections', (): void => { { 'urn:uma:variables:port': umaPort, 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, - 'urn:uma:variables:eyePath': 'eye', 'urn:uma:variables:backupFilePath': '', } ); diff --git a/test/integration/Demo.test.ts b/test/integration/Demo.test.ts index a82b66d..26e8c59 100644 --- a/test/integration/Demo.test.ts +++ b/test/integration/Demo.test.ts @@ -34,7 +34,6 @@ describe('A demo server setup', (): void => { { 'urn:uma:variables:port': umaPort, 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, - 'urn:uma:variables:eyePath': 'eye', 'urn:uma:variables:policyContainer': policyContainer, 'urn:uma:variables:backupFilePath': '', } diff --git a/test/integration/Odrl.test.ts b/test/integration/Odrl.test.ts index c640dde..25af20c 100644 --- a/test/integration/Odrl.test.ts +++ b/test/integration/Odrl.test.ts @@ -23,7 +23,6 @@ describe('An ODRL server setup', (): void => { { 'urn:uma:variables:port': umaPort, 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, - 'urn:uma:variables:eyePath': 'eye', 'urn:uma:variables:backupFilePath': '', } ); diff --git a/test/integration/Oidc.test.ts b/test/integration/Oidc.test.ts index 854f01a..29fe029 100644 --- a/test/integration/Oidc.test.ts +++ b/test/integration/Oidc.test.ts @@ -29,7 +29,6 @@ describe('A server supporting OIDC tokens', (): void => { { 'urn:uma:variables:port': umaPort, 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, - 'urn:uma:variables:eyePath': 'eye', 'urn:uma:variables:backupFilePath': '', } ); diff --git a/test/integration/Policies.test.ts b/test/integration/Policies.test.ts index a413468..4a045f3 100644 --- a/test/integration/Policies.test.ts +++ b/test/integration/Policies.test.ts @@ -88,7 +88,6 @@ describe('A policy server setup', (): void => { { 'urn:uma:variables:port': umaPort, 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, - 'urn:uma:variables:eyePath': 'eye', 'urn:uma:variables:backupFilePath': '', } ) as App;