From b47d4fa655f7cdc4b37d1d678766772aa7134296 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Tue, 8 Apr 2025 14:22:35 +0200 Subject: [PATCH 1/9] WIP: experiment materializePolicy using SDS24 paper dynamic policy algorithm --- package-lock.json | 124 +++++++++++++++++++++++++++++++++ package.json | 4 +- test.ts | 171 ++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 3 +- 4 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 test.ts diff --git a/package-lock.json b/package-lock.json index 33a8bde..9523d3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "0.2.2", "license": "MIT", "dependencies": { + "@rdfjs/dataset": "^2.0.2", "@rdfjs/types": "^1.1.0", "@types/n3": "^1.16.3", + "clownface-shacl-path": "^2.4.0", "eyereasoner": "^16.18.4", "n3": "^1.20.4", "rdf-isomorphic": "^1.3.1", @@ -1151,6 +1153,56 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@rdfjs/data-model": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-2.1.0.tgz", + "integrity": "sha512-pnjwSqDCXxxJQPm3TyDaqoWynYYQBl1pZC7rIPhdck7RbcEVF8hIBg5vXXosUbNcW3qwyAEBtYGojoWRnxBPew==", + "bin": { + "rdfjs-data-model-test": "bin/test.js" + } + }, + "node_modules/@rdfjs/dataset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rdfjs/dataset/-/dataset-2.0.2.tgz", + "integrity": "sha512-6YJx+5n5Uxzq9dd9I0GGcIo6eopZOPfcsAfxSGX5d+YBzDgVa1cbtEBFnaPyPKiQsOm4+Cr3nwypjpg02YKPlA==", + "bin": { + "rdfjs-dataset-test": "bin/test.js" + } + }, + "node_modules/@rdfjs/environment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rdfjs/environment/-/environment-1.0.0.tgz", + "integrity": "sha512-+S5YjSvfoQR5r7YQCRCCVHvIEyrWia7FJv2gqM3s5EDfotoAQmFeBagApa9c/eQFi5EiNhmBECE5nB8LIxTaHg==" + }, + "node_modules/@rdfjs/namespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rdfjs/namespace/-/namespace-2.0.1.tgz", + "integrity": "sha512-U85NWVGnL3gWvOZ4eXwUcv3/bom7PAcutSBQqmVWvOaslPy+kDzAJCH1WYBLpdQd4yMmJ+bpJcDl9rcHtXeixg==", + "dependencies": { + "@rdfjs/data-model": "^2.0.1" + } + }, + "node_modules/@rdfjs/term-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rdfjs/term-map/-/term-map-2.0.2.tgz", + "integrity": "sha512-EJ2FmmdEUsSR/tU1nrizRLWzH24YzhuvesrbUWxC3Fs0ilYNdtTbg0RaFJDUnJF3HkbNBQe8Zrt/uvU/hcKnHg==", + "dependencies": { + "@rdfjs/to-ntriples": "^3.0.1" + } + }, + "node_modules/@rdfjs/term-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@rdfjs/term-set/-/term-set-2.0.3.tgz", + "integrity": "sha512-DyXrKWEx+mtAFUZVU7bc3Va6/KZ8PsIp0RVdyWT9jfDgI/HCvNisZaBtAcm+SYTC45o+7WLkbudkk1bfaKVB0A==", + "dependencies": { + "@rdfjs/to-ntriples": "^3.0.1" + } + }, + "node_modules/@rdfjs/to-ntriples": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@rdfjs/to-ntriples/-/to-ntriples-3.0.1.tgz", + "integrity": "sha512-gjoPAvh4j7AbGMjcDn/8R4cW+d/FPtbfbMM0uQXkyfBFtNUW2iVgrqsgJ65roLc54Y9A2TTFaeeTGSvY9a0HCQ==" + }, "node_modules/@rdfjs/types": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.1.2.tgz", @@ -1210,6 +1262,39 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@tpluscode/rdf-ns-builders": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@tpluscode/rdf-ns-builders/-/rdf-ns-builders-5.0.0.tgz", + "integrity": "sha512-rtMFbArdief+s0z2A3TOb/gNe5O5xn9LDiEpilCf6lGYCUIfyqoOvZY80fS/eILwcF2Mj6cUQN1WBQ+1neJmaw==", + "dependencies": { + "@rdfjs/data-model": "^2.1.0", + "@rdfjs/namespace": "^2.0.1", + "@rdfjs/types": "^2", + "@types/rdfjs__namespace": "^2.0.10", + "@zazuko/prefixes": "^2.3.0" + } + }, + "node_modules/@tpluscode/rdf-ns-builders/node_modules/@rdfjs/types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-2.0.1.tgz", + "integrity": "sha512-uyAzpugX7KekAXAHq26m3JlUIZJOC0uSBhpnefGV5i15bevDyyejoB7I+9MKeUrzXD8OOUI3+4FeV1wwQr5ihA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@tpluscode/rdf-string": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@tpluscode/rdf-string/-/rdf-string-1.3.4.tgz", + "integrity": "sha512-bWdUgsJC84CFYPQ0GCatA7rZqQme12ubogqm81gOd+m/5pK/Ua5yK8iYGLh0ArtPCmYqXm160UCOCLm17h1AUw==", + "dependencies": { + "@rdfjs/data-model": "^2.0.0", + "@rdfjs/environment": "^1.0.0", + "@rdfjs/term-map": "^2.0.0", + "@rdfjs/types": "*", + "@tpluscode/rdf-ns-builders": ">=3", + "@zazuko/prefixes": ">=1" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1348,6 +1433,14 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/rdfjs__namespace": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/rdfjs__namespace/-/rdfjs__namespace-2.0.10.tgz", + "integrity": "sha512-xoVzEIOxcpyteEmzaj94MSBbrBFs+vqv05joMhzLEiPRwsBBDnhkdBCaaDxR1Tf7wOW0kB2R1IYe4C3vEBFPgA==", + "dependencies": { + "@rdfjs/types": "*" + } + }, "node_modules/@types/readable-stream": { "version": "2.3.15", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", @@ -1395,6 +1488,11 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, + "node_modules/@zazuko/prefixes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@zazuko/prefixes/-/prefixes-2.4.0.tgz", + "integrity": "sha512-bd53k5XgFKWR56sofHeAcIbv8o0m2HsJlbHaHbrMufUCdgiZsCLvZn84Vh1dhcsyBHOD0EIo9AD4pNWDQLVRaw==" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1850,6 +1948,32 @@ "node": ">=12" } }, + "node_modules/clownface": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/clownface/-/clownface-2.0.3.tgz", + "integrity": "sha512-E76TBJ7CgU9+/5paSAvuNdMO+fzFThnvRVtidosktYppYkXM8V7tid8Ezzo8S1OmoWxKUam3yfkZlfCid4OiJQ==", + "peer": true, + "dependencies": { + "@rdfjs/data-model": "^2.0.1", + "@rdfjs/environment": "0 - 1", + "@rdfjs/namespace": "^2.0.0" + } + }, + "node_modules/clownface-shacl-path": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/clownface-shacl-path/-/clownface-shacl-path-2.4.0.tgz", + "integrity": "sha512-0kVoROr51QAYbh3q/IHTXF3lwUgWF2xhZ66/l6Q244VghoZ5JTWON0/1Lp6Jjg2UXIe14MvAaUk2mW0IiwUtKQ==", + "dependencies": { + "@rdfjs/term-map": "^2.0.0", + "@rdfjs/term-set": "^2.0.1", + "@tpluscode/rdf-ns-builders": ">=3.0.2", + "@tpluscode/rdf-string": "^1.3.1" + }, + "peerDependencies": { + "@types/sparqljs": "^3.1.10", + "clownface": "1 - 2" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", diff --git a/package.json b/package.json index c4c15b7..b49ac3b 100644 --- a/package.json +++ b/package.json @@ -39,14 +39,16 @@ "@types/jest": "^29.5.12", "@types/tmp": "^0.2.6", "jest": "^29.7.0", - "ts-jest": "^29.2.5", "jest-rdf": "^2.0.0", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.4.5" }, "dependencies": { + "@rdfjs/dataset": "^2.0.2", "@rdfjs/types": "^1.1.0", "@types/n3": "^1.16.3", + "clownface-shacl-path": "^2.4.0", "eyereasoner": "^16.18.4", "n3": "^1.20.4", "rdf-isomorphic": "^1.3.1", diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..cccf2bd --- /dev/null +++ b/test.ts @@ -0,0 +1,171 @@ +import { NamedNode, Parser, Store, Writer, DataFactory, Quad } from 'n3'; +import { ODRLEngineMultipleSteps, ODRLEvaluator } from "./dist/index"; + +const { namedNode, quad } = DataFactory +// Test algorithm for "Dynamic ODRL Specification" +// see paper "Interoperable and Continuous Usage Control Enforcement in Dataspaces" https://raw.githubusercontent.com/woutslabbinck/papers/main/2024/Interoperable_and_Continuous_Usage_Control_Enforcement_in_Dataspaces.pdf + +// need to use https://www.npmjs.com/package/clownface-shacl-path +// how to use TS module: https://github.com/woutslabbinck/ODRL-shape/blob/main/index.ts#L17 + + +// Variant on test case 036: Read request from Alice to resource X returns into yes (temporal lt) (Alice Request Read X). +// https://github.com/SolidLabResearch/ODRL-Test-Suite/blob/main/data/test_cases/testcase-036-alice-read-x.ttl +const policy = ` +@prefix odrl: . +@prefix ex: . +@prefix dct: . +@prefix odrluc: . + + a odrl:Set ; + odrl:uid ; + dct:description "ALICE may READ resource X when it is before 2024-02-12T11:20:10.999Z." ; + dct:source ; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:action odrl:read ; + odrl:target ex:x ; + odrl:constraint . + + odrl:leftOperand odrl:dateTime ; + odrl:operator odrl:lt ; + odrl:rightOperandReference ex:operandReference1 . + +ex:operandReference1 a odrluc:OperandReference ; + odrluc:reference ex:externalSource ; + odrluc:path ex:updatedValue . +` + +const sotw = ` +@prefix ex: . +@prefix temp: . +@prefix dct: . + + a ex:Sotw ; + ex:includes temp:currentTime . + +temp:currentTime dct:issued "2017-02-12T11:20:10.999Z"^^ . + +# external value that will be materialized in the policy +ex:externalSource ex:updatedValue "2018-02-12T11:20:10.999Z"^^ . +` + +const request = ` +@prefix odrl: . +@prefix ex: . +@prefix dct: . + + a odrl:Request ; + odrl:uid ; + dct:description "Requesting Party ALICE requests to READ resource X." ; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:action odrl:read ; + odrl:target ex:x . +` + +async function main() { + const parser = new Parser() + const writer = new Writer() + + const odrlDynamicPolicyQuads = parser.parse(policy) + + const odrlPolicyQuads: Quad[] = [] + const odrlRequestQuads = parser.parse(request) + const stateOfTheWorldQuads = parser.parse(sotw) + + // TODO: algorithm to fetch value using SHACL path from sotw in policy + + //@ts-ignore + const { findNodes } = await import('clownface-shacl-path') + //@ts-ignore + const clownface = (await import('clownface')).default + //@ts-ignore + const { dataset } = await import('@rdfjs/dataset'); + + const odrlPolicyStore = new Store(odrlDynamicPolicyQuads) + const odrlDynamicPolicyStore = new Store(odrlDynamicPolicyQuads) + const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects("https://www.w3.org/TR/rdf-schema#type", 'http://example.org/odrluc#OperandReference', null); + + const operandReferenceNode = operandReferenceNodes[0] + const constraintNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/ns/odrl/2/rightOperandReference", operandReferenceNode, null) + console.log(constraintNodes); + + const operandReferencePath = odrlDynamicPolicyStore.getObjects(operandReferenceNode, 'http://example.org/odrluc#path', null) + console.log(operandReferencePath[0]); // TODO: should be extracted properly + + const nodes = findNodes(clownface({ dataset: new Store(stateOfTheWorldQuads) }), operandReferencePath[0]) + const instantiatedValue = nodes.terms[0]; // according to our algorithm, there should be only one term + console.log(instantiatedValue); + + + for (const constraintNode of constraintNodes) { + // remove reference thingy + odrlPolicyStore.removeQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperandReference"), operandReferenceNode) + const rightOperandTriple = quad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperand"), instantiatedValue) + odrlPolicyStore.addQuads([rightOperandTriple]) + + console.log(writer.quadsToString([rightOperandTriple])) + } + + odrlPolicyQuads.push(...odrlPolicyStore.getQuads(null, null, null, null)) + + // const instantiatedPolicyQuads = materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) + // console.log(writer.quadsToString(instantiatedPolicyQuads)); + + // odrlPolicyQuads.push(...instantiatedPolicyQuads) + + // reasoning over new policy + const evaluator = new ODRLEvaluator(new ODRLEngineMultipleSteps()); + const reasoningResult = await evaluator.evaluate( + odrlPolicyQuads, + odrlRequestQuads, + stateOfTheWorldQuads) + + const output = writer.quadsToString(reasoningResult); + console.log(output); + +} +main() + +async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]): Promise { + //@ts-ignore + const { findNodes } = await import('clownface-shacl-path') + //@ts-ignore + const clownface = (await import('clownface')).default + + const odrlPolicyStore = new Store(dynamicPolicy) + + const odrlDynamicPolicyStore = new Store(dynamicPolicy) + const stateOfTheWorldStore = new Store(stateOfTheWorld) + + const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects("https://www.w3.org/TR/rdf-schema#type", 'http://example.org/odrluc#OperandReference', null); + + for (const operandReferenceNode of operandReferenceNodes) { + // ODRL constraint that has that given rightOperandReference + const constraintNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/ns/odrl/2/rightOperandReference", operandReferenceNode, null) + + // SHACL Property path + // TODO: needs to be extracted properly (currently only the most simple form is extracted) + const operandReferencePath = odrlDynamicPolicyStore.getObjects(operandReferenceNode, 'http://example.org/odrluc#path', null) + + // extract SHACL Property Path using clownface + const nodes = findNodes(clownface({ dataset: stateOfTheWorldStore }), operandReferencePath[0]) + const instantiatedValue = nodes.terms[0]; // according to our algorithm (see paper), there should be only one term + + for (const constraintNode of constraintNodes) { + // remove rightOperandRefrence triple from constraint + odrlPolicyStore.removeQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperandReference"), operandReferenceNode) + // add materialized rightOperand + odrlPolicyStore.addQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperand"), instantiatedValue) + } + } + + + // TODO: need a clean up, there is dangling operandReference stuff in the ODRL Policy + return odrlPolicyStore.getQuads(null, null, null, null) +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 341e5f3..c2a8f69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "outDir": "dist", "preserveConstEnums": true, "sourceMap": true, - "stripInternal": true + "stripInternal": true, + "module": "NodeNext", // required for ESM and commonjs library combination: https://stackoverflow.com/questions/71463698/why-we-need-nodenext-typescript-compiler-option-when-we-have-esnext }, "include": [ "src" From bc3c299a995f45472435ee39096588c8fac292d6 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Tue, 8 Apr 2025 16:55:13 +0200 Subject: [PATCH 2/9] feat: clean up materialization --- test.ts | 54 ++++++++++++------------------------------------------ 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/test.ts b/test.ts index cccf2bd..c9b892b 100644 --- a/test.ts +++ b/test.ts @@ -19,7 +19,7 @@ const policy = ` a odrl:Set ; odrl:uid ; - dct:description "ALICE may READ resource X when it is before 2024-02-12T11:20:10.999Z." ; + dct:description "ALICE may READ resource X when it is before 'ex:updateValue' (see sotw)." ; dct:source ; odrl:permission . @@ -67,6 +67,7 @@ const request = ` odrl:action odrl:read ; odrl:target ex:x . ` +const writer = new Writer() async function main() { const parser = new Parser() @@ -78,46 +79,11 @@ async function main() { const odrlRequestQuads = parser.parse(request) const stateOfTheWorldQuads = parser.parse(sotw) - // TODO: algorithm to fetch value using SHACL path from sotw in policy - - //@ts-ignore - const { findNodes } = await import('clownface-shacl-path') - //@ts-ignore - const clownface = (await import('clownface')).default - //@ts-ignore - const { dataset } = await import('@rdfjs/dataset'); - - const odrlPolicyStore = new Store(odrlDynamicPolicyQuads) - const odrlDynamicPolicyStore = new Store(odrlDynamicPolicyQuads) - const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects("https://www.w3.org/TR/rdf-schema#type", 'http://example.org/odrluc#OperandReference', null); - - const operandReferenceNode = operandReferenceNodes[0] - const constraintNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/ns/odrl/2/rightOperandReference", operandReferenceNode, null) - console.log(constraintNodes); - - const operandReferencePath = odrlDynamicPolicyStore.getObjects(operandReferenceNode, 'http://example.org/odrluc#path', null) - console.log(operandReferencePath[0]); // TODO: should be extracted properly - - const nodes = findNodes(clownface({ dataset: new Store(stateOfTheWorldQuads) }), operandReferencePath[0]) - const instantiatedValue = nodes.terms[0]; // according to our algorithm, there should be only one term - console.log(instantiatedValue); - - - for (const constraintNode of constraintNodes) { - // remove reference thingy - odrlPolicyStore.removeQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperandReference"), operandReferenceNode) - const rightOperandTriple = quad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperand"), instantiatedValue) - odrlPolicyStore.addQuads([rightOperandTriple]) - - console.log(writer.quadsToString([rightOperandTriple])) - } - - odrlPolicyQuads.push(...odrlPolicyStore.getQuads(null, null, null, null)) - - // const instantiatedPolicyQuads = materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) - // console.log(writer.quadsToString(instantiatedPolicyQuads)); + const instantiatedPolicyQuads = await materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) - // odrlPolicyQuads.push(...instantiatedPolicyQuads) + odrlPolicyQuads.push(...instantiatedPolicyQuads) + + console.log(writer.quadsToString(odrlPolicyQuads)); // reasoning over new policy const evaluator = new ODRLEvaluator(new ODRLEngineMultipleSteps()); @@ -132,6 +98,8 @@ async function main() { } main() +// algorithm to fetch value using SHACL path from sotw in policy +// Note: currently it only fetches a simple shacl property path async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]): Promise { //@ts-ignore const { findNodes } = await import('clownface-shacl-path') @@ -143,9 +111,11 @@ async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]) const odrlDynamicPolicyStore = new Store(dynamicPolicy) const stateOfTheWorldStore = new Store(stateOfTheWorld) - const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects("https://www.w3.org/TR/rdf-schema#type", 'http://example.org/odrluc#OperandReference', null); + + const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/1999/02/22-rdf-syntax-ns#type", 'http://example.org/odrluc#OperandReference', null); for (const operandReferenceNode of operandReferenceNodes) { + // ODRL constraint that has that given rightOperandReference const constraintNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/ns/odrl/2/rightOperandReference", operandReferenceNode, null) @@ -156,7 +126,7 @@ async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]) // extract SHACL Property Path using clownface const nodes = findNodes(clownface({ dataset: stateOfTheWorldStore }), operandReferencePath[0]) const instantiatedValue = nodes.terms[0]; // according to our algorithm (see paper), there should be only one term - + for (const constraintNode of constraintNodes) { // remove rightOperandRefrence triple from constraint odrlPolicyStore.removeQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperandReference"), operandReferenceNode) From bb224d29bc2c1c6212b99b592a5a9b309bedf296 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 9 Apr 2025 12:03:05 +0200 Subject: [PATCH 3/9] feat: add proper shacl property path extraction thanks to RDF Lens --- package-lock.json | 61 ++++++++++++++++++ package.json | 1 + test-attempt2.ts | 153 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 test-attempt2.ts diff --git a/package-lock.json b/package-lock.json index 9523d3e..d0d57ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "eyereasoner": "^16.18.4", "n3": "^1.20.4", "rdf-isomorphic": "^1.3.1", + "rdf-lens": "^1.3.5", "rdf-parse": "^3.0.0", "rdf-store-stream": "^2.0.1", "rdf-vocabulary": "^1.0.0", @@ -1295,6 +1296,17 @@ "@zazuko/prefixes": ">=1" } }, + "node_modules/@treecg/types": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@treecg/types/-/types-0.4.6.tgz", + "integrity": "sha512-iL8rBhAX4INvfuHZzJ1RELh1i/fspviylENZTk6D2p/xDazY1pTkd+y/hxKYVWCC0jHNolDWRsH7ozwaFGNQKA==", + "dependencies": { + "@rdfjs/types": "*", + "loglevel": "^1.8.1", + "loglevel-plugin-prefix": "^0.8.4", + "rdf-data-factory": "^1.1.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3628,6 +3640,23 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4110,6 +4139,38 @@ "rdf-terms": "^1.7.0" } }, + "node_modules/rdf-lens": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/rdf-lens/-/rdf-lens-1.3.5.tgz", + "integrity": "sha512-tSHPMRNnkJEYUXgm9Fb6Xbhc54wXfSsc9eIH3PkK6NLTGRUIMDVCcLxXVkj+IyvkemCjWPsweAx2/FX9ZwLZUQ==", + "dependencies": { + "@rdfjs/types": "^2.0.1", + "@treecg/types": "^0.4.6", + "@types/n3": "^1.21.1", + "n3": "^1.23.1", + "rdf-data-factory": "^2.0.2" + } + }, + "node_modules/rdf-lens/node_modules/@rdfjs/types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-2.0.1.tgz", + "integrity": "sha512-uyAzpugX7KekAXAHq26m3JlUIZJOC0uSBhpnefGV5i15bevDyyejoB7I+9MKeUrzXD8OOUI3+4FeV1wwQr5ihA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rdf-lens/node_modules/rdf-data-factory": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", + "integrity": "sha512-WzPoYHwQYWvIP9k+7IBLY1b4nIDitzAK4mA37WumAF/Cjvu/KOtYJH9IPZnUTWNSd5K2+pq4vrcE9WZC4sRHhg==", + "dependencies": { + "@rdfjs/types": "^2.0.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/rubensworks/" + } + }, "node_modules/rdf-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/rdf-parse/-/rdf-parse-3.0.0.tgz", diff --git a/package.json b/package.json index b49ac3b..10f7c0c 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "eyereasoner": "^16.18.4", "n3": "^1.20.4", "rdf-isomorphic": "^1.3.1", + "rdf-lens": "^1.3.5", "rdf-parse": "^3.0.0", "rdf-store-stream": "^2.0.1", "rdf-vocabulary": "^1.0.0", diff --git a/test-attempt2.ts b/test-attempt2.ts new file mode 100644 index 0000000..ac312db --- /dev/null +++ b/test-attempt2.ts @@ -0,0 +1,153 @@ +// use RDF lens instead of clownface:: https://ceur-ws.org/Vol-3759/paper13.pdf +import { Quad, Term } from "@rdfjs/types"; +import { BasicLensM, Cont, pred, ShaclPath } from "rdf-lens"; +import { NamedNode, Parser, Store, Writer, DataFactory, Literal, BlankNode} from 'n3'; +import { ODRLEngineMultipleSteps, ODRLEvaluator } from "./dist/index"; + +const { namedNode } = DataFactory + +// odlurc path as defined in "Interoperable and Continuous Usage Control Enforcement in Dataspaces" +export const pathLens = pred(namedNode("http://example.org/odrluc#path")) + .one() + .then(ShaclPath); + +// Utility function to extract the SHACL property path of the Dynamic ODRL Policy using `odrluc:path` +export function getPath(id: Term, quads: Quad[]): BasicLensM { + return pathLens.execute({ id, quads }).mapAll((x) => x.id); +} + +// Utility function to extract the value from an external resource using a SHACL Property path +export function usePath(id: Term, quads: Quad[], lens: BasicLensM): Term[] { + return lens.execute({ id, quads }) +} + + +const policy = ` +@prefix odrl: . +@prefix ex: . +@prefix dct: . +@prefix odrluc: . + + a odrl:Set ; + odrl:uid ; + dct:description "ALICE may READ resource X when it is before 'ex:updateValue' (see sotw)." ; + dct:source ; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:action odrl:read ; + odrl:target ex:x ; + odrl:constraint . + + odrl:leftOperand odrl:dateTime ; + odrl:operator odrl:lt ; + odrl:rightOperandReference ex:operandReference1 . + +ex:operandReference1 a odrluc:OperandReference ; + odrluc:reference ex:externalSource ; + odrluc:path ex:updatedValue . +` + + +const sotw = ` +@prefix ex: . +@prefix temp: . +@prefix dct: . + + a ex:Sotw ; + ex:includes temp:currentTime . + +temp:currentTime dct:issued "2017-02-12T11:20:10.999Z"^^ . + +# external value that will be materialized in the policy +ex:externalSource ex:updatedValue "2018-02-12T11:20:10.999Z"^^ . +` + +const request = ` +@prefix odrl: . +@prefix ex: . +@prefix dct: . + + a odrl:Request ; + odrl:uid ; + dct:description "Requesting Party ALICE requests to READ resource X." ; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:action odrl:read ; + odrl:target ex:x . +` + +async function main() { + const parser = new Parser() + const writer = new Writer() + + const odrlDynamicPolicyQuads = parser.parse(policy) + + const odrlPolicyQuads: Quad[] = [] + const odrlRequestQuads = parser.parse(request) + const stateOfTheWorldQuads = parser.parse(sotw) + + const instantiatedPolicyQuads = await materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) + + odrlPolicyQuads.push(...instantiatedPolicyQuads) + + console.log(writer.quadsToString(odrlPolicyQuads)); + + // reasoning over new policy + const evaluator = new ODRLEvaluator(new ODRLEngineMultipleSteps()); + const reasoningResult = await evaluator.evaluate( + odrlPolicyQuads, + odrlRequestQuads, + stateOfTheWorldQuads) + + const output = writer.quadsToString(reasoningResult); + console.log(output); + +} +main() + +// algorithm to fetch value using SHACL path from sotw in policy +export async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]): Promise { + const odrlPolicyStore = new Store(dynamicPolicy) + const odrlDynamicPolicyStore = new Store(dynamicPolicy) + + // get all constraints containing references + const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/1999/02/22-rdf-syntax-ns#type", 'http://example.org/odrluc#OperandReference', null); + + for (const operandReferenceNode of operandReferenceNodes) { + // ODRL constraint that has that given rightOperandReference + const constraintNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/ns/odrl/2/rightOperandReference", operandReferenceNode, null) + + // external resource + const externalResource = odrlDynamicPolicyStore.getObjects(operandReferenceNode, namedNode("http://example.org/odrluc#reference"), null)[0] + if (!externalResource) { + throw Error("There is no source present in the Dynamic Policy Constraint.") + } + + // SHACL Property path + const lens = getPath(operandReferenceNode, dynamicPolicy); + + // extract SHACL Property Path using clownface + const getValue = usePath(externalResource, stateOfTheWorld, lens); + const instantiatedValue = getValue[0]; // according to our algorithm (see paper), there should be only one term + + if (!(instantiatedValue instanceof NamedNode || instantiatedValue instanceof Literal || instantiatedValue instanceof BlankNode)) { + // Now term is a valid Quad_Object + throw Error("Instantiated value is not a proper value."); + } + for (const constraintNode of constraintNodes) { + // remove rightOperandRefrence triple from constraint + odrlPolicyStore.removeQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperandReference"), operandReferenceNode) + // add materialized rightOperand + odrlPolicyStore.addQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperand"), instantiatedValue) + } + } + + + // TODO: need a clean up, there is dangling operandReference stuff in the ODRL Policy + return odrlPolicyStore.getQuads(null, null, null, null) +} + From bc5341a01d9ed40160ec91bedb1c58a719602ba8 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 9 Apr 2025 13:31:58 +0200 Subject: [PATCH 4/9] feat: remove dangling quads using cbd lens --- test-attempt2.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test-attempt2.ts b/test-attempt2.ts index ac312db..9be365e 100644 --- a/test-attempt2.ts +++ b/test-attempt2.ts @@ -1,11 +1,17 @@ // use RDF lens instead of clownface:: https://ceur-ws.org/Vol-3759/paper13.pdf import { Quad, Term } from "@rdfjs/types"; -import { BasicLensM, Cont, pred, ShaclPath } from "rdf-lens"; +import { BasicLensM, Cont, pred, ShaclPath, CBDLens } from "rdf-lens"; import { NamedNode, Parser, Store, Writer, DataFactory, Literal, BlankNode} from 'n3'; import { ODRLEngineMultipleSteps, ODRLEvaluator } from "./dist/index"; const { namedNode } = DataFactory +// TODO: change baseIRI for odrluc to https://w3id.org/force/odrlproposed +// TODO: create mini spec at https://w3id.org/force/odrlproposed and create a vocabulary/ontology there +// Next steps: +// - put proposals Beatriz and I from brainstorm in odrlproposed +// - put discussion Pieter and I yesterday regarding ODRL and RDF Context ASsociation in odrlproposed + // odlurc path as defined in "Interoperable and Continuous Usage Control Enforcement in Dataspaces" export const pathLens = pred(namedNode("http://example.org/odrluc#path")) .one() @@ -144,10 +150,11 @@ export async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: // add materialized rightOperand odrlPolicyStore.addQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperand"), instantiatedValue) } - } - - // TODO: need a clean up, there is dangling operandReference stuff in the ODRL Policy + // remove OperandReference triples from Policy store + const operandReferenceQuads = CBDLens.execute({id:operandReferenceNode, quads: dynamicPolicy}) + odrlPolicyStore.removeQuads(operandReferenceQuads) + } return odrlPolicyStore.getQuads(null, null, null, null) } From 0c04773700602b47286fb8d0ce4951a57c1b1b7c Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 9 Apr 2025 14:18:02 +0200 Subject: [PATCH 5/9] refactor: add materialzePolicy function to ODRL-Evaluator and clean up --- test.ts => demo/dynamic-policy.ts | 57 +--------- package-lock.json | 134 +----------------------- package.json | 5 +- src/evaluator/DynamicConstraint.ts | 66 ++++++++++++ src/index.browser.ts | 12 +-- src/index.core.ts | 12 +++ src/index.ts | 8 +- src/reasoner/EyeJsReasoner.ts | 2 +- src/util/Vocabularies.ts | 42 ++++++++ test-attempt2.ts | 160 ----------------------------- 10 files changed, 135 insertions(+), 363 deletions(-) rename test.ts => demo/dynamic-policy.ts (52%) create mode 100644 src/evaluator/DynamicConstraint.ts create mode 100644 src/index.core.ts create mode 100644 src/util/Vocabularies.ts delete mode 100644 test-attempt2.ts diff --git a/test.ts b/demo/dynamic-policy.ts similarity index 52% rename from test.ts rename to demo/dynamic-policy.ts index c9b892b..aff8892 100644 --- a/test.ts +++ b/demo/dynamic-policy.ts @@ -1,12 +1,6 @@ -import { NamedNode, Parser, Store, Writer, DataFactory, Quad } from 'n3'; -import { ODRLEngineMultipleSteps, ODRLEvaluator } from "./dist/index"; - -const { namedNode, quad } = DataFactory -// Test algorithm for "Dynamic ODRL Specification" -// see paper "Interoperable and Continuous Usage Control Enforcement in Dataspaces" https://raw.githubusercontent.com/woutslabbinck/papers/main/2024/Interoperable_and_Continuous_Usage_Control_Enforcement_in_Dataspaces.pdf - -// need to use https://www.npmjs.com/package/clownface-shacl-path -// how to use TS module: https://github.com/woutslabbinck/ODRL-shape/blob/main/index.ts#L17 +import { Quad } from "@rdfjs/types"; +import { Parser, Writer } from 'n3'; +import { materializePolicy, ODRLEngineMultipleSteps, ODRLEvaluator } from "../dist/index"; // Variant on test case 036: Read request from Alice to resource X returns into yes (temporal lt) (Alice Request Read X). @@ -15,7 +9,7 @@ const policy = ` @prefix odrl: . @prefix ex: . @prefix dct: . -@prefix odrluc: . +@prefix odrluc: . a odrl:Set ; odrl:uid ; @@ -38,6 +32,7 @@ ex:operandReference1 a odrluc:OperandReference ; odrluc:path ex:updatedValue . ` +// state of the world -> with external value indicating the time const sotw = ` @prefix ex: . @prefix temp: . @@ -67,7 +62,6 @@ const request = ` odrl:action odrl:read ; odrl:target ex:x . ` -const writer = new Writer() async function main() { const parser = new Parser() @@ -98,44 +92,3 @@ async function main() { } main() -// algorithm to fetch value using SHACL path from sotw in policy -// Note: currently it only fetches a simple shacl property path -async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]): Promise { - //@ts-ignore - const { findNodes } = await import('clownface-shacl-path') - //@ts-ignore - const clownface = (await import('clownface')).default - - const odrlPolicyStore = new Store(dynamicPolicy) - - const odrlDynamicPolicyStore = new Store(dynamicPolicy) - const stateOfTheWorldStore = new Store(stateOfTheWorld) - - - const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/1999/02/22-rdf-syntax-ns#type", 'http://example.org/odrluc#OperandReference', null); - - for (const operandReferenceNode of operandReferenceNodes) { - - // ODRL constraint that has that given rightOperandReference - const constraintNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/ns/odrl/2/rightOperandReference", operandReferenceNode, null) - - // SHACL Property path - // TODO: needs to be extracted properly (currently only the most simple form is extracted) - const operandReferencePath = odrlDynamicPolicyStore.getObjects(operandReferenceNode, 'http://example.org/odrluc#path', null) - - // extract SHACL Property Path using clownface - const nodes = findNodes(clownface({ dataset: stateOfTheWorldStore }), operandReferencePath[0]) - const instantiatedValue = nodes.terms[0]; // according to our algorithm (see paper), there should be only one term - - for (const constraintNode of constraintNodes) { - // remove rightOperandRefrence triple from constraint - odrlPolicyStore.removeQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperandReference"), operandReferenceNode) - // add materialized rightOperand - odrlPolicyStore.addQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperand"), instantiatedValue) - } - } - - - // TODO: need a clean up, there is dangling operandReference stuff in the ODRL Policy - return odrlPolicyStore.getQuads(null, null, null, null) -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d0d57ec..ab81132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,15 @@ "version": "0.2.2", "license": "MIT", "dependencies": { - "@rdfjs/dataset": "^2.0.2", "@rdfjs/types": "^1.1.0", "@types/n3": "^1.16.3", - "clownface-shacl-path": "^2.4.0", "eyereasoner": "^16.18.4", "n3": "^1.20.4", "rdf-isomorphic": "^1.3.1", "rdf-lens": "^1.3.5", "rdf-parse": "^3.0.0", "rdf-store-stream": "^2.0.1", - "rdf-vocabulary": "^1.0.0", + "rdf-vocabulary": "^1.0.1", "streamify-string": "^1.0.1", "tmp": "^0.2.3", "uuidv4": "^6.2.13" @@ -1154,56 +1152,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rdfjs/data-model": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-2.1.0.tgz", - "integrity": "sha512-pnjwSqDCXxxJQPm3TyDaqoWynYYQBl1pZC7rIPhdck7RbcEVF8hIBg5vXXosUbNcW3qwyAEBtYGojoWRnxBPew==", - "bin": { - "rdfjs-data-model-test": "bin/test.js" - } - }, - "node_modules/@rdfjs/dataset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@rdfjs/dataset/-/dataset-2.0.2.tgz", - "integrity": "sha512-6YJx+5n5Uxzq9dd9I0GGcIo6eopZOPfcsAfxSGX5d+YBzDgVa1cbtEBFnaPyPKiQsOm4+Cr3nwypjpg02YKPlA==", - "bin": { - "rdfjs-dataset-test": "bin/test.js" - } - }, - "node_modules/@rdfjs/environment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rdfjs/environment/-/environment-1.0.0.tgz", - "integrity": "sha512-+S5YjSvfoQR5r7YQCRCCVHvIEyrWia7FJv2gqM3s5EDfotoAQmFeBagApa9c/eQFi5EiNhmBECE5nB8LIxTaHg==" - }, - "node_modules/@rdfjs/namespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@rdfjs/namespace/-/namespace-2.0.1.tgz", - "integrity": "sha512-U85NWVGnL3gWvOZ4eXwUcv3/bom7PAcutSBQqmVWvOaslPy+kDzAJCH1WYBLpdQd4yMmJ+bpJcDl9rcHtXeixg==", - "dependencies": { - "@rdfjs/data-model": "^2.0.1" - } - }, - "node_modules/@rdfjs/term-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@rdfjs/term-map/-/term-map-2.0.2.tgz", - "integrity": "sha512-EJ2FmmdEUsSR/tU1nrizRLWzH24YzhuvesrbUWxC3Fs0ilYNdtTbg0RaFJDUnJF3HkbNBQe8Zrt/uvU/hcKnHg==", - "dependencies": { - "@rdfjs/to-ntriples": "^3.0.1" - } - }, - "node_modules/@rdfjs/term-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@rdfjs/term-set/-/term-set-2.0.3.tgz", - "integrity": "sha512-DyXrKWEx+mtAFUZVU7bc3Va6/KZ8PsIp0RVdyWT9jfDgI/HCvNisZaBtAcm+SYTC45o+7WLkbudkk1bfaKVB0A==", - "dependencies": { - "@rdfjs/to-ntriples": "^3.0.1" - } - }, - "node_modules/@rdfjs/to-ntriples": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@rdfjs/to-ntriples/-/to-ntriples-3.0.1.tgz", - "integrity": "sha512-gjoPAvh4j7AbGMjcDn/8R4cW+d/FPtbfbMM0uQXkyfBFtNUW2iVgrqsgJ65roLc54Y9A2TTFaeeTGSvY9a0HCQ==" - }, "node_modules/@rdfjs/types": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.1.2.tgz", @@ -1263,39 +1211,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@tpluscode/rdf-ns-builders": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@tpluscode/rdf-ns-builders/-/rdf-ns-builders-5.0.0.tgz", - "integrity": "sha512-rtMFbArdief+s0z2A3TOb/gNe5O5xn9LDiEpilCf6lGYCUIfyqoOvZY80fS/eILwcF2Mj6cUQN1WBQ+1neJmaw==", - "dependencies": { - "@rdfjs/data-model": "^2.1.0", - "@rdfjs/namespace": "^2.0.1", - "@rdfjs/types": "^2", - "@types/rdfjs__namespace": "^2.0.10", - "@zazuko/prefixes": "^2.3.0" - } - }, - "node_modules/@tpluscode/rdf-ns-builders/node_modules/@rdfjs/types": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-2.0.1.tgz", - "integrity": "sha512-uyAzpugX7KekAXAHq26m3JlUIZJOC0uSBhpnefGV5i15bevDyyejoB7I+9MKeUrzXD8OOUI3+4FeV1wwQr5ihA==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@tpluscode/rdf-string": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@tpluscode/rdf-string/-/rdf-string-1.3.4.tgz", - "integrity": "sha512-bWdUgsJC84CFYPQ0GCatA7rZqQme12ubogqm81gOd+m/5pK/Ua5yK8iYGLh0ArtPCmYqXm160UCOCLm17h1AUw==", - "dependencies": { - "@rdfjs/data-model": "^2.0.0", - "@rdfjs/environment": "^1.0.0", - "@rdfjs/term-map": "^2.0.0", - "@rdfjs/types": "*", - "@tpluscode/rdf-ns-builders": ">=3", - "@zazuko/prefixes": ">=1" - } - }, "node_modules/@treecg/types": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/@treecg/types/-/types-0.4.6.tgz", @@ -1445,14 +1360,6 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/rdfjs__namespace": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/rdfjs__namespace/-/rdfjs__namespace-2.0.10.tgz", - "integrity": "sha512-xoVzEIOxcpyteEmzaj94MSBbrBFs+vqv05joMhzLEiPRwsBBDnhkdBCaaDxR1Tf7wOW0kB2R1IYe4C3vEBFPgA==", - "dependencies": { - "@rdfjs/types": "*" - } - }, "node_modules/@types/readable-stream": { "version": "2.3.15", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", @@ -1500,11 +1407,6 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, - "node_modules/@zazuko/prefixes": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@zazuko/prefixes/-/prefixes-2.4.0.tgz", - "integrity": "sha512-bd53k5XgFKWR56sofHeAcIbv8o0m2HsJlbHaHbrMufUCdgiZsCLvZn84Vh1dhcsyBHOD0EIo9AD4pNWDQLVRaw==" - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1960,32 +1862,6 @@ "node": ">=12" } }, - "node_modules/clownface": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/clownface/-/clownface-2.0.3.tgz", - "integrity": "sha512-E76TBJ7CgU9+/5paSAvuNdMO+fzFThnvRVtidosktYppYkXM8V7tid8Ezzo8S1OmoWxKUam3yfkZlfCid4OiJQ==", - "peer": true, - "dependencies": { - "@rdfjs/data-model": "^2.0.1", - "@rdfjs/environment": "0 - 1", - "@rdfjs/namespace": "^2.0.0" - } - }, - "node_modules/clownface-shacl-path": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/clownface-shacl-path/-/clownface-shacl-path-2.4.0.tgz", - "integrity": "sha512-0kVoROr51QAYbh3q/IHTXF3lwUgWF2xhZ66/l6Q244VghoZ5JTWON0/1Lp6Jjg2UXIe14MvAaUk2mW0IiwUtKQ==", - "dependencies": { - "@rdfjs/term-map": "^2.0.0", - "@rdfjs/term-set": "^2.0.1", - "@tpluscode/rdf-ns-builders": ">=3.0.2", - "@tpluscode/rdf-string": "^1.3.1" - }, - "peerDependencies": { - "@types/sparqljs": "^3.1.10", - "clownface": "1 - 2" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4243,11 +4119,11 @@ } }, "node_modules/rdf-vocabulary": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rdf-vocabulary/-/rdf-vocabulary-1.0.0.tgz", - "integrity": "sha512-aDMeOiyr7ZUx9UjjgQogXW5e+KfJSxa+HML5BIXE2lQiXp+qn9FggfnejBkcnqUCoypFtjDvejZ3UN3gGkurTw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rdf-vocabulary/-/rdf-vocabulary-1.0.1.tgz", + "integrity": "sha512-0I7osG03qJ/7O/cEY/1wDLWnx+JBPoxPa9Iz/8856W7Ci7XNLBj8ZSWZCqVCOi/v5P6nenw5V9gkmPkg5TGZkg==", "dependencies": { - "@rdfjs/types": "^1.1.0" + "@rdfjs/types": "*" } }, "node_modules/rdfa-streaming-parser": { diff --git a/package.json b/package.json index 10f7c0c..2adf9e9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "clean": "rm -rf ./dist", "demo:test-engine": "ts-node demo/test-n3-engine.ts", "demo:test-evaluator": "ts-node demo/test-n3-evaluator.ts", + "demo:dynamic-policy": "ts-node demo/dynamic-policy.ts", "prepare": "npm run build", "release": "npm run build && npm publish --access public", "test": "jest" @@ -45,17 +46,15 @@ "typescript": "^5.4.5" }, "dependencies": { - "@rdfjs/dataset": "^2.0.2", "@rdfjs/types": "^1.1.0", "@types/n3": "^1.16.3", - "clownface-shacl-path": "^2.4.0", "eyereasoner": "^16.18.4", "n3": "^1.20.4", "rdf-isomorphic": "^1.3.1", "rdf-lens": "^1.3.5", "rdf-parse": "^3.0.0", "rdf-store-stream": "^2.0.1", - "rdf-vocabulary": "^1.0.0", + "rdf-vocabulary": "^1.0.1", "streamify-string": "^1.0.1", "tmp": "^0.2.3", "uuidv4": "^6.2.13" diff --git a/src/evaluator/DynamicConstraint.ts b/src/evaluator/DynamicConstraint.ts new file mode 100644 index 0000000..7179c08 --- /dev/null +++ b/src/evaluator/DynamicConstraint.ts @@ -0,0 +1,66 @@ +import { Quad, Term } from "@rdfjs/types"; +import { BlankNode, Literal, NamedNode, Store } from 'n3'; +import { BasicLensM, CBDLens, Cont, pred, ShaclPath } from "rdf-lens"; +import { ODRL, ODRLUC, RDF } from "../util/Vocabularies"; + +// use RDF lens instead of clownface for SHACL Property Paths: https://ceur-ws.org/Vol-3759/paper13.pdf +export const pathLens = pred(ODRLUC.terms.path) + .one() + .then(ShaclPath); + +// Utility function to extract the SHACL property path of the Dynamic ODRL Policy using `odrluc:path` +export function getPath(id: Term, quads: Quad[]): BasicLensM { + return pathLens.execute({ id, quads }).mapAll((x) => x.id); +} + +// Utility function to extract the value from an external resource using a SHACL Property path +export function usePath(id: Term, quads: Quad[], lens: BasicLensM): Term[] { + return lens.execute({ id, quads }) +} + +// implements dynamic policy constraint algorithm +// algorithm to fetch value using SHACL path from sotw in policy +// will only materialize when RightOperandReference class is present +export async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]): Promise { + const odrlPolicyStore = new Store(dynamicPolicy) + const odrlDynamicPolicyStore = new Store(dynamicPolicy) + + // get all constraints containing references + const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects(RDF.terms.type, ODRLUC.terms.OperandReference, null); + + for (const operandReferenceNode of operandReferenceNodes) { + // ODRL constraint that has that given rightOperandReference + const constraintNodes = odrlDynamicPolicyStore.getSubjects(ODRL.terms.rightOperandReference, operandReferenceNode, null) + + // external resource + const externalResource = odrlDynamicPolicyStore.getObjects(operandReferenceNode, ODRLUC.terms.reference, null)[0] + if (!externalResource) { + throw Error("There is no source present in the Dynamic Policy Constraint.") + } + + // SHACL Property path + const lens = getPath(operandReferenceNode, dynamicPolicy); + + // extract SHACL Property Path using clownface + const getValue = usePath(externalResource, stateOfTheWorld, lens); + const instantiatedValue = getValue[0]; // according to our algorithm (see paper), there should be only one term + + if (!(instantiatedValue instanceof NamedNode || instantiatedValue instanceof Literal || instantiatedValue instanceof BlankNode)) { + // Now term is a valid Quad_Object + throw Error("Instantiated value is not a proper value."); + } + for (const constraintNode of constraintNodes) { + // remove rightOperandRefrence triple from constraint + odrlPolicyStore.removeQuad(constraintNode, ODRL.terms.rightOperandReference, operandReferenceNode) + // add materialized rightOperand + odrlPolicyStore.addQuad(constraintNode, ODRL.terms.rightOperand, instantiatedValue) + } + + // remove OperandReference triples from Policy store + const operandReferenceQuads = CBDLens.execute({id:operandReferenceNode, quads: dynamicPolicy}) + odrlPolicyStore.removeQuads(operandReferenceQuads) + } + return odrlPolicyStore.getQuads(null, null, null, null) + } + + \ No newline at end of file diff --git a/src/index.browser.ts b/src/index.browser.ts index e4cdd8f..0901d8c 100644 --- a/src/index.browser.ts +++ b/src/index.browser.ts @@ -1,11 +1 @@ -export * from './evaluator/Engine' -export * from './evaluator/Evaluate' -export * from './evaluator/Validate' - -export * from './reasoner/EyeJsReasoner' -// export * from './reasoner/EyeReasoner' -export * from './reasoner/Reasoner' - -export * from './util/Notation3Util' -// export * from './util/RDFUtil' -export * from './rules/Rules' \ No newline at end of file +export * from "./index.core" \ No newline at end of file diff --git a/src/index.core.ts b/src/index.core.ts new file mode 100644 index 0000000..cf8c0f5 --- /dev/null +++ b/src/index.core.ts @@ -0,0 +1,12 @@ +export * from './evaluator/Engine' +export * from './evaluator/Evaluate' +export * from './evaluator/Validate' +export * from './evaluator/DynamicConstraint' + +export * from './reasoner/EyeJsReasoner' +export * from './reasoner/Reasoner' + +export * from './util/Notation3Util' +export * from './util/Vocabularies' + +export * from './rules/Rules' \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 06a7431..78a8326 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,4 @@ -export * from './evaluator/Engine' -export * from './evaluator/Evaluate' -export * from './evaluator/Validate' - -export * from './reasoner/EyeJsReasoner' +export * from "./index.core" export * from './reasoner/EyeReasoner' -export * from './reasoner/Reasoner' -export * from './util/Notation3Util' export * from './util/RDFUtil' \ No newline at end of file diff --git a/src/reasoner/EyeJsReasoner.ts b/src/reasoner/EyeJsReasoner.ts index 5b5694b..cdfe28e 100644 --- a/src/reasoner/EyeJsReasoner.ts +++ b/src/reasoner/EyeJsReasoner.ts @@ -1,4 +1,4 @@ -import { n3reasoner } from "eyereasoner/dist"; +import { n3reasoner } from "eyereasoner"; import { Reasoner } from "./Reasoner"; export class EyeJsReasoner extends Reasoner { diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts new file mode 100644 index 0000000..9b7cce8 --- /dev/null +++ b/src/util/Vocabularies.ts @@ -0,0 +1,42 @@ +import { createVocabulary } from 'rdf-vocabulary'; + +export const RDF = createVocabulary( + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'type', +); + +export const ODRL = createVocabulary( + 'http://www.w3.org/ns/odrl/2/', + 'Agreement', + 'Offer', + 'Permission', + 'Policy', + 'Request', + 'Set', + 'action', + 'target', + 'assignee', + 'assigner', + 'constraint', + 'operator', + 'permission', + 'dateTime', + 'purpose', + 'uid', + 'leftOperand', + 'rightOperand', + 'rightOperandReference', + 'gt', + 'gteq', + 'lt', + 'lteq', + 'eq', + 'neq', + 'read' +); + +export const ODRLUC = createVocabulary("https://w3id.org/force/odrlproposed#", + "OperandReference", + "reference", + "path" +) \ No newline at end of file diff --git a/test-attempt2.ts b/test-attempt2.ts deleted file mode 100644 index 9be365e..0000000 --- a/test-attempt2.ts +++ /dev/null @@ -1,160 +0,0 @@ -// use RDF lens instead of clownface:: https://ceur-ws.org/Vol-3759/paper13.pdf -import { Quad, Term } from "@rdfjs/types"; -import { BasicLensM, Cont, pred, ShaclPath, CBDLens } from "rdf-lens"; -import { NamedNode, Parser, Store, Writer, DataFactory, Literal, BlankNode} from 'n3'; -import { ODRLEngineMultipleSteps, ODRLEvaluator } from "./dist/index"; - -const { namedNode } = DataFactory - -// TODO: change baseIRI for odrluc to https://w3id.org/force/odrlproposed -// TODO: create mini spec at https://w3id.org/force/odrlproposed and create a vocabulary/ontology there -// Next steps: -// - put proposals Beatriz and I from brainstorm in odrlproposed -// - put discussion Pieter and I yesterday regarding ODRL and RDF Context ASsociation in odrlproposed - -// odlurc path as defined in "Interoperable and Continuous Usage Control Enforcement in Dataspaces" -export const pathLens = pred(namedNode("http://example.org/odrluc#path")) - .one() - .then(ShaclPath); - -// Utility function to extract the SHACL property path of the Dynamic ODRL Policy using `odrluc:path` -export function getPath(id: Term, quads: Quad[]): BasicLensM { - return pathLens.execute({ id, quads }).mapAll((x) => x.id); -} - -// Utility function to extract the value from an external resource using a SHACL Property path -export function usePath(id: Term, quads: Quad[], lens: BasicLensM): Term[] { - return lens.execute({ id, quads }) -} - - -const policy = ` -@prefix odrl: . -@prefix ex: . -@prefix dct: . -@prefix odrluc: . - - a odrl:Set ; - odrl:uid ; - dct:description "ALICE may READ resource X when it is before 'ex:updateValue' (see sotw)." ; - dct:source ; - odrl:permission . - - a odrl:Permission ; - odrl:assignee ex:alice ; - odrl:action odrl:read ; - odrl:target ex:x ; - odrl:constraint . - - odrl:leftOperand odrl:dateTime ; - odrl:operator odrl:lt ; - odrl:rightOperandReference ex:operandReference1 . - -ex:operandReference1 a odrluc:OperandReference ; - odrluc:reference ex:externalSource ; - odrluc:path ex:updatedValue . -` - - -const sotw = ` -@prefix ex: . -@prefix temp: . -@prefix dct: . - - a ex:Sotw ; - ex:includes temp:currentTime . - -temp:currentTime dct:issued "2017-02-12T11:20:10.999Z"^^ . - -# external value that will be materialized in the policy -ex:externalSource ex:updatedValue "2018-02-12T11:20:10.999Z"^^ . -` - -const request = ` -@prefix odrl: . -@prefix ex: . -@prefix dct: . - - a odrl:Request ; - odrl:uid ; - dct:description "Requesting Party ALICE requests to READ resource X." ; - odrl:permission . - - a odrl:Permission ; - odrl:assignee ex:alice ; - odrl:action odrl:read ; - odrl:target ex:x . -` - -async function main() { - const parser = new Parser() - const writer = new Writer() - - const odrlDynamicPolicyQuads = parser.parse(policy) - - const odrlPolicyQuads: Quad[] = [] - const odrlRequestQuads = parser.parse(request) - const stateOfTheWorldQuads = parser.parse(sotw) - - const instantiatedPolicyQuads = await materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) - - odrlPolicyQuads.push(...instantiatedPolicyQuads) - - console.log(writer.quadsToString(odrlPolicyQuads)); - - // reasoning over new policy - const evaluator = new ODRLEvaluator(new ODRLEngineMultipleSteps()); - const reasoningResult = await evaluator.evaluate( - odrlPolicyQuads, - odrlRequestQuads, - stateOfTheWorldQuads) - - const output = writer.quadsToString(reasoningResult); - console.log(output); - -} -main() - -// algorithm to fetch value using SHACL path from sotw in policy -export async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]): Promise { - const odrlPolicyStore = new Store(dynamicPolicy) - const odrlDynamicPolicyStore = new Store(dynamicPolicy) - - // get all constraints containing references - const operandReferenceNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/1999/02/22-rdf-syntax-ns#type", 'http://example.org/odrluc#OperandReference', null); - - for (const operandReferenceNode of operandReferenceNodes) { - // ODRL constraint that has that given rightOperandReference - const constraintNodes = odrlDynamicPolicyStore.getSubjects("http://www.w3.org/ns/odrl/2/rightOperandReference", operandReferenceNode, null) - - // external resource - const externalResource = odrlDynamicPolicyStore.getObjects(operandReferenceNode, namedNode("http://example.org/odrluc#reference"), null)[0] - if (!externalResource) { - throw Error("There is no source present in the Dynamic Policy Constraint.") - } - - // SHACL Property path - const lens = getPath(operandReferenceNode, dynamicPolicy); - - // extract SHACL Property Path using clownface - const getValue = usePath(externalResource, stateOfTheWorld, lens); - const instantiatedValue = getValue[0]; // according to our algorithm (see paper), there should be only one term - - if (!(instantiatedValue instanceof NamedNode || instantiatedValue instanceof Literal || instantiatedValue instanceof BlankNode)) { - // Now term is a valid Quad_Object - throw Error("Instantiated value is not a proper value."); - } - for (const constraintNode of constraintNodes) { - // remove rightOperandRefrence triple from constraint - odrlPolicyStore.removeQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperandReference"), operandReferenceNode) - // add materialized rightOperand - odrlPolicyStore.addQuad(constraintNode, namedNode("http://www.w3.org/ns/odrl/2/rightOperand"), instantiatedValue) - } - - // remove OperandReference triples from Policy store - const operandReferenceQuads = CBDLens.execute({id:operandReferenceNode, quads: dynamicPolicy}) - odrlPolicyStore.removeQuads(operandReferenceQuads) - } - return odrlPolicyStore.getQuads(null, null, null, null) -} - From 9b80b04277e49795143598aaa7619f3c99ab7b91 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 9 Apr 2025 14:20:33 +0200 Subject: [PATCH 6/9] fix: make materializepolicy syncrhonous --- demo/dynamic-policy.ts | 2 +- src/evaluator/DynamicConstraint.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/dynamic-policy.ts b/demo/dynamic-policy.ts index aff8892..b0ce100 100644 --- a/demo/dynamic-policy.ts +++ b/demo/dynamic-policy.ts @@ -73,7 +73,7 @@ async function main() { const odrlRequestQuads = parser.parse(request) const stateOfTheWorldQuads = parser.parse(sotw) - const instantiatedPolicyQuads = await materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) + const instantiatedPolicyQuads = materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) odrlPolicyQuads.push(...instantiatedPolicyQuads) diff --git a/src/evaluator/DynamicConstraint.ts b/src/evaluator/DynamicConstraint.ts index 7179c08..6d75d2c 100644 --- a/src/evaluator/DynamicConstraint.ts +++ b/src/evaluator/DynamicConstraint.ts @@ -21,7 +21,7 @@ export function usePath(id: Term, quads: Quad[], lens: BasicLensM): // implements dynamic policy constraint algorithm // algorithm to fetch value using SHACL path from sotw in policy // will only materialize when RightOperandReference class is present -export async function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]): Promise { +export function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[]): Quad[] { const odrlPolicyStore = new Store(dynamicPolicy) const odrlDynamicPolicyStore = new Store(dynamicPolicy) From 2b96090cba1345e99cd817a8a0c89e39e6934ee7 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 9 Apr 2025 16:31:45 +0200 Subject: [PATCH 7/9] feat: integrate materialize policies within evaluator and add tests --- demo/dynamic-policy.ts | 26 +++- src/evaluator/DynamicConstraint.ts | 4 +- src/evaluator/Evaluate.ts | 5 +- test/integration/ODRL-Evaluator.test.ts | 135 ++++++++++++++++-- test/unit/evaluator/DynamicConstraint.test.ts | 91 ++++++++++++ 5 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 test/unit/evaluator/DynamicConstraint.test.ts diff --git a/demo/dynamic-policy.ts b/demo/dynamic-policy.ts index b0ce100..5e49dee 100644 --- a/demo/dynamic-policy.ts +++ b/demo/dynamic-policy.ts @@ -1,6 +1,6 @@ import { Quad } from "@rdfjs/types"; import { Parser, Writer } from 'n3'; -import { materializePolicy, ODRLEngineMultipleSteps, ODRLEvaluator } from "../dist/index"; +import { materializePolicy, ODRLEngineMultipleSteps, ODRLEvaluator, resourceToOptimisedTurtle } from "../dist/index"; // Variant on test case 036: Read request from Alice to resource X returns into yes (temporal lt) (Alice Request Read X). @@ -69,26 +69,40 @@ async function main() { const odrlDynamicPolicyQuads = parser.parse(policy) - const odrlPolicyQuads: Quad[] = [] const odrlRequestQuads = parser.parse(request) const stateOfTheWorldQuads = parser.parse(sotw) const instantiatedPolicyQuads = materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) - odrlPolicyQuads.push(...instantiatedPolicyQuads) + console.log("Instantiated Policy:") + console.log(writer.quadsToString(instantiatedPolicyQuads)); - console.log(writer.quadsToString(odrlPolicyQuads)); - // reasoning over new policy + // reasoning over dynamic policy const evaluator = new ODRLEvaluator(new ODRLEngineMultipleSteps()); const reasoningResult = await evaluator.evaluate( - odrlPolicyQuads, + odrlDynamicPolicyQuads, odrlRequestQuads, stateOfTheWorldQuads) const output = writer.quadsToString(reasoningResult); + console.log("Compliance Report") console.log(output); + const prefixes = { + 'odrl': 'http://www.w3.org/ns/odrl/2/', + 'ex': 'http://example.org/', + 'temp': 'http://example.com/request/', + 'dct': 'http://purl.org/dc/terms/', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + 'foaf': 'http://xmlns.com/foaf/0.1/', + 'report': 'https://w3id.org/force/compliance-report#' + } + + // created report with N3 + // @ts-ignore + console.log(resourceToOptimisedTurtle(reasoningResult, prefixes)); + } main() diff --git a/src/evaluator/DynamicConstraint.ts b/src/evaluator/DynamicConstraint.ts index 6d75d2c..6009c2a 100644 --- a/src/evaluator/DynamicConstraint.ts +++ b/src/evaluator/DynamicConstraint.ts @@ -43,9 +43,9 @@ export function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[] // extract SHACL Property Path using clownface const getValue = usePath(externalResource, stateOfTheWorld, lens); - const instantiatedValue = getValue[0]; // according to our algorithm (see paper), there should be only one term + const instantiatedValue = getValue[0]; // according to the algorithm (see paper), there should be only one term - if (!(instantiatedValue instanceof NamedNode || instantiatedValue instanceof Literal || instantiatedValue instanceof BlankNode)) { + if (!(instantiatedValue instanceof NamedNode || instantiatedValue instanceof Literal)) { // Now term is a valid Quad_Object throw Error("Instantiated value is not a proper value."); } diff --git a/src/evaluator/Evaluate.ts b/src/evaluator/Evaluate.ts index 8ebf411..78a5646 100644 --- a/src/evaluator/Evaluate.ts +++ b/src/evaluator/Evaluate.ts @@ -1,6 +1,7 @@ import type { Quad } from '@rdfjs/types'; import { ODRLEngine, Engine} from './Engine'; import { RDFValidator, TripleTermValidator, SHACLValidator } from './Validate'; +import { materializePolicy } from './DynamicConstraint'; export interface Evaluator { /** @@ -64,9 +65,11 @@ export class ODRLEvaluator implements Evaluator { // if there are compact policies -> they must be expanded (also reocmmended by ODRL ยง 2.7.1 Compact Policy) + // handle dynamic policies + const instantiatedPolicies = materializePolicy(policy, state); // evaluate // the evaluation will result into a conformance report - const evaluation = await this.engine.evaluate([...policy, ...request, ...state]) + const evaluation = await this.engine.evaluate([...instantiatedPolicies, ...request, ...state]) // TODO: think about when the report can be empty // does it always mean there is not enough information? diff --git a/test/integration/ODRL-Evaluator.test.ts b/test/integration/ODRL-Evaluator.test.ts index 6d52aa5..f637dea 100644 --- a/test/integration/ODRL-Evaluator.test.ts +++ b/test/integration/ODRL-Evaluator.test.ts @@ -1,10 +1,12 @@ import { ODRLEngineMultipleSteps, ODRLEvaluator, turtleStringToStore, blanknodeify } from "../../src"; -import { Quad } from "n3"; +import { Quad, Parser } from "n3"; import "jest-rdf"; describe('The default ODRL evaluator', () => { const odrlEvaluator = new ODRLEvaluator(new ODRLEngineMultipleSteps()); - it ('Handle skos exact match', async () => { + const parser = new Parser() + + it('handles skos exact match.', async () => { const odrlPolicyText = ` . . @@ -14,7 +16,7 @@ describe('The default ODRL evaluator', () => { . . `; -const odrlRequestText = ` + const odrlRequestText = ` . . . @@ -22,10 +24,10 @@ const odrlRequestText = ` . . `; -const stateOfTheWorldText = ` + const stateOfTheWorldText = ` "2025-01-14T10:09:08.370Z"^^ . `; - const expectedReport =` + const expectedReport = ` @prefix odrl: . @prefix ex: . @prefix temp: . @@ -52,15 +54,118 @@ const stateOfTheWorldText = ` a report:ActionReport; report:satisfactionState report:Satisfied.` - const odrlPolicyStore = await turtleStringToStore(odrlPolicyText); - const odrlRequestStore = await turtleStringToStore(odrlRequestText); - const stateOfTheWorldStore = await turtleStringToStore(stateOfTheWorldText); - const expectedReportStore = await turtleStringToStore(expectedReport); - const report = await odrlEvaluator.evaluate( - odrlPolicyStore.getQuads(null, null, null, null), - odrlRequestStore.getQuads(null, null, null, null), - stateOfTheWorldStore.getQuads(null, null, null, null)); - - expect(blanknodeify(report as any as Quad[])).toBeRdfIsomorphic(blanknodeify(expectedReportStore.getQuads(null, null, null, null))) + const odrlPolicyQuads = parser.parse(odrlPolicyText); + const odrlRequestQuads = parser.parse(odrlRequestText); + const stateOfTheWorldQuads = parser.parse(stateOfTheWorldText); + + const expectedReportQuads = parser.parse(expectedReport); + const report = await odrlEvaluator.evaluate( + odrlPolicyQuads, + odrlRequestQuads, + stateOfTheWorldQuads); + + expect(blanknodeify(report as any as Quad[])).toBeRdfIsomorphic(blanknodeify(expectedReportQuads)) }); + + it('handles dynamic constraint policies.', async () => { + const policy = ` + @prefix odrl: . + @prefix ex: . + @prefix dct: . + @prefix odrluc: . + + a odrl:Set ; + odrl:uid ; + dct:description "ALICE may READ resource X when it is before 'ex:updateValue' (see sotw)." ; + dct:source ; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:action odrl:read ; + odrl:target ex:x ; + odrl:constraint . + + odrl:leftOperand odrl:dateTime ; + odrl:operator odrl:lt ; + odrl:rightOperandReference ex:operandReference1 . + + ex:operandReference1 a odrluc:OperandReference ; + odrluc:reference ex:externalSource ; + odrluc:path ex:updatedValue . + ` + + const sotw = ` + @prefix ex: . + @prefix temp: . + @prefix dct: . + + a ex:Sotw ; + ex:includes temp:currentTime . + + temp:currentTime dct:issued "2017-02-12T11:20:10.999Z"^^ . + + # external value that will be materialized in the policy + ex:externalSource ex:updatedValue "2018-02-12T11:20:10.999Z"^^ . + ` + + const request = ` + @prefix odrl: . + @prefix ex: . + @prefix dct: . + + a odrl:Request ; + odrl:uid ; + dct:description "Requesting Party ALICE requests to READ resource X." ; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:action odrl:read ; + odrl:target ex:x . + ` + + const expectedReport = ` +@prefix odrl: . +@prefix dct: . +@prefix xsd: . +@prefix report: . + + + a report:PolicyReport; + dct:created "2017-02-12T11:20:10.999Z"^^xsd:dateTime; + report:policy ; + report:policyRequest ; + report:ruleReport . + a report:PermissionReport; + report:attemptState report:Attempted; + report:rule ; + report:ruleRequest ; + report:premiseReport , , , ; + report:activationState report:Active. + a report:TargetReport; + report:satisfactionState report:Satisfied. + a report:PartyReport; + report:satisfactionState report:Satisfied. + a report:ActionReport; + report:satisfactionState report:Satisfied. + a report:ConstraintReport; + report:constraint ; + report:constraintLeftOperand "2017-02-12T11:20:10.999Z"^^xsd:dateTime; + report:constraintOperator odrl:lt; + report:constraintRightOperand "2018-02-12T11:20:10.999Z"^^xsd:dateTime; + report:satisfactionState report:Satisfied. +` + const odrlPolicyQuads = parser.parse(policy); + const odrlRequestQuads = parser.parse(request); + const stateOfTheWorldQuads = parser.parse(sotw); + + const expectedReportQuads = parser.parse(expectedReport); + const report = await odrlEvaluator.evaluate( + odrlPolicyQuads, + odrlRequestQuads, + stateOfTheWorldQuads); + expect(blanknodeify(report as any as Quad[])).toBeRdfIsomorphic(blanknodeify(expectedReportQuads)) + + }) }) \ No newline at end of file diff --git a/test/unit/evaluator/DynamicConstraint.test.ts b/test/unit/evaluator/DynamicConstraint.test.ts new file mode 100644 index 0000000..6deeb82 --- /dev/null +++ b/test/unit/evaluator/DynamicConstraint.test.ts @@ -0,0 +1,91 @@ +import { Parser } from "n3"; +import "jest-rdf"; +import { materializePolicy } from "../../../src"; + +describe('The Dynamic Constraints materialize function', () => { + const dynamicPolicy = ` +@prefix odrl: . +@prefix ex: . +@prefix dct: . +@prefix odrluc: . + + a odrl:Set ; + odrl:uid ; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:action odrl:read ; + odrl:target ex:x ; + odrl:constraint . + + odrl:leftOperand odrl:dateTime ; + odrl:operator odrl:lt ; + odrl:rightOperandReference ex:operandReference1 . + +ex:operandReference1 a odrluc:OperandReference ; + odrluc:reference ex:externalSource ; + odrluc:path ex:updatedValue . +` + const sotw = ` +@prefix ex: . +@prefix dct: . + +ex:externalSource ex:updatedValue "2018-02-12T11:20:10.999Z"^^ . + ` + const instantiatedPolicy = ` + . + . + . + . + . + . + . + . + . + . + "2018-02-12T11:20:10.999Z"^^ . + ` + const parser = new Parser(); + const odrlDynamicPolicyQuads = parser.parse(dynamicPolicy); + const stateOfTheWorldQuads = parser.parse(sotw); + const instantiatedPolicyQuads = parser.parse(instantiatedPolicy); + + it("materializes when a dynamic policy and relevant sotw is provided.", () => { + const materializedPolicyQuads = materializePolicy(odrlDynamicPolicyQuads, stateOfTheWorldQuads) + + expect(materializedPolicyQuads).toBeRdfIsomorphic(instantiatedPolicyQuads); + }); + + it("does nothing when an instantiated policy is presented.", () => { + const materializedPolicyQuads = materializePolicy(instantiatedPolicyQuads, stateOfTheWorldQuads) + + expect(materializedPolicyQuads).toBeRdfIsomorphic(instantiatedPolicyQuads); + }) + + it("throws an error when no external source is provided in the sotw.", () => { + expect(() => materializePolicy(odrlDynamicPolicyQuads, [])).toThrow(Error) + }) + + it("throws an error when no external source property is provided.", () => { + const operandReference = ` +@prefix ex: . +@prefix odrluc: . +ex:operandReference1 a odrluc:OperandReference ; + odrluc:path ex:updatedValue .` + + const operandReferenceQuads = parser.parse(operandReference) + expect(() => materializePolicy(operandReferenceQuads, [])).toThrow(Error) + }) + + it("throws nope when no path property is provided.", () => { + const operandReference = ` +@prefix ex: . +@prefix odrluc: . +ex:operandReference1 a odrluc:OperandReference ; + odrluc:reference ex:externalSource .` + + const operandReferenceQuads = parser.parse(operandReference) + expect(() => materializePolicy(operandReferenceQuads, [])).toThrow("nope") + }) +}) \ No newline at end of file From 909920ae885a0c5062e6c5026b1c4703a59fe27f Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 9 Apr 2025 17:07:16 +0200 Subject: [PATCH 8/9] fix: optimize imports --- demo/dynamic-policy.ts | 1 - src/evaluator/DynamicConstraint.ts | 2 +- test/integration/ODRL-Evaluator.test.ts | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/demo/dynamic-policy.ts b/demo/dynamic-policy.ts index 5e49dee..6ac6988 100644 --- a/demo/dynamic-policy.ts +++ b/demo/dynamic-policy.ts @@ -1,4 +1,3 @@ -import { Quad } from "@rdfjs/types"; import { Parser, Writer } from 'n3'; import { materializePolicy, ODRLEngineMultipleSteps, ODRLEvaluator, resourceToOptimisedTurtle } from "../dist/index"; diff --git a/src/evaluator/DynamicConstraint.ts b/src/evaluator/DynamicConstraint.ts index 6009c2a..ca9e17d 100644 --- a/src/evaluator/DynamicConstraint.ts +++ b/src/evaluator/DynamicConstraint.ts @@ -1,5 +1,5 @@ import { Quad, Term } from "@rdfjs/types"; -import { BlankNode, Literal, NamedNode, Store } from 'n3'; +import { Literal, NamedNode, Store } from 'n3'; import { BasicLensM, CBDLens, Cont, pred, ShaclPath } from "rdf-lens"; import { ODRL, ODRLUC, RDF } from "../util/Vocabularies"; diff --git a/test/integration/ODRL-Evaluator.test.ts b/test/integration/ODRL-Evaluator.test.ts index f637dea..2c8f415 100644 --- a/test/integration/ODRL-Evaluator.test.ts +++ b/test/integration/ODRL-Evaluator.test.ts @@ -1,6 +1,6 @@ -import { ODRLEngineMultipleSteps, ODRLEvaluator, turtleStringToStore, blanknodeify } from "../../src"; -import { Quad, Parser } from "n3"; import "jest-rdf"; +import { Parser, Quad } from "n3"; +import { blanknodeify, ODRLEngineMultipleSteps, ODRLEvaluator } from "../../src"; describe('The default ODRL evaluator', () => { const odrlEvaluator = new ODRLEvaluator(new ODRLEngineMultipleSteps()); From f381f41de4108be5d62238c275f1082787bcbc38 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Thu, 10 Apr 2025 14:33:01 +0200 Subject: [PATCH 9/9] fix: typo in comments --- src/evaluator/DynamicConstraint.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/evaluator/DynamicConstraint.ts b/src/evaluator/DynamicConstraint.ts index ca9e17d..b1edd26 100644 --- a/src/evaluator/DynamicConstraint.ts +++ b/src/evaluator/DynamicConstraint.ts @@ -41,12 +41,12 @@ export function materializePolicy(dynamicPolicy: Quad[], stateOfTheWorld: Quad[] // SHACL Property path const lens = getPath(operandReferenceNode, dynamicPolicy); - // extract SHACL Property Path using clownface + // extract SHACL Property Path using RDF-Lens const getValue = usePath(externalResource, stateOfTheWorld, lens); const instantiatedValue = getValue[0]; // according to the algorithm (see paper), there should be only one term if (!(instantiatedValue instanceof NamedNode || instantiatedValue instanceof Literal)) { - // Now term is a valid Quad_Object + // Now instantiatedValue is a valid value throw Error("Instantiated value is not a proper value."); } for (const constraintNode of constraintNodes) {