From 0b0083a2117e6cc8f4b7761b3325932ef033d5e5 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Mon, 13 Jan 2025 18:27:18 +0100 Subject: [PATCH 01/15] WIP: Integrating ODRL Evaluator --- .gitignore | 3 +- packages/ucp/src/util/Vocabularies.ts | 3 +- packages/uma/bin/odrl.ts | 45 ++++++ packages/uma/config/default.json | 2 +- packages/uma/config/odrl.json | 119 ++++++++++++++ .../config/policies/authorizers/default.json | 7 +- .../uma/config/policies/authorizers/odrl.json | 19 +++ packages/uma/config/rules/odrl/policy0.ttl | 26 +++ packages/uma/package.json | 2 + packages/uma/src/index.ts | 1 + .../policies/authorizers/OdrlAuthorizer.ts | 149 ++++++++++++++++++ yarn.lock | 136 +++++++++++++--- 12 files changed, 486 insertions(+), 26 deletions(-) create mode 100644 packages/uma/bin/odrl.ts create mode 100644 packages/uma/config/odrl.json create mode 100644 packages/uma/config/policies/authorizers/odrl.json create mode 100644 packages/uma/config/rules/odrl/policy0.ttl create mode 100644 packages/uma/src/policies/authorizers/OdrlAuthorizer.ts diff --git a/.gitignore b/.gitignore index 257bbd16..0003d53f 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,5 @@ tmp # Misc .DS_Store -.vscode/ \ No newline at end of file +.vscode/ +.idea diff --git a/packages/ucp/src/util/Vocabularies.ts b/packages/ucp/src/util/Vocabularies.ts index cf0d14a6..96479282 100644 --- a/packages/ucp/src/util/Vocabularies.ts +++ b/packages/ucp/src/util/Vocabularies.ts @@ -108,12 +108,13 @@ export const RDF = createVocabulary( 'type', ); - + export const ODRL = createVocabulary( 'http://www.w3.org/ns/odrl/2/', 'Agreement', 'Offer', 'Permission', + 'Request', 'action', 'target', 'assignee', diff --git a/packages/uma/bin/odrl.ts b/packages/uma/bin/odrl.ts new file mode 100644 index 00000000..ee487e30 --- /dev/null +++ b/packages/uma/bin/odrl.ts @@ -0,0 +1,45 @@ +import { ServerInitializer, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +import * as path from 'path'; +import { ComponentsManager } from 'componentsjs'; + +const protocol = 'http'; +const host = 'localhost'; +const port = 4000; + +const baseUrl = `${protocol}://${host}:${port}/uma`; +const rootDir = path.join(__dirname, '../'); + +export const launch: () => Promise = async () => { + + const variables: Record = {}; + + variables['urn:uma:variables:port'] = port; + variables['urn:uma:variables:host'] = host; + variables['urn:uma:variables:protocol'] = protocol; + variables['urn:uma:variables:baseUrl'] = baseUrl; + + variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/odrl'); + // variables['urn:uma:variables:rulesDir'] = path.join(rootDir, './config/rules/n3'); + + variables['urn:uma:variables:mainModulePath'] = rootDir; + variables['urn:uma:variables:customConfigPath'] = path.join(rootDir, './config/odrl.json'); + + const mainModulePath = variables['urn:uma:variables:mainModulePath']; + const configPath = variables['urn:uma:variables:customConfigPath']; + + setGlobalLoggerFactory(new WinstonLoggerFactory('info')); + + const manager = await ComponentsManager.build({ + mainModulePath, + logLevel: 'silly', + typeChecking: false, + }); + + await manager.configRegistry.register(configPath); + + const umaServer: ServerInitializer = await manager.instantiate('urn:uma:default:NodeHttpServer',{variables}); + await umaServer.handleSafe(); + +}; + +launch(); diff --git a/packages/uma/config/default.json b/packages/uma/config/default.json index a0ed3888..6ecaa44a 100644 --- a/packages/uma/config/default.json +++ b/packages/uma/config/default.json @@ -18,7 +18,7 @@ "sai-uma:config/routes/vc.json", "sai-uma:config/routes/contract.json", "sai-uma:config/tickets/storage/default.json", - "sai-uma:config/tickets/strategy/claim-elimination.json", + "sai-uma:config/tickets/strategy/immediate-authorizer.json", "sai-uma:config/tokens/factory/default.json", "sai-uma:config/tokens/storage/default.json", "sai-uma:config/variables/default.json" diff --git a/packages/uma/config/odrl.json b/packages/uma/config/odrl.json new file mode 100644 index 00000000..ae2ffbcb --- /dev/null +++ b/packages/uma/config/odrl.json @@ -0,0 +1,119 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma/^0.0.0/components/context.jsonld", + "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld" + ], + "import": [ + "sai-uma:config/credentials/verifiers/default.json", + "sai-uma:config/dialog/negotiators/contract.json", + "sai-uma:config/policies/authorizers/odrl.json", + "sai-uma:config/resources/storage/default.json", + "sai-uma:config/routes/discovery.json", + "sai-uma:config/routes/introspection.json", + "sai-uma:config/routes/keys.json", + "sai-uma:config/routes/resources.json", + "sai-uma:config/routes/tickets.json", + "sai-uma:config/routes/tokens_contract.json", + "sai-uma:config/routes/log.json", + "sai-uma:config/routes/vc.json", + "sai-uma:config/routes/contract.json", + "sai-uma:config/tickets/storage/default.json", + "sai-uma:config/tickets/strategy/immediate-authorizer.json", + "sai-uma:config/tokens/factory/default.json", + "sai-uma:config/tokens/storage/default.json", + "sai-uma:config/variables/default.json" + ], + "@graph": [ + { + "@id": "urn:uma:default:NodeHttpServer", + "@type": "ServerInitializer", + "port": { + "@id": "urn:uma:variables:port" + }, + "serverFactory": { + "@id": "urn:uma:default:ServerFactory", + "@type": "BaseServerFactory", + "configurator": { + "comment": "Handles all request events from the server.", + "@id": "urn:uma:default:HandlerServerConfigurator", + "@type": "HandlerServerConfigurator", + "handler": { + "@id": "urn:uma:default:NodeHttpRequestResponseHandler" + } + } + } + }, + { + "@id": "urn:uma:default:HttpHandler", + "@type": "SequenceHandler", + "handlers": [ + { + "comment": "Adds all the necessary CORS headers.", + "@id": "urn:uma:default:CorsHandler", + "@type": "CorsHandler", + "options_methods": [ + "GET", + "HEAD", + "OPTIONS", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "options_credentials": true, + "options_preflightContinue": false, + "options_exposedHeaders": [ + "Allow", + "ETag", + "Last-Modified", + "Link", + "Location", + "Updates-Via", + "Www-Authenticate" + ] + }, + { + "@id": "urn:uma:default:NodeHttpRequestResponseHandler" + } + ] + }, + { + "@id": "urn:uma:default:NodeHttpRequestResponseHandler", + "@type": "NodeHttpRequestResponseHandler", + "targetExtractor": { + "@type": "BaseTargetExtractor", + "includeQueryString": true + }, + "httpHandler": { + "@id": "urm:uma:default:JsonHttpErrorHandler", + "@type": "JsonHttpErrorHandler", + "handler": { + "@id": "urm:uma:default:JsonFormHttpHandler", + "@type": "JsonFormHttpHandler", + "handler": { + "@id": "urn:uma:default:RoutedHttpRequestHandler", + "@type": "RoutedHttpRequestHandler", + "routes": [ + { "@id": "urn:uma:default:UmaConfigRoute" }, + { "@id": "urn:uma:default:JwksRoute" }, + { "@id": "urn:uma:default:DemoTokenRoute" }, + { "@id": "urn:uma:default:PermissionRegistrationRoute" }, + { "@id": "urn:uma:default:ResourceRegistrationRoute" }, + { "@id": "urn:uma:default:ResourceRegistrationOpsRoute" }, + { "@id": "urn:uma:default:IntrospectionRoute" }, + { "@id": "urn:uma:default:LogRoute" }, + { "@id": "urn:uma:default:VCRoute" }, + { "@id": "urn:uma:default:ContractRoute" } + ], + "defaultHandler": { + "@type": "DefaultRequestHandler" + } + } + } + } + }, + { + "comment": "Configuration for the UMA AS." + } + ] +} diff --git a/packages/uma/config/policies/authorizers/default.json b/packages/uma/config/policies/authorizers/default.json index 83c99660..0a42821f 100644 --- a/packages/uma/config/policies/authorizers/default.json +++ b/packages/uma/config/policies/authorizers/default.json @@ -49,11 +49,8 @@ } ], "fallback": { - "@id": "urn:uma:default:PolicyBasedAuthorizer", - "@type": "PolicyBasedAuthorizer", - "rulesDir": { - "@id": "urn:uma:variables:rulesDir" - }, + "@id": "urn:uma:default:OdrlAuthorizer", + "@type": "OdrlAuthorizer", "policies": { "@id": "urn:uma:default:RulesStorage", "@type": "DirectoryUCRulesStorage", diff --git a/packages/uma/config/policies/authorizers/odrl.json b/packages/uma/config/policies/authorizers/odrl.json new file mode 100644 index 00000000..c3df189d --- /dev/null +++ b/packages/uma/config/policies/authorizers/odrl.json @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma/^0.0.0/components/context.jsonld", + "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/ucp/^0.0.0/components/context.jsonld" + ], + "@graph": [ + { + "@id": "urn:uma:default:Authorizer", + "@type": "OdrlAuthorizer", + "policies": { + "@id": "urn:uma:default:RulesStorage", + "@type": "DirectoryUCRulesStorage", + "directoryPath": { + "@id": "urn:uma:variables:policyDir" + } + } + } + ] +} diff --git a/packages/uma/config/rules/odrl/policy0.ttl b/packages/uma/config/rules/odrl/policy0.ttl new file mode 100644 index 00000000..cac75c6a --- /dev/null +++ b/packages/uma/config/rules/odrl/policy0.ttl @@ -0,0 +1,26 @@ +@prefix ex: . +@prefix odrl: . + +ex:usagePolicy1 a odrl:Agreement . +ex:usagePolicy1 odrl:permission ex:permission1 . +ex:permission1 a odrl:Permission . +ex:permission1 odrl:action odrl:modify . +ex:permission1 odrl:target . +ex:permission1 odrl:assignee . +ex:permission1 odrl:assigner . + +ex:usagePolicy2 a odrl:Agreement . +ex:usagePolicy2 odrl:permission ex:permission2 . +ex:permission2 a odrl:Permission . +ex:permission2 odrl:action odrl:modify . +ex:permission2 odrl:target . +ex:permission2 odrl:assignee . +ex:permission2 odrl:assigner . + +ex:usagePolicy3 a odrl:Agreement . +ex:usagePolicy3 odrl:permission ex:permission3 . +ex:permission3 a odrl:Permission . +ex:permission3 odrl:action odrl:read . +ex:permission3 odrl:target . +ex:permission3 odrl:assignee . +ex:permission3 odrl:assigner . diff --git a/packages/uma/package.json b/packages/uma/package.json index 84d3c918..770b4c52 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -57,6 +57,7 @@ "build:components": "yarn run -T componentsjs-generator -r sai-uma -s src -c dist/components -i .componentsignore --lenient", "test": "yarn run -T jest --coverage", "start": "yarn run -T ts-node bin/main.ts", + "start:odrl": "yarn run -T ts-node bin/odrl.ts", "demo": "yarn run -T ts-node bin/demo.ts" }, "dependencies": { @@ -72,6 +73,7 @@ "koreografeye": "^0.4.8", "logform": "^2.6.0", "n3": "^1.17.2", + "odrl-evaluator": "^0.1.0", "ts-node": "^10.9.2", "uri-template-lite": "^23.4.0", "winston": "^3.11.0" diff --git a/packages/uma/src/index.ts b/packages/uma/src/index.ts index 3d4dc657..e6e1d480 100644 --- a/packages/uma/src/index.ts +++ b/packages/uma/src/index.ts @@ -23,6 +23,7 @@ export * from './policies/authorizers/Authorizer'; export * from './policies/authorizers/AllAuthorizer'; export * from './policies/authorizers/NamespacedAuthorizer'; export * from './policies/authorizers/NoneAuthorizer'; +export * from './policies/authorizers/OdrlAuthorizer'; export * from './policies/authorizers/PolicyBasedAuthorizer'; export * from './policies/authorizers/WebIdAuthorizer'; diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts new file mode 100644 index 00000000..cb299e33 --- /dev/null +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -0,0 +1,149 @@ +import {Authorizer} from "./Authorizer"; +import {ODRL, UconRequest, UCRulesStorage} from "@solidlab/ucp"; +import {WEBID} from "../../credentials/Claims"; +import {ODRLEvaluator, ODRLEngineMultipleSteps} from 'odrl-evaluator' +import {DataFactory, Store, Writer} from "n3"; +import quad = DataFactory.quad; +import namedNode = DataFactory.namedNode; +import literal = DataFactory.literal; +import {generate_uuid} from "koreografeye"; +import { getLoggerFor, RDF } from '@solid/community-server'; +import {ClaimSet} from "../../credentials/ClaimSet"; +import {Permission} from "../../views/Permission"; +import {Requirements} from "../../credentials/Requirements"; + +export class OdrlAuthorizer implements Authorizer { + protected readonly logger = getLoggerFor(this); + private readonly odrlEvaluator: ODRLEvaluator; + + /** + * Creates a OdrlAuthorizer enforcing policies using ODRL with the ODRL Evaluator. + * + * @param policies - A store containing the ODRL policy rules. + */ + constructor( + private readonly policies: UCRulesStorage, + ) { + const engine = new ODRLEngineMultipleSteps(); + this.odrlEvaluator = new ODRLEvaluator(engine); + } + + public async permissions(claims: ClaimSet, query?: Permission[]): Promise { + this.logger.info(`Calculating permissions. ${JSON.stringify({claims, query})}`); + console.log(claims) + + if (!query) { + this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit queries.') + return []; + } + + this.logger.info(Object.keys(claims).length + " claim received") + const requests: UconRequest[] = []; + for (const {resource_id, resource_scopes} of query) { + + if (!resource_id) { + this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit resources.'); + continue; + } + + // ODRL can only handle odrl actions + requests.push({ + subject: typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous', + resource: resource_id, + action: resource_scopes ? transformActionsCssToOdrl(resource_scopes) : ["http://www.w3.org/ns/odrl/2/use"], + claims + }); + } + const permissions: Permission[] = await Promise.all(requests.map( + async (request) => { + const scopes_permitted = []; + + // 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")))); + + // prepare policy + const policyStore = (await this.policies.getStore()) + + + for (const action of request.action) { + // prepare request + const req = new Store(); + const requestNode = generate_uuid(); + const permissionNode = generate_uuid(); + req.add(quad(requestNode, RDF.terms.type, ODRL.terms.Request)); + req.add(quad(requestNode, ODRL.terms.permission, permissionNode)); + + req.add(quad(permissionNode, RDF.terms.type, ODRL.terms.Permission)); + req.add(quad(permissionNode, ODRL.terms.assignee, namedNode(request.subject))); + req.add(quad(permissionNode, ODRL.terms.target, namedNode(request.resource))); + req.add(quad(permissionNode, ODRL.terms.action, namedNode(action))); + + // evaluate policies + const reports = await this.odrlEvaluator.evaluate([...policyStore], [...req], [...sotw]); + const reportStore = new Store(reports) + + // TODO: handle multiple reports -> possible to be generated + // fetch active and attempted + const permissionReportNode = reportStore.getQuads(null, "http://example.com/report/temp/rule", permissionNode, null); + if (permissionReportNode.length !== 1) { + this.logger.warn("Expected only one Permission Report. No permissions granted."); + + console.log(new Writer().quadsToString([...policyStore])) + console.log(new Writer().quadsToString([...req])) + console.log(new Writer().quadsToString([...sotw])) + console.log(new Writer().quadsToString(reports)) + break; + } + const activationState = reportStore.getObjects(permissionReportNode[0].subject, "http://example.com/report/temp/activationState", "http://example.com/report/temp/Active"); + if (activationState.length === 1) { + scopes_permitted.push(action) + } + } + + + + // extract allowed scopes + return { + resource_id: request.resource, + resource_scopes: transformActionsOdrlToCss(scopes_permitted) + } + } + )); + // console.log(permissions) + return permissions; + } + + public async credentials(permissions: Permission[], query?: Requirements | undefined): Promise { + throw new Error("Method not implemented."); + } + +} +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]])); + +function transformActionsCssToOdrl(actions: string[]): string[] { + // scopes come from UmaClient.ts -> see CSS package + + // in UMAPermissionReader, only the last part of the URN will be used, divided by a colon + // again, see CSS package + return actions.map(action => scopeCssToOdrl.get(action)!); +} + +function transformActionsOdrlToCss(actions: string[]): string[] { + const cssActions = [] + for (const action of actions) { + if (action === 'http://www.w3.org/ns/odrl/2/use'){ + return Array.from(scopeCssToOdrl.keys()); + } + cssActions.push(scopeOdrlToCss.get(action)!); + } + return cssActions; +} diff --git a/yarn.lock b/yarn.lock index a7b2adf1..f7d53a0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4325,6 +4325,7 @@ __metadata: koreografeye: "npm:^0.4.8" logform: "npm:^2.6.0" n3: "npm:^1.17.2" + odrl-evaluator: "npm:^0.1.0" ts-node: "npm:^10.9.2" uri-template-lite: "npm:^23.4.0" winston: "npm:^3.11.0" @@ -4601,10 +4602,10 @@ __metadata: languageName: node linkType: hard -"@types/emscripten@npm:^1.39.10": - version: 1.39.10 - resolution: "@types/emscripten@npm:1.39.10" - checksum: 10c0/c9adde9307d54efb5152931bfe99966fbe12fbd4d07663fb5cdc4cc1bd3a1f030882d50d4a27875b7b2d9713d160609e67b72e92177a021c9f4699ee5ac41035 +"@types/emscripten@npm:^1.39.10, @types/emscripten@npm:^1.39.13": + version: 1.40.1 + resolution: "@types/emscripten@npm:1.40.1" + checksum: 10c0/0d6cd29e551f85ba49a0e7d58de16c857960d40e57553e7cc2860b7d80c4210c992ed292998ec3fd3bdc3b41d96541e91d01a6c232106ac0ad79b4710e87f38d languageName: node linkType: hard @@ -5022,6 +5023,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:8.3.4": + version: 8.3.4 + resolution: "@types/uuid@npm:8.3.4" + checksum: 10c0/b9ac98f82fcf35962317ef7dc44d9ac9e0f6fdb68121d384c88fe12ea318487d5585d3480fa003cf28be86a3bbe213ca688ba786601dce4a97724765eb5b1cf2 + languageName: node + linkType: hard + "@types/uuid@npm:^9.0.0, @types/uuid@npm:^9.0.5": version: 9.0.7 resolution: "@types/uuid@npm:9.0.7" @@ -7024,6 +7032,20 @@ __metadata: languageName: node linkType: hard +"eyereasoner@npm:^16.18.4": + version: 16.34.1 + resolution: "eyereasoner@npm:16.34.1" + dependencies: + n3: "npm:^1.16.3" + swipl-wasm: "npm:4.0.13" + peerDependencies: + "@rdfjs/types": ^1.1.0 + bin: + eyereasoner: dist/bin/index.js + checksum: 10c0/a1c78114edc20d94b7cb052305d97bc16e49ece53b7c221cda4309c6e92deb1ed485ba31fff728e4bd1195ee16421e1cf25f5da43225e52939ddac2bca9678ba + languageName: node + linkType: hard + "fast-check@npm:3.23.2, fast-check@npm:^3.21.0, fast-check@npm:^3.23.1": version: 3.23.2 resolution: "fast-check@npm:3.23.2" @@ -9503,13 +9525,13 @@ __metadata: languageName: node linkType: hard -"n3@npm:^1.16.1, n3@npm:^1.16.3, n3@npm:^1.16.4, n3@npm:^1.17.0, n3@npm:^1.17.1, n3@npm:^1.17.2, n3@npm:^1.6.3": - version: 1.17.2 - resolution: "n3@npm:1.17.2" +"n3@npm:^1.16.1, n3@npm:^1.16.3, n3@npm:^1.16.4, n3@npm:^1.17.0, n3@npm:^1.17.1, n3@npm:^1.17.2, n3@npm:^1.20.4, n3@npm:^1.6.3": + version: 1.25.1 + resolution: "n3@npm:1.25.1" dependencies: - queue-microtask: "npm:^1.1.2" + buffer: "npm:^6.0.3" readable-stream: "npm:^4.0.0" - checksum: 10c0/ab02073aa46b012cbaedc3027bcb081cec0421b19dd96c8c6d1fdb2c2dcf5ef23a3962e6f7638b78c50abf6ed5b9da05d2ef2ef6ef67887ce27ed8c3d1e174a6 + checksum: 10c0/0a4efc3151d79c7200b57f78bb5d4411f6746d7f2d63ccc7fe068fa9a4d20afc03001957d9216ce2da62f2de266452713f9867112e9b3170307eb4ac059d5d1d languageName: node linkType: hard @@ -9721,6 +9743,24 @@ __metadata: languageName: node linkType: hard +"odrl-evaluator@npm:^0.1.0": + version: 0.1.1 + resolution: "odrl-evaluator@npm:0.1.1" + dependencies: + "@rdfjs/types": "npm:^1.1.0" + "@types/n3": "npm:^1.16.3" + eyereasoner: "npm:^16.18.4" + n3: "npm:^1.20.4" + rdf-isomorphic: "npm:^1.3.1" + rdf-parse: "npm:^3.0.0" + rdf-store-stream: "npm:^2.0.1" + streamify-string: "npm:^1.0.1" + tmp: "npm:^0.2.3" + uuidv4: "npm:^6.2.13" + checksum: 10c0/5091b4402b2def1fb6bf8e7749d7f2b89a1817da59abfa6ae9f1a0db2fd3ae50222eebc578fba21f9d0f455cb5b515a067ac13584c43e3ba98f50454ccf30e16 + languageName: node + linkType: hard + "oidc-provider@npm:^8.4.0": version: 8.4.3 resolution: "oidc-provider@npm:8.4.3" @@ -10123,7 +10163,7 @@ __metadata: languageName: node linkType: hard -"queue-microtask@npm:^1.1.2, queue-microtask@npm:^1.2.2": +"queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 @@ -10232,7 +10272,7 @@ __metadata: languageName: node linkType: hard -"rdf-isomorphic@npm:^1.3.0": +"rdf-isomorphic@npm:^1.3.0, rdf-isomorphic@npm:^1.3.1": version: 1.3.1 resolution: "rdf-isomorphic@npm:1.3.1" dependencies: @@ -10308,6 +10348,38 @@ __metadata: languageName: node linkType: hard +"rdf-parse@npm:^3.0.0": + version: 3.0.0 + resolution: "rdf-parse@npm:3.0.0" + dependencies: + "@comunica/actor-http-fetch": "npm:^2.0.1" + "@comunica/actor-http-proxy": "npm:^2.0.1" + "@comunica/actor-rdf-parse-html": "npm:^2.0.1" + "@comunica/actor-rdf-parse-html-microdata": "npm:^2.0.1" + "@comunica/actor-rdf-parse-html-rdfa": "npm:^2.0.1" + "@comunica/actor-rdf-parse-html-script": "npm:^2.0.1" + "@comunica/actor-rdf-parse-jsonld": "npm:^2.0.1" + "@comunica/actor-rdf-parse-n3": "npm:^2.0.1" + "@comunica/actor-rdf-parse-rdfxml": "npm:^2.0.1" + "@comunica/actor-rdf-parse-shaclc": "npm:^2.6.2" + "@comunica/actor-rdf-parse-xml-rdfa": "npm:^2.0.1" + "@comunica/bus-http": "npm:^2.0.1" + "@comunica/bus-init": "npm:^2.0.1" + "@comunica/bus-rdf-parse": "npm:^2.0.1" + "@comunica/bus-rdf-parse-html": "npm:^2.0.1" + "@comunica/config-query-sparql": "npm:^2.0.1" + "@comunica/core": "npm:^2.0.1" + "@comunica/mediator-combine-pipeline": "npm:^2.0.1" + "@comunica/mediator-combine-union": "npm:^2.0.1" + "@comunica/mediator-number": "npm:^2.0.1" + "@comunica/mediator-race": "npm:^2.0.1" + "@rdfjs/types": "npm:*" + readable-stream: "npm:^4.3.0" + stream-to-string: "npm:^1.2.0" + checksum: 10c0/c85c1caed0772e69a3862ca6875c986ad44ab3d46744c6781e46395333f751b4f505ba490a983771d2eeddf791cae5fefb404fd1f3ff414a493962c009016335 + languageName: node + linkType: hard + "rdf-quad@npm:^1.5.0": version: 1.5.0 resolution: "rdf-quad@npm:1.5.0" @@ -10718,7 +10790,7 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": +"rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" dependencies: @@ -11365,6 +11437,17 @@ __metadata: languageName: node linkType: hard +"swipl-wasm@npm:4.0.13": + version: 4.0.13 + resolution: "swipl-wasm@npm:4.0.13" + dependencies: + "@types/emscripten": "npm:^1.39.13" + bin: + swipl-generate: dist/bin/index.js + checksum: 10c0/ea61942ceb60883bddd213d029ce21bb7010860621b69dfd1e13209b17948c90bc3c422153244a8662f5eb89275135b3905be5274d827ce4f2effac6a383f038 + languageName: node + linkType: hard + "syncpack@npm:^13.0.2": version: 13.0.2 resolution: "syncpack@npm:13.0.2" @@ -11478,12 +11561,10 @@ __metadata: languageName: node linkType: hard -"tmp@npm:^0.2.1": - version: 0.2.1 - resolution: "tmp@npm:0.2.1" - dependencies: - rimraf: "npm:^3.0.0" - checksum: 10c0/67607aa012059c9ce697bee820ee51bc0f39b29a8766def4f92d3f764d67c7cf9205d537d24e0cb1ce9685c40d4c628ead010910118ea18348666b5c46ed9123 +"tmp@npm:^0.2.1, tmp@npm:^0.2.3": + version: 0.2.3 + resolution: "tmp@npm:0.2.3" + checksum: 10c0/3e809d9c2f46817475b452725c2aaa5d11985cf18d32a7a970ff25b568438e2c076c2e8609224feef3b7923fa9749b74428e3e634f6b8e520c534eef2fd24125 languageName: node linkType: hard @@ -11924,6 +12005,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 10c0/bcbb807a917d374a49f475fae2e87fdca7da5e5530820ef53f65ba1d12131bd81a92ecf259cc7ce317cbe0f289e7d79fdfebcef9bfa3087c8c8a2fa304c9be54 + languageName: node + linkType: hard + "uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" @@ -11933,6 +12023,16 @@ __metadata: languageName: node linkType: hard +"uuidv4@npm:^6.2.13": + version: 6.2.13 + resolution: "uuidv4@npm:6.2.13" + dependencies: + "@types/uuid": "npm:8.3.4" + uuid: "npm:8.3.2" + checksum: 10c0/33287c7c71e19c5a9fe0d936c0df648338965442c80b5b26e51c13ec9a9c524a72c718355e0cea8a9431a78680d72a41a236b87046ecc0a05f518c8e22df9e35 + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" From 25cfa4baa67827b36860969a609c1e3ef71ca802 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 15 Jan 2025 11:38:21 +0100 Subject: [PATCH 02/15] fix: fix policy such that demo flow works; also add test --- packages/uma/config/rules/odrl/policy0.ttl | 19 +- packages/uma/package.json | 2 +- .../policies/authorizers/OdrlAuthorizer.ts | 177 ++++++++++++++---- .../uma/test/authz/OdrlAuthorizer.test.ts | 136 ++++++++++++++ yarn.lock | 4 +- 5 files changed, 302 insertions(+), 36 deletions(-) create mode 100644 packages/uma/test/authz/OdrlAuthorizer.test.ts diff --git a/packages/uma/config/rules/odrl/policy0.ttl b/packages/uma/config/rules/odrl/policy0.ttl index cac75c6a..07b9f138 100644 --- a/packages/uma/config/rules/odrl/policy0.ttl +++ b/packages/uma/config/rules/odrl/policy0.ttl @@ -9,14 +9,31 @@ ex:permission1 odrl:target . ex:permission1 odrl:assignee . ex:permission1 odrl:assigner . +ex:usagePolicy1a a odrl:Agreement . +ex:usagePolicy1a odrl:permission ex:permission1a . +ex:permission1a a odrl:Permission . +ex:permission1a odrl:action odrl:create . +ex:permission1a odrl:target . +ex:permission1a odrl:assignee . +ex:permission1a odrl:assigner . + ex:usagePolicy2 a odrl:Agreement . -ex:usagePolicy2 odrl:permission ex:permission2 . +ex:usagePolicy2 odrl:permission ex:permission2a . ex:permission2 a odrl:Permission . ex:permission2 odrl:action odrl:modify . ex:permission2 odrl:target . ex:permission2 odrl:assignee . ex:permission2 odrl:assigner . +ex:usagePolicy2a a odrl:Agreement . +ex:usagePolicy2a odrl:permission ex:permission2 . +ex:permission2a a odrl:Permission . +ex:permission2a odrl:action odrl:create . +ex:permission2a odrl:target . +ex:permission2a odrl:assignee . +ex:permission2a odrl:assigner . + + ex:usagePolicy3 a odrl:Agreement . ex:usagePolicy3 odrl:permission ex:permission3 . ex:permission3 a odrl:Permission . diff --git a/packages/uma/package.json b/packages/uma/package.json index 770b4c52..91d88346 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -73,7 +73,7 @@ "koreografeye": "^0.4.8", "logform": "^2.6.0", "n3": "^1.17.2", - "odrl-evaluator": "^0.1.0", + "odrl-evaluator": "^0.1.1", "ts-node": "^10.9.2", "uri-template-lite": "^23.4.0", "winston": "^3.11.0" diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index cb299e33..8c492707 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -1,16 +1,16 @@ -import {Authorizer} from "./Authorizer"; -import {ODRL, UconRequest, UCRulesStorage} from "@solidlab/ucp"; -import {WEBID} from "../../credentials/Claims"; -import {ODRLEvaluator, ODRLEngineMultipleSteps} from 'odrl-evaluator' -import {DataFactory, Store, Writer} from "n3"; -import quad = DataFactory.quad; -import namedNode = DataFactory.namedNode; +import { createVocabulary, DC, getLoggerFor, RDF } from '@solid/community-server'; +import { ODRL, UconRequest, UCRulesStorage } from '@solidlab/ucp'; +import { generate_uuid } from 'koreografeye'; +import { DataFactory, Literal, NamedNode, Quad_Subject, Store } from 'n3'; +import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator' +import { WEBID } from '../../credentials/Claims'; +import { ClaimSet } from '../../credentials/ClaimSet'; +import { Requirements } from '../../credentials/Requirements'; +import { Permission } from '../../views/Permission'; +import { Authorizer } from './Authorizer'; import literal = DataFactory.literal; -import {generate_uuid} from "koreografeye"; -import { getLoggerFor, RDF } from '@solid/community-server'; -import {ClaimSet} from "../../credentials/ClaimSet"; -import {Permission} from "../../views/Permission"; -import {Requirements} from "../../credentials/Requirements"; +import namedNode = DataFactory.namedNode; +import quad = DataFactory.quad; export class OdrlAuthorizer implements Authorizer { protected readonly logger = getLoggerFor(this); @@ -24,20 +24,18 @@ export class OdrlAuthorizer implements Authorizer { constructor( private readonly policies: UCRulesStorage, ) { - const engine = new ODRLEngineMultipleSteps(); + const engine = new ODRLEngineMultipleSteps(new EyeReasoner('/usr/local/bin/eye', ["--quiet", "--nope", "--pass-only-new"])); + // const engine = new ODRLEngineMultipleSteps(); this.odrlEvaluator = new ODRLEvaluator(engine); } public async permissions(claims: ClaimSet, query?: Permission[]): Promise { this.logger.info(`Calculating permissions. ${JSON.stringify({claims, query})}`); - console.log(claims) - if (!query) { this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit queries.') return []; } - this.logger.info(Object.keys(claims).length + " claim received") const requests: UconRequest[] = []; for (const {resource_id, resource_scopes} of query) { @@ -85,25 +83,23 @@ export class OdrlAuthorizer implements Authorizer { 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 permissionReportNode = reportStore.getQuads(null, "http://example.com/report/temp/rule", permissionNode, null); - if (permissionReportNode.length !== 1) { - this.logger.warn("Expected only one Permission Report. No permissions granted."); - - console.log(new Writer().quadsToString([...policyStore])) - console.log(new Writer().quadsToString([...req])) - console.log(new Writer().quadsToString([...sotw])) - console.log(new Writer().quadsToString(reports)) - break; - } - const activationState = reportStore.getObjects(permissionReportNode[0].subject, "http://example.com/report/temp/activationState", "http://example.com/report/temp/Active"); - if (activationState.length === 1) { - scopes_permitted.push(action) - } - } - + // console.log(new Writer().quadsToString([...req])) + const PolicyReportNodes = reportStore.getSubjects(RDF.type, CR.PolicyReport, null); + for (const policyReportNode of PolicyReportNodes) { + const policyReport = parseComplianceReport(policyReportNode, reportStore) + // console.log(new Writer().quadsToString([...policyStore])) + // console.log(new Writer().quadsToString([...sotw])) + // console.log(new Writer().quadsToString(reports)) + if (policyReport.ruleReport.activationState === ActivationState.Active && + policyReport.ruleReport.type === RuleReportType.PermissionReport) { + scopes_permitted.push(action) + } + } + } // extract allowed scopes return { resource_id: request.resource, @@ -147,3 +143,120 @@ function transformActionsOdrlToCss(actions: string[]): string[] { } return cssActions; } + +type PolicyReport = { + id: NamedNode; + created: Literal; + request: NamedNode; + policy: NamedNode; + ruleReport: RuleReport; +} +type RuleReport = { + id: NamedNode; + type: RuleReportType; + activationState: ActivationState + // TODO: others + 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= 'http://example.com/report/temp/Permission', + ProhibitionReport= 'http://example.com/report/temp/Prohibition', + ObligationReport= 'http://example.com/report/temp/Obligation', +} +enum SatisfactionState { + Satisfied= 'http://example.com/report/temp/Satisfied', + Unsatisfied= 'http://example.com/report/temp/Unsatisfied', +} + +enum PremiseReportType { + ConstraintReport = 'http://example.com/report/temp/ConstraintReport', + PartyReport = 'http://example.com/report/temp/PartyReport', + TargetReport = 'http://example.com/report/temp/TargetReport', + ActionReport = 'http://example.com/report/temp/ActionReport', +} + +enum ActivationState { + Active= 'http://example.com/report/temp/Active', + Inactive= 'http://example.com/report/temp/Inactive', +} +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 ruleReportNode = store.getQuads(identifier,CR.ruleReport, null, null)[0].object as NamedNode; + const premises :PremiseReport[] = []; + const premiseNodes = store.getObjects(ruleReportNode,CR.premiseReport, null) as NamedNode[]; + for (const premiseNode of premiseNodes) { + premises.push({ + id: premiseNode, + type: store.getObjects(premiseNode, RDF.type, null)[0].value as PremiseReportType, + premiseReport:[], // Note: won't get nested premises this way + satisfactionState: store.getObjects(premiseNode, CR.satisfactionState, null)[0].value as SatisfactionState + }) + } + const ruleReport :RuleReport = { + id: ruleReportNode, + type: store.getObjects(ruleReportNode, RDF.type, null)[0].value as RuleReportType, + activationState: store.getObjects(ruleReportNode, CR.activationState, null)[0].value as ActivationState, + premiseReport: premises + } + 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: ruleReport + } +} + +const CR = createVocabulary('http://example.com/report/temp/', + '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', + ) diff --git a/packages/uma/test/authz/OdrlAuthorizer.test.ts b/packages/uma/test/authz/OdrlAuthorizer.test.ts new file mode 100644 index 00000000..1bbefa4b --- /dev/null +++ b/packages/uma/test/authz/OdrlAuthorizer.test.ts @@ -0,0 +1,136 @@ +import {OdrlAuthorizer} from "../../src/policies/authorizers/OdrlAuthorizer"; +import {turtleStringToStore} from "odrl-evaluator"; +import {Store} from "n3"; +import {MemoryUCRulesStorage, UCRulesStorage} from "@solidlab/ucp"; +import {ClaimSet} from "../../src/credentials/ClaimSet"; +import {Permission} from "../../src/views/Permission"; +import {Authorizer} from "../../src/policies/authorizers/Authorizer"; + +const resourceModifyPolicy = ` +@prefix ex: . +@prefix odrl: . + +ex:usagePolicy1 a odrl:Agreement . +ex:usagePolicy1 odrl:permission ex:permission1 . +ex:permission1 a odrl:Permission . +ex:permission1 odrl:action odrl:modify . +ex:permission1 odrl:target . +ex:permission1 odrl:assignee . +ex:permission1 odrl:assigner . +`; + +const resourceCreatePolicy = ` +@prefix ex: . +@prefix odrl: . +ex:usagePolicy1a a odrl:Agreement . +ex:usagePolicy1a odrl:permission ex:permission1a . +ex:permission1a a odrl:Permission . +ex:permission1a odrl:action odrl:create . +ex:permission1a odrl:target . +ex:permission1a odrl:assignee . +ex:permission1a odrl:assigner . +` + +const containerModifyPolicy = ` +@prefix ex: . +@prefix odrl: . +ex:usagePolicy2 a odrl:Agreement . +ex:usagePolicy2 odrl:permission ex:permission2a . +ex:permission2 a odrl:Permission . +ex:permission2 odrl:action odrl:modify . +ex:permission2 odrl:target . +ex:permission2 odrl:assignee . +ex:permission2 odrl:assigner . +` + +const containerCreatePolicy = ` +@prefix ex: . +@prefix odrl: . +ex:usagePolicy2a a odrl:Agreement . +ex:usagePolicy2a odrl:permission ex:permission2 . +ex:permission2a a odrl:Permission . +ex:permission2a odrl:action odrl:create . +ex:permission2a odrl:target . +ex:permission2a odrl:assignee . +ex:permission2a odrl:assigner . +` + +const resourceReadPolicy = ` +@prefix ex: . +@prefix odrl: . +ex:usagePolicy3 a odrl:Agreement . +ex:usagePolicy3 odrl:permission ex:permission3 . +ex:permission3 a odrl:Permission . +ex:permission3 odrl:action odrl:read . +ex:permission3 odrl:target . +ex:permission3 odrl:assignee . +ex:permission3 odrl:assigner . +` +describe('Odrl Authorizer', () => { + let policyStore: Store; + let rulesStorage: UCRulesStorage = new MemoryUCRulesStorage(); + let odrlAuthorizer: Authorizer; + let claims: ClaimSet; + let query: Permission[]; + + beforeAll(async () => { + policyStore = await turtleStringToStore(resourceModifyPolicy); + await rulesStorage.addRule(policyStore); + odrlAuthorizer = new OdrlAuthorizer(rulesStorage); + }) + + beforeEach(async () => { + claims = {'urn:solidlab:uma:claims:types:webid': 'https://woslabbi.pod.knows.idlab.ugent.be/profile/card#me'} + query = [ + { + resource_id: 'http://localhost:3000/alice/other/resource.txt', + resource_scopes: ['urn:example:css:modes:write', 'urn:example:css:modes:create'] + }, + { + resource_id: 'http://localhost:3000/alice/other/', + resource_scopes: ['urn:example:css:modes:create'] + } + ] + }) + + test("for a modify policy, should give only write access when the claims match.", async () => { + const expectedPermission: Permission[] = [ + { + resource_id: 'http://localhost:3000/alice/other/resource.txt', + resource_scopes: ['urn:example:css:modes:write'] + }, + { + resource_id: 'http://localhost:3000/alice/other/', + resource_scopes: [] + } + ] + const calculatedPermissions = await odrlAuthorizer.permissions(claims, query); + expect(calculatedPermissions).toEqual(expectedPermission); + }); + + test("for a modify policy, should give no access due to lack of claims.", async () => { + claims = {} + const expectedPermission: Permission[] = [ + { + resource_id: 'http://localhost:3000/alice/other/resource.txt', + resource_scopes: [] + }, + { + resource_id: 'http://localhost:3000/alice/other/', + resource_scopes: [] + } + ] + const calculatedPermissions = await odrlAuthorizer.permissions(claims, query); + expect(calculatedPermissions).toEqual(expectedPermission); + }); + + test("for appropriate create resource policies, should give all access when the claims match.", async () => { + const policyStore = await turtleStringToStore(resourceModifyPolicy + resourceCreatePolicy + containerCreatePolicy + containerModifyPolicy + resourceReadPolicy); + const ruleStorage = new MemoryUCRulesStorage(); + await ruleStorage.addRule(policyStore); + const odrlAuthorizer = new OdrlAuthorizer(ruleStorage); + const calculatedPermissions = await odrlAuthorizer.permissions(claims, query); + expect(calculatedPermissions).toEqual(query); + + }) +}) diff --git a/yarn.lock b/yarn.lock index f7d53a0f..ff75b866 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4325,7 +4325,7 @@ __metadata: koreografeye: "npm:^0.4.8" logform: "npm:^2.6.0" n3: "npm:^1.17.2" - odrl-evaluator: "npm:^0.1.0" + odrl-evaluator: "npm:^0.1.1" ts-node: "npm:^10.9.2" uri-template-lite: "npm:^23.4.0" winston: "npm:^3.11.0" @@ -9743,7 +9743,7 @@ __metadata: languageName: node linkType: hard -"odrl-evaluator@npm:^0.1.0": +"odrl-evaluator@npm:^0.1.1": version: 0.1.1 resolution: "odrl-evaluator@npm:0.1.1" dependencies: From aab248d9c184b4c2241f179655c325badadfd7db Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 15 Jan 2025 11:47:38 +0100 Subject: [PATCH 03/15] fix: make sure eyejs reasoner is used --- packages/uma/src/policies/authorizers/OdrlAuthorizer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index 8c492707..9a789ec4 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -24,8 +24,8 @@ export class OdrlAuthorizer implements Authorizer { constructor( private readonly policies: UCRulesStorage, ) { - const engine = new ODRLEngineMultipleSteps(new EyeReasoner('/usr/local/bin/eye', ["--quiet", "--nope", "--pass-only-new"])); - // const engine = new ODRLEngineMultipleSteps(); + // const engine = new ODRLEngineMultipleSteps(new EyeReasoner('/usr/local/bin/eye', ["--quiet", "--nope", "--pass-only-new"])); + const engine = new ODRLEngineMultipleSteps(); this.odrlEvaluator = new ODRLEvaluator(engine); } From f115370d7f057fa5af883146ff0bceacee94bec2 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Thu, 16 Jan 2025 17:31:31 +0100 Subject: [PATCH 04/15] feat: add proper scripts to run odrl thingy --- package.json | 2 + .../policies/authorizers/OdrlAuthorizer.ts | 14 +- scripts/test-uma-ODRL.ts | 24 ++++ scripts/util/UMA-client.ts | 124 ++++++++++++++++++ 4 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 scripts/test-uma-ODRL.ts create mode 100644 scripts/util/UMA-client.ts diff --git a/package.json b/package.json index 583749ff..8167682c 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "build": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited -t run build", "test": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited run test", "start": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited run start", + "start:odrl": "yarn workspace @solidlab/uma run start:odrl & yarn workspace @solidlab/uma-css run start", "start:demo": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited run demo", "script:demo": "yarn exec tsx ./demo/flow.ts", "script:demo-test": "yarn exec tsx ./demo/flow-test.ts", @@ -64,6 +65,7 @@ "script:registration": "yarn exec ts-node ./scripts/test-registration.ts", "script:ucp-enforcement": "yarn exec ts-node ./scripts/test-ucp-enforcement.ts", "script:uma-ucp": "yarn exec ts-node ./scripts/test-uma-ucp.ts", + "script:uma-odrl": "yarn exec ts-node ./scripts/test-uma-ODRL.ts", "script:flow": "yarn run script:public && yarn run script:private && yarn run script:uma-ucp && yarn run script:registration && yarn run script:ucp-enforcement", "sync:list": "syncpack list-mismatches", "sync:fix": "syncpack fix-mismatches" diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index 9a789ec4..b9f6c6b7 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -2,15 +2,14 @@ import { createVocabulary, DC, getLoggerFor, RDF } from '@solid/community-server import { ODRL, UconRequest, UCRulesStorage } from '@solidlab/ucp'; import { generate_uuid } from 'koreografeye'; import { DataFactory, Literal, NamedNode, Quad_Subject, Store } from 'n3'; -import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator' +import { ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator' import { WEBID } from '../../credentials/Claims'; import { ClaimSet } from '../../credentials/ClaimSet'; import { Requirements } from '../../credentials/Requirements'; import { Permission } from '../../views/Permission'; import { Authorizer } from './Authorizer'; -import literal = DataFactory.literal; -import namedNode = DataFactory.namedNode; -import quad = DataFactory.quad; + +const {quad, namedNode, literal} = DataFactory export class OdrlAuthorizer implements Authorizer { protected readonly logger = getLoggerFor(this); @@ -93,6 +92,7 @@ export class OdrlAuthorizer implements Authorizer { // console.log(new Writer().quadsToString([...policyStore])) // console.log(new Writer().quadsToString([...sotw])) // console.log(new Writer().quadsToString(reports)) + // console.log(policyReport) if (policyReport.ruleReport.activationState === ActivationState.Active && policyReport.ruleReport.type === RuleReportType.PermissionReport) { scopes_permitted.push(action) @@ -169,9 +169,9 @@ type PremiseReport = { // is it possible to just use CR.namespace + "term"? // https://github.com/microsoft/TypeScript/issues/40793 enum RuleReportType { - PermissionReport= 'http://example.com/report/temp/Permission', - ProhibitionReport= 'http://example.com/report/temp/Prohibition', - ObligationReport= 'http://example.com/report/temp/Obligation', + PermissionReport= 'http://example.com/report/temp/PermissionReport', + ProhibitionReport= 'http://example.com/report/temp/ProhibitionReport', + ObligationReport= 'http://example.com/report/temp/ObligationReport', } enum SatisfactionState { Satisfied= 'http://example.com/report/temp/Satisfied', diff --git a/scripts/test-uma-ODRL.ts b/scripts/test-uma-ODRL.ts new file mode 100644 index 00000000..0c453bc9 --- /dev/null +++ b/scripts/test-uma-ODRL.ts @@ -0,0 +1,24 @@ +import {UserManagedAccessFetcher} from "./util/UMA-client"; + +const resource = "http://localhost:3000/alice/other/resource.txt" +const claim_token = "https://woslabbi.pod.knows.idlab.ugent.be/profile/card#me" +const claim_token_format = 'urn:solidlab:uma:claims:formats:webid' +const fetcher = new UserManagedAccessFetcher({token:claim_token, token_format: claim_token_format}); + +async function main() { + console.log(`Testing UMA flow using UMA Fetcher\n`) + const response = await fetcher.fetch(resource, { + method: "PUT", + body: "some text" + }) + + console.log(`Creating document with RPT, expecting HTTP status in 200 range: ${response.status}\n`); + + const anonymousResponse = await fetch(resource) + console.log(`Reading document without RPT, expecting HTTP status in 400 range: ${anonymousResponse.status}\n`); + + const readingResponse = await fetcher.fetch(resource) + + console.log(`Reading document with RPT, expecting the content written away: ${await readingResponse.text()}\n`); +} +main() diff --git a/scripts/util/UMA-client.ts b/scripts/util/UMA-client.ts new file mode 100644 index 00000000..e8bb02eb --- /dev/null +++ b/scripts/util/UMA-client.ts @@ -0,0 +1,124 @@ +import { fetch } from 'cross-fetch' + +/** + * Decodes a JSON Web Token (JWT) by parsing its payload. + * + * @param {string} token - The JSON Web Token to be parsed. + * @returns {Object} The decoded payload of the JWT as a JavaScript object. + * + */ +export function parseJwt(token: string): Object { + return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); +} + +/** + * Represents User-Managed Access (UMA) information, extracted from a RPT Request Response. + * + * @property {string} tokenEndpoint - The endpoint where the token can be requested. + * @property {string} ticket - The ticket used for the UMA session. + */ +export type UMA_Session = { + tokenEndpoint: string, + ticket: string +} + +/** + * Parses the 'WWW-Authenticate' header from the given headers to extract UMA session details. + * + * @param {Headers} headers - The HTTP headers from which the 'WWW-Authenticate' header is to be extracted. + * @returns {UMA_Session} The parsed UMA session details. + * @throws Will throw an error if the 'WWW-Authenticate' header is not present. + */ +export function parseAuthenticateHeader(headers: Headers): UMA_Session { + const wwwAuthenticateHeader = headers.get("WWW-Authenticate") + if (!wwwAuthenticateHeader) throw Error("No WWW-Authenticate Header present"); + + const { as_uri, ticket } = Object.fromEntries(wwwAuthenticateHeader.replace(/^UMA /, '').split(', ').map( + param => param.split('=').map(s => s.replace(/"/g, '')) + )); + + const tokenEndpoint = as_uri + "/token" // NOTE: should normally be retrieved from .well-known/uma2-configuration + + return { + tokenEndpoint, + ticket + } +} + +/** + * Represents a claim with a token and its format. + * + * @property {string} token - The claim token. + * @property {string} token_format - The format of the claim token. + */ +export type Claim = { + token: string, + token_format: string +} + +/** + * Authenticated fetcher following the User Managed Access 2.0 Grant for Oauth 2.0 Authorization flow + * using one claim. + * (https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html) + */ +export class UserManagedAccessFetcher { + private readonly claim: Claim; + private readonly grant_type= 'urn:ietf:params:oauth:grant-type:uma-ticket'; + public constructor(claim: Claim) { + this.claim = claim; + } + + public async fetch(url: string, init: RequestInit = {}): Promise { + // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.1 + // 3.1 Client Requests Resource Without Providing an Access Token + const noTokenResponse = await fetch(url, init); + if (noTokenResponse.status > 199 && noTokenResponse.status < 300) { + console.log('No Authorization token was required.') + return noTokenResponse; + } + // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.2 + // 3.2 Resource Server Responds to Client's Tokenless Access Attempt + + const { tokenEndpoint, ticket } = parseAuthenticateHeader(noTokenResponse.headers) + + const content = { + grant_type: this.grant_type, + ticket, + claim_token: encodeURIComponent(this.claim.token), + claim_token_format: this.claim.token_format, + } + + // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.1 + // 3.3.1 Client Request to Authorization Server for RPT + const asRequestResponse = await fetch(tokenEndpoint, { + method: "POST", + headers: { + "content-type": "application/json" + }, + body: JSON.stringify(content), + }); + + if (asRequestResponse.status !== 200) { + // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.6 + // 3.3.6 Authorization Server Response to Client on Authorization Failure + // TODO: log properly + return asRequestResponse + throw Error("Authorization token not granted" + await asRequestResponse.text()); + } + + // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.5 + // 3.3.5 Authorization Server Response to Client on Authorization Success + const asResponse = await asRequestResponse.json(); + + // RPT added to header + const headers = new Headers(init.headers); + headers.set('Authorization', `${asResponse.token_type} ${asResponse.access_token}`); + + // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.4 + // 3.4 Client Requests Resource and Provides an RPT + // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.5 + // 3.5 Resource Server Responds to Client's RPT-Accompanied Resource Request + return fetch(url, { ...init, headers }); + + } +} From ec07f96832f43ffd7364fc34779e7d84c3ac4691 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Mon, 20 Jan 2025 13:04:16 +0100 Subject: [PATCH 05/15] feat: clean up ODRL Authorizer --- .../policies/authorizers/OdrlAuthorizer.ts | 210 ++++++++++-------- 1 file changed, 122 insertions(+), 88 deletions(-) diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index b9f6c6b7..2236c503 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -1,6 +1,5 @@ import { createVocabulary, DC, getLoggerFor, RDF } from '@solid/community-server'; -import { ODRL, UconRequest, UCRulesStorage } from '@solidlab/ucp'; -import { generate_uuid } from 'koreografeye'; +import { basicPolicy, ODRL, UCPPolicy, UCRulesStorage } from '@solidlab/ucp'; import { DataFactory, Literal, NamedNode, Quad_Subject, Store } from 'n3'; import { ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator' import { WEBID } from '../../credentials/Claims'; @@ -11,6 +10,22 @@ import { Authorizer } from './Authorizer'; const {quad, namedNode, literal} = DataFactory +/** + * Permission evaluation is performed as follows: + * + * 1. Conversion of Permission queries to ODRL Requests. + * - A translation is performed to transform CSS actions to ODRL actions. + * - One ODRL Request per Action and target Resource. + * + * 2. ODRL Evaluator performs ODRL Evaluation + * - No policy selection is performed (all policies are inserted rather than all relevant). + * - No conflict resolution strategy is present (Prohibition policies are ignored). + * - No duties are checked. + * + * 3. Conversion from ODRL Policy Compliance Reports to Permissions + * - Selecting the ODRL actions from Active Permission Reports + * - Translation from ODRL actions to CSS actions + */ export class OdrlAuthorizer implements Authorizer { protected readonly logger = getLoggerFor(this); private readonly odrlEvaluator: ODRLEvaluator; @@ -18,6 +33,7 @@ export class OdrlAuthorizer implements Authorizer { /** * Creates a OdrlAuthorizer enforcing policies using ODRL with the ODRL Evaluator. * + * * @param policies - A store containing the ODRL policy rules. */ constructor( @@ -35,79 +51,66 @@ export class OdrlAuthorizer implements Authorizer { return []; } - const requests: UconRequest[] = []; - for (const {resource_id, resource_scopes} of query) { - - if (!resource_id) { - this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit resources.'); - continue; - } - - // ODRL can only handle odrl actions - requests.push({ - subject: typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous', - resource: resource_id, - action: resource_scopes ? transformActionsCssToOdrl(resource_scopes) : ["http://www.w3.org/ns/odrl/2/use"], - claims - }); - } - const permissions: Permission[] = await Promise.all(requests.map( - async (request) => { - const scopes_permitted = []; - - // 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")))); - - // prepare policy - const policyStore = (await this.policies.getStore()) + // key value store for building the permissions to be granted on a resource + const grantedPermissions: { [key: string]: string[] } = {}; + // prepare policy + const policyStore = (await this.policies.getStore()) - for (const action of request.action) { - // prepare request - const req = new Store(); - const requestNode = generate_uuid(); - const permissionNode = generate_uuid(); - req.add(quad(requestNode, RDF.terms.type, ODRL.terms.Request)); - req.add(quad(requestNode, ODRL.terms.permission, permissionNode)); + // 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")))); - req.add(quad(permissionNode, RDF.terms.type, ODRL.terms.Permission)); - req.add(quad(permissionNode, ODRL.terms.assignee, namedNode(request.subject))); - req.add(quad(permissionNode, ODRL.terms.target, namedNode(request.resource))); - req.add(quad(permissionNode, ODRL.terms.action, namedNode(action))); + const subject = typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous'; - // evaluate policies - const reports = await this.odrlEvaluator.evaluate([...policyStore], [...req], [...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 - // console.log(new Writer().quadsToString([...req])) + for (const {resource_id, resource_scopes} of query) { + if (!resource_id) { + this.logger.warn('The OdrlAuthorizer can only calculate permissions for explicit resources.'); + continue; + } - const PolicyReportNodes = reportStore.getSubjects(RDF.type, CR.PolicyReport, null); - for (const policyReportNode of PolicyReportNodes) { - const policyReport = parseComplianceReport(policyReportNode, reportStore) - // console.log(new Writer().quadsToString([...policyStore])) - // console.log(new Writer().quadsToString([...sotw])) - // console.log(new Writer().quadsToString(reports)) - // console.log(policyReport) - if (policyReport.ruleReport.activationState === ActivationState.Active && - policyReport.ruleReport.type === RuleReportType.PermissionReport) { - scopes_permitted.push(action) + grantedPermissions[resource_id] = []; + const actions = resource_scopes ? transformActionsCssToOdrl(resource_scopes) : ["http://www.w3.org/ns/odrl/2/use"] + for (const action of actions) { + this.logger.info(`Evaluating Request [S R AR]: [${subject} ${resource_id} ${action}]`); + const requestPolicy: UCPPolicy = { + type: ODRL.Request, + rules: [ + { + action: action, + resource: resource_id, + requestingParty: subject } - } - + ] } - // extract allowed scopes - return { - resource_id: request.resource, - resource_scopes: transformActionsOdrlToCss(scopes_permitted) + const requestStore = basicPolicy(requestPolicy).representation + // evaluate policies + const reports = await this.odrlEvaluator.evaluate( + [...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) + if (policyReport.ruleReport[0].activationState === ActivationState.Active && + policyReport.ruleReport[0].type === RuleReportType.PermissionReport) { + grantedPermissions[resource_id].push(action); + } } } - )); - // console.log(permissions) + } + const permissions: Permission[] = [] + Object.keys(grantedPermissions).forEach( + resource_id => permissions.push({ + resource_id, + resource_scopes: transformActionsOdrlToCss(grantedPermissions[resource_id]) + }) ); return permissions; } @@ -125,6 +128,10 @@ scopeCssToOdrl.set('urn:example:css:modes:write','http://www.w3.org/ns/odrl/2/wr const scopeOdrlToCss : Map = new Map(Array.from(scopeCssToOdrl, entry => [entry[1], entry[0]])); +/** + * Transform the Actions enforced by the Community Solid Server to equivalent ODRL Actions + * @param actions + */ function transformActionsCssToOdrl(actions: string[]): string[] { // scopes come from UmaClient.ts -> see CSS package @@ -132,7 +139,10 @@ function transformActionsCssToOdrl(actions: string[]): string[] { // again, see CSS package return actions.map(action => scopeCssToOdrl.get(action)!); } - +/** + * Transform ODRL Actions to equivalent Actions enforced by the Community Solid Server + * @param actions + */ function transformActionsOdrlToCss(actions: string[]): string[] { const cssActions = [] for (const action of actions) { @@ -149,13 +159,14 @@ type PolicyReport = { created: Literal; request: NamedNode; policy: NamedNode; - ruleReport: RuleReport; + ruleReport: RuleReport[]; } type RuleReport = { id: NamedNode; type: RuleReportType; activationState: ActivationState - // TODO: others + rule: NamedNode; + requestedRule: NamedNode; premiseReport: PremiseReport[] } @@ -189,35 +200,58 @@ enum ActivationState { Active= 'http://example.com/report/temp/Active', Inactive= 'http://example.com/report/temp/Inactive', } -function parseComplianceReport(identifier: Quad_Subject, store: Store):PolicyReport{ - const exists = store.getQuads(identifier,RDF.type,CR.PolicyReport, null).length ===1; + +/** + * 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 ruleReportNode = store.getQuads(identifier,CR.ruleReport, null, null)[0].object as NamedNode; - const premises :PremiseReport[] = []; - const premiseNodes = store.getObjects(ruleReportNode,CR.premiseReport, null) as NamedNode[]; - for (const premiseNode of premiseNodes) { - premises.push({ - id: premiseNode, - type: store.getObjects(premiseNode, RDF.type, null)[0].value as PremiseReportType, - premiseReport:[], // Note: won't get nested premises this way - satisfactionState: store.getObjects(premiseNode, CR.satisfactionState, null)[0].value as SatisfactionState - }) - } - const ruleReport :RuleReport = { - id: ruleReportNode, - type: store.getObjects(ruleReportNode, RDF.type, null)[0].value as RuleReportType, - activationState: store.getObjects(ruleReportNode, CR.activationState, null)[0].value as ActivationState, - premiseReport: premises - } + 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: ruleReport + 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('http://example.com/report/temp/', 'PolicyReport', 'RuleReport', From 559908b7da174e8054578e00ba90a336ef51b7fb Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Mon, 20 Jan 2025 13:28:01 +0100 Subject: [PATCH 06/15] fix: fixes typescript and componentjs build errors --- .../server/description/AccountSettingsStorageDescriber.ts | 4 ++-- packages/uma/src/credentials/verify/JwtVerifier.ts | 4 ++-- packages/uma/src/policies/authorizers/OdrlAuthorizer.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/css/src/server/description/AccountSettingsStorageDescriber.ts b/packages/css/src/server/description/AccountSettingsStorageDescriber.ts index 0444a6a8..e3d606d4 100644 --- a/packages/css/src/server/description/AccountSettingsStorageDescriber.ts +++ b/packages/css/src/server/description/AccountSettingsStorageDescriber.ts @@ -3,8 +3,8 @@ import type { AccountSettings, AccountStore, PodStore, ResourceIdentifier } from import { StorageDescriber } from '@solid/community-server'; import { DataFactory } from 'n3'; import { stringToTerm } from 'rdf-string'; -import namedNode = DataFactory.namedNode; -import quad = DataFactory.quad; + +const {quad, namedNode} = DataFactory /** * Adds triples to the storage description resource, based on the settings of diff --git a/packages/uma/src/credentials/verify/JwtVerifier.ts b/packages/uma/src/credentials/verify/JwtVerifier.ts index 4e16cef2..fe9c6da1 100644 --- a/packages/uma/src/credentials/verify/JwtVerifier.ts +++ b/packages/uma/src/credentials/verify/JwtVerifier.ts @@ -4,7 +4,7 @@ import { ClaimSet } from '../ClaimSet'; import { Credential } from "../Credential"; import { JWT } from '../Formats'; import { decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose'; -import buildGetJwks from 'get-jwks'; +import buildGetJwks, {GetJwks} from 'get-jwks'; /** * An UNSECURE Verifier that parses Tokens of the format `encode_uri(webId)[:encode_uri(clientId)]`, @@ -12,7 +12,7 @@ import buildGetJwks from 'get-jwks'; */ export class JwtVerifier implements Verifier { protected readonly logger = getLoggerFor(this); - protected jwks = buildGetJwks(); + protected jwks:GetJwks = buildGetJwks(); constructor( private readonly allowedClaims: string[], diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index 2236c503..a2006cfb 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -1,7 +1,7 @@ import { createVocabulary, DC, getLoggerFor, RDF } from '@solid/community-server'; import { basicPolicy, ODRL, UCPPolicy, UCRulesStorage } from '@solidlab/ucp'; import { DataFactory, Literal, NamedNode, Quad_Subject, Store } from 'n3'; -import { ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator' +import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator' import { WEBID } from '../../credentials/Claims'; import { ClaimSet } from '../../credentials/ClaimSet'; import { Requirements } from '../../credentials/Requirements'; @@ -39,8 +39,8 @@ export class OdrlAuthorizer implements Authorizer { constructor( private readonly policies: UCRulesStorage, ) { - // const engine = new ODRLEngineMultipleSteps(new EyeReasoner('/usr/local/bin/eye', ["--quiet", "--nope", "--pass-only-new"])); - const engine = new ODRLEngineMultipleSteps(); + const engine = new ODRLEngineMultipleSteps(new EyeReasoner('/usr/local/bin/eye', ["--quiet", "--nope", "--pass-only-new"])); + // const engine = new ODRLEngineMultipleSteps(); this.odrlEvaluator = new ODRLEvaluator(engine); } From 5f2e802642c054c1e5aedfa509f4bcbea793f937 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 4 Apr 2025 15:23:45 +0200 Subject: [PATCH 07/15] fix: Allow configuration of eye reasoner path --- packages/uma/bin/demo.ts | 1 + packages/uma/bin/main.ts | 1 + packages/uma/bin/odrl.ts | 1 + packages/uma/config/policies/authorizers/default.json | 1 + packages/uma/config/policies/authorizers/odrl.json | 1 + packages/uma/config/variables/default.json | 5 +++++ packages/uma/src/policies/authorizers/OdrlAuthorizer.ts | 6 ++++-- 7 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/uma/bin/demo.ts b/packages/uma/bin/demo.ts index c4414feb..3e7ee5b0 100644 --- a/packages/uma/bin/demo.ts +++ b/packages/uma/bin/demo.ts @@ -20,6 +20,7 @@ export const launch: () => Promise = async () => { // variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/policy'); variables['urn:uma:variables:rulesDir'] = path.join(rootDir, './config/rules/n3'); + variables['urn:uma:variables:eyePath'] = 'eye'; variables['urn:uma:variables:mainModulePath'] = rootDir; variables['urn:uma:variables:customConfigPath'] = path.join(rootDir, './config/demo.json'); diff --git a/packages/uma/bin/main.ts b/packages/uma/bin/main.ts index 2e32ab73..61c43a8a 100644 --- a/packages/uma/bin/main.ts +++ b/packages/uma/bin/main.ts @@ -20,6 +20,7 @@ export const launch: () => Promise = async () => { variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/policy'); variables['urn:uma:variables:rulesDir'] = path.join(rootDir, './config/rules/n3'); + variables['urn:uma:variables:eyePath'] = 'eye'; variables['urn:uma:variables:mainModulePath'] = rootDir; variables['urn:uma:variables:customConfigPath'] = path.join(rootDir, './config/default.json'); diff --git a/packages/uma/bin/odrl.ts b/packages/uma/bin/odrl.ts index ee487e30..c4e1e64b 100644 --- a/packages/uma/bin/odrl.ts +++ b/packages/uma/bin/odrl.ts @@ -20,6 +20,7 @@ export const launch: () => Promise = async () => { variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/odrl'); // variables['urn:uma:variables:rulesDir'] = path.join(rootDir, './config/rules/n3'); + variables['urn:uma:variables:eyePath'] = 'eye'; variables['urn:uma:variables:mainModulePath'] = rootDir; variables['urn:uma:variables:customConfigPath'] = path.join(rootDir, './config/odrl.json'); diff --git a/packages/uma/config/policies/authorizers/default.json b/packages/uma/config/policies/authorizers/default.json index 0a42821f..c6daccc1 100644 --- a/packages/uma/config/policies/authorizers/default.json +++ b/packages/uma/config/policies/authorizers/default.json @@ -51,6 +51,7 @@ "fallback": { "@id": "urn:uma:default:OdrlAuthorizer", "@type": "OdrlAuthorizer", + "eyePath": { "@id": "urn:uma:variables:eyePath" }, "policies": { "@id": "urn:uma:default:RulesStorage", "@type": "DirectoryUCRulesStorage", diff --git a/packages/uma/config/policies/authorizers/odrl.json b/packages/uma/config/policies/authorizers/odrl.json index c3df189d..c42b2a13 100644 --- a/packages/uma/config/policies/authorizers/odrl.json +++ b/packages/uma/config/policies/authorizers/odrl.json @@ -7,6 +7,7 @@ { "@id": "urn:uma:default:Authorizer", "@type": "OdrlAuthorizer", + "eyePath": { "@id": "urn:uma:variables:eyePath" }, "policies": { "@id": "urn:uma:default:RulesStorage", "@type": "DirectoryUCRulesStorage", diff --git a/packages/uma/config/variables/default.json b/packages/uma/config/variables/default.json index f71183b9..7b677185 100644 --- a/packages/uma/config/variables/default.json +++ b/packages/uma/config/variables/default.json @@ -26,6 +26,11 @@ "comment": "Path to the directory containing the N3 rules.", "@id": "urn:uma:variables:rulesDir", "@type": "Variable" + }, + { + "comment": "Path of the local eye reasoner.", + "@id": "urn:uma:variables:eyePath", + "@type": "Variable" } ] } diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index a2006cfb..2e5f65f9 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -38,9 +38,11 @@ export class OdrlAuthorizer implements Authorizer { */ constructor( private readonly policies: UCRulesStorage, + private readonly eyePath?: string, ) { - const engine = new ODRLEngineMultipleSteps(new EyeReasoner('/usr/local/bin/eye', ["--quiet", "--nope", "--pass-only-new"])); - // const engine = new ODRLEngineMultipleSteps(); + const engine = eyePath ? + new ODRLEngineMultipleSteps(new EyeReasoner(eyePath, ["--quiet", "--nope", "--pass-only-new"])) : + new ODRLEngineMultipleSteps(); this.odrlEvaluator = new ODRLEvaluator(engine); } From b59061f6ba376ff633677f99d7ad8da3194b34f1 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 27 May 2025 13:31:16 +0200 Subject: [PATCH 08/15] fix: Make test scripts work when using ODRL evaluator --- packages/uma/config/rules/policy/policy0.ttl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/uma/config/rules/policy/policy0.ttl b/packages/uma/config/rules/policy/policy0.ttl index 340e0217..8a267103 100644 --- a/packages/uma/config/rules/policy/policy0.ttl +++ b/packages/uma/config/rules/policy/policy0.ttl @@ -4,7 +4,15 @@ ex:usagePolicy a odrl:Agreement . ex:usagePolicy odrl:permission ex:permission . ex:permission a odrl:Permission . -ex:permission odrl:action odrl:read , odrl:modify . +ex:permission odrl:action odrl:read , odrl:create , odrl:modify . ex:permission odrl:target , . ex:permission odrl:assignee . ex:permission odrl:assigner . + +ex:usagePolicy2 a odrl:Agreement . +ex:usagePolicy2 odrl:permission ex:permission2 . +ex:permission2 a odrl:Permission . +ex:permission2 odrl:action odrl:create , odrl:modify . +ex:permission2 odrl:target , . +ex:permission2 odrl:assignee . +ex:permission2 odrl:assigner . From 7ca95ab6a89d5a780b7a602b46eced518320f6ec Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 27 May 2025 08:53:04 +0200 Subject: [PATCH 09/15] feat: Update ODRL evaluator --- packages/uma/package.json | 2 +- .../policies/authorizers/OdrlAuthorizer.ts | 36 +++---- yarn.lock | 98 ++++++++++++++++++- 3 files changed, 113 insertions(+), 23 deletions(-) diff --git a/packages/uma/package.json b/packages/uma/package.json index 91d88346..59a154c7 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -73,7 +73,7 @@ "koreografeye": "^0.4.8", "logform": "^2.6.0", "n3": "^1.17.2", - "odrl-evaluator": "^0.1.1", + "odrl-evaluator": "^0.3.0", "ts-node": "^10.9.2", "uri-template-lite": "^23.4.0", "winston": "^3.11.0" diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts index 2e5f65f9..70bac1d8 100644 --- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts @@ -1,6 +1,6 @@ import { createVocabulary, DC, getLoggerFor, RDF } from '@solid/community-server'; import { basicPolicy, ODRL, UCPPolicy, UCRulesStorage } from '@solidlab/ucp'; -import { DataFactory, Literal, NamedNode, Quad_Subject, Store } from 'n3'; +import { DataFactory, Literal, NamedNode, Quad_Subject, Store, Writer } from 'n3'; import { EyeReasoner, ODRLEngineMultipleSteps, ODRLEvaluator } from 'odrl-evaluator' import { WEBID } from '../../credentials/Claims'; import { ClaimSet } from '../../credentials/ClaimSet'; @@ -35,13 +35,14 @@ 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, - private readonly eyePath?: string, + eyePath?: string, ) { const engine = eyePath ? - new ODRLEngineMultipleSteps(new EyeReasoner(eyePath, ["--quiet", "--nope", "--pass-only-new"])) : + new ODRLEngineMultipleSteps({reasoner: new EyeReasoner(eyePath, ["--quiet", "--nope", "--pass-only-new"])}) : new ODRLEngineMultipleSteps(); this.odrlEvaluator = new ODRLEvaluator(engine); } @@ -100,8 +101,9 @@ export class OdrlAuthorizer implements Authorizer { const PolicyReportNodes = reportStore.getSubjects(RDF.type, CR.PolicyReport, null); for (const policyReportNode of PolicyReportNodes) { const policyReport = parseComplianceReport(policyReportNode, reportStore) - if (policyReport.ruleReport[0].activationState === ActivationState.Active && - policyReport.ruleReport[0].type === RuleReportType.PermissionReport) { + const activeReports = policyReport.ruleReport.filter( + (report) => report.activationState === ActivationState.Active); + if (activeReports.length > 0 && activeReports[0].type === RuleReportType.PermissionReport) { grantedPermissions[resource_id].push(action); } } @@ -182,25 +184,25 @@ type PremiseReport = { // is it possible to just use CR.namespace + "term"? // https://github.com/microsoft/TypeScript/issues/40793 enum RuleReportType { - PermissionReport= 'http://example.com/report/temp/PermissionReport', - ProhibitionReport= 'http://example.com/report/temp/ProhibitionReport', - ObligationReport= 'http://example.com/report/temp/ObligationReport', + 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= 'http://example.com/report/temp/Satisfied', - Unsatisfied= 'http://example.com/report/temp/Unsatisfied', + Satisfied= 'https://w3id.org/force/compliance-report#Satisfied', + Unsatisfied= 'https://w3id.org/force/compliance-report#Unsatisfied', } enum PremiseReportType { - ConstraintReport = 'http://example.com/report/temp/ConstraintReport', - PartyReport = 'http://example.com/report/temp/PartyReport', - TargetReport = 'http://example.com/report/temp/TargetReport', - ActionReport = 'http://example.com/report/temp/ActionReport', + 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= 'http://example.com/report/temp/Active', - Inactive= 'http://example.com/report/temp/Inactive', + Active= 'https://w3id.org/force/compliance-report#Active', + Inactive= 'https://w3id.org/force/compliance-report#Inactive', } /** @@ -254,7 +256,7 @@ function parsePremiseReport(identifier: Quad_Subject, store: Store): PremiseRepo satisfactionState: store.getObjects(identifier, CR.satisfactionState, null)[0].value as SatisfactionState } } -const CR = createVocabulary('http://example.com/report/temp/', +const CR = createVocabulary('https://w3id.org/force/compliance-report#', 'PolicyReport', 'RuleReport', 'PermissionReport', diff --git a/yarn.lock b/yarn.lock index ff75b866..e0b71c5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4096,6 +4096,15 @@ __metadata: languageName: node linkType: hard +"@rdfjs/types@npm:^2.0.0, @rdfjs/types@npm:^2.0.1": + version: 2.0.1 + resolution: "@rdfjs/types@npm:2.0.1" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/81012b02e28737e15dfc27068664bbab7b989cc2ff82e0a6a341df492d9d97210b5e462d85bfb7882aa0ef5bf84e2c5e95647fb779b67b7b1910b1837d79c500 + languageName: node + linkType: hard + "@rubensworks/saxes@npm:^6.0.1": version: 6.0.1 resolution: "@rubensworks/saxes@npm:6.0.1" @@ -4325,7 +4334,7 @@ __metadata: koreografeye: "npm:^0.4.8" logform: "npm:^2.6.0" n3: "npm:^1.17.2" - odrl-evaluator: "npm:^0.1.1" + odrl-evaluator: "npm:^0.3.0" ts-node: "npm:^10.9.2" uri-template-lite: "npm:^23.4.0" winston: "npm:^3.11.0" @@ -4440,6 +4449,18 @@ __metadata: languageName: node linkType: hard +"@treecg/types@npm:^0.4.6": + version: 0.4.6 + resolution: "@treecg/types@npm:0.4.6" + dependencies: + "@rdfjs/types": "npm:*" + loglevel: "npm:^1.8.1" + loglevel-plugin-prefix: "npm:^0.8.4" + rdf-data-factory: "npm:^1.1.0" + checksum: 10c0/956dfc34dbbcad4ef3099431be55bd1f5c8b1f5fca71f0bc90ca5823017ba25c70f6db1a066691541c900a59e1ad7522f041965162f66627e19e20c99bce5cf9 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.9 resolution: "@tsconfig/node10@npm:1.0.9" @@ -4842,6 +4863,16 @@ __metadata: languageName: node linkType: hard +"@types/n3@npm:^1.21.1": + version: 1.24.2 + resolution: "@types/n3@npm:1.24.2" + dependencies: + "@rdfjs/types": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/4d9fceea390134a1e4c3da63d493b841ffea8ef581264fdd6b764809403f99e94517770799fa53bfc01000b88a2627385502b20df7c0e7461869e694bb667b0a + languageName: node + linkType: hard + "@types/node@npm:^20.11.25": version: 20.17.17 resolution: "@types/node@npm:20.17.17" @@ -9139,6 +9170,20 @@ __metadata: languageName: node linkType: hard +"loglevel-plugin-prefix@npm:^0.8.4": + version: 0.8.4 + resolution: "loglevel-plugin-prefix@npm:0.8.4" + checksum: 10c0/357524eec4c165ff823b5bbf72e8373ff529e5cb95c1f4b20749847bd5b5b16ab328d6d33d1a9019f1a2dc52e28fca5d595e52f2ee20e24986182a6f9552a9ec + languageName: node + linkType: hard + +"loglevel@npm:^1.8.1": + version: 1.9.2 + resolution: "loglevel@npm:1.9.2" + checksum: 10c0/1e317fa4648fe0b4a4cffef6de037340592cee8547b07d4ce97a487abe9153e704b98451100c799b032c72bb89c9366d71c9fb8192ada8703269263ae77acdc7 + languageName: node + linkType: hard + "lowercase-keys@npm:^3.0.0": version: 3.0.0 resolution: "lowercase-keys@npm:3.0.0" @@ -9535,6 +9580,16 @@ __metadata: languageName: node linkType: hard +"n3@npm:^1.23.1": + version: 1.25.2 + resolution: "n3@npm:1.25.2" + dependencies: + buffer: "npm:^6.0.3" + readable-stream: "npm:^4.0.0" + checksum: 10c0/47cb03555ddceb98be9c5ed10d67c9a045afe13d1a4225fe2ace9a63dcf8e1b6afe6019d72cb03e320c2c1d2fb6942c18ec92cdf3673e792a2084d5b1b5e089f + languageName: node + linkType: hard + "nanoid@npm:^5.0.4": version: 5.0.4 resolution: "nanoid@npm:5.0.4" @@ -9743,21 +9798,23 @@ __metadata: languageName: node linkType: hard -"odrl-evaluator@npm:^0.1.1": - version: 0.1.1 - resolution: "odrl-evaluator@npm:0.1.1" +"odrl-evaluator@npm:^0.3.0": + version: 0.3.0 + resolution: "odrl-evaluator@npm:0.3.0" dependencies: "@rdfjs/types": "npm:^1.1.0" "@types/n3": "npm:^1.16.3" eyereasoner: "npm:^16.18.4" n3: "npm:^1.20.4" rdf-isomorphic: "npm:^1.3.1" + rdf-lens: "npm:^1.3.5" rdf-parse: "npm:^3.0.0" rdf-store-stream: "npm:^2.0.1" + rdf-vocabulary: "npm:^1.0.1" streamify-string: "npm:^1.0.1" tmp: "npm:^0.2.3" uuidv4: "npm:^6.2.13" - checksum: 10c0/5091b4402b2def1fb6bf8e7749d7f2b89a1817da59abfa6ae9f1a0db2fd3ae50222eebc578fba21f9d0f455cb5b515a067ac13584c43e3ba98f50454ccf30e16 + checksum: 10c0/a42fd82a48012ffbd8ff7165883bce10bce1a31fd5d8f82ba951cf67a9b438bbcf5d4fc6eb5ac52635ebc7c2411613509267cecf41ed3169ae5a5fe88da261f3 languageName: node linkType: hard @@ -10230,6 +10287,15 @@ __metadata: languageName: node linkType: hard +"rdf-data-factory@npm:^2.0.2": + version: 2.0.2 + resolution: "rdf-data-factory@npm:2.0.2" + dependencies: + "@rdfjs/types": "npm:^2.0.0" + checksum: 10c0/5c72ebc22f4910fd35bf3efb1e1def10785aca6f8d22dd8cee34c9ffb764225698ebdfeaf09a2afba6c25e17733940ab52cefd5c751cc2720b29b5ee56ef32ee + languageName: node + linkType: hard + "rdf-dereference@npm:^2.2.0": version: 2.2.0 resolution: "rdf-dereference@npm:2.2.0" @@ -10293,6 +10359,19 @@ __metadata: languageName: node linkType: hard +"rdf-lens@npm:^1.3.5": + version: 1.3.5 + resolution: "rdf-lens@npm:1.3.5" + dependencies: + "@rdfjs/types": "npm:^2.0.1" + "@treecg/types": "npm:^0.4.6" + "@types/n3": "npm:^1.21.1" + n3: "npm:^1.23.1" + rdf-data-factory: "npm:^2.0.2" + checksum: 10c0/28522512d32569e913cefc37ee125f47942943a67e2daf407195162ba000b0b46317599ed78ec3432cbdf67118ba0a1b70126a116a8d023d2f99fac7f935b822 + languageName: node + linkType: hard + "rdf-literal@npm:^1.2.0, rdf-literal@npm:^1.3.0": version: 1.3.1 resolution: "rdf-literal@npm:1.3.1" @@ -10506,6 +10585,15 @@ __metadata: languageName: node linkType: hard +"rdf-vocabulary@npm:^1.0.1": + version: 1.0.1 + resolution: "rdf-vocabulary@npm:1.0.1" + dependencies: + "@rdfjs/types": "npm:*" + checksum: 10c0/5f9f9a7c2dea9084b57a26b03989b24b00c2d606eefe18f8a12adf5425da11329f01e132ed3ce0363a399a854cacad38226865426ecab92dd0719846c6eea34e + languageName: node + linkType: hard + "rdfa-streaming-parser@npm:^2.0.1": version: 2.0.1 resolution: "rdfa-streaming-parser@npm:2.0.1" From 765dfd8da2eafdfe8c2d585c4fc58d4e02dc1db6 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 27 May 2025 08:53:55 +0200 Subject: [PATCH 10/15] fix: Prevent error for namespaces without authorizer --- packages/uma/src/policies/authorizers/NamespacedAuthorizer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uma/src/policies/authorizers/NamespacedAuthorizer.ts b/packages/uma/src/policies/authorizers/NamespacedAuthorizer.ts index 2d199800..9236e4f3 100644 --- a/packages/uma/src/policies/authorizers/NamespacedAuthorizer.ts +++ b/packages/uma/src/policies/authorizers/NamespacedAuthorizer.ts @@ -42,7 +42,7 @@ export class NamespacedAuthorizer implements Authorizer { } // Find applicable authorizer - const authorizer = ns ? this.authorizers[ns] : this.fallback; + const authorizer = (ns && this.authorizers[ns]) || this.fallback; // Delegate to authorizer return authorizer.permissions(claims, query); From 685f6ba244514a05de3adbcd41cc93bded840e08 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 27 May 2025 13:07:00 +0200 Subject: [PATCH 11/15] fix: Update demo scripts to work with ODRL evaluator --- demo/flow-test.ts | 41 +++++++++++++++--------------- demo/flow.ts | 48 +++++++++++++++++------------------ packages/uma/config/demo.json | 5 +--- 3 files changed, 44 insertions(+), 50 deletions(-) diff --git a/demo/flow-test.ts b/demo/flow-test.ts index db9f999f..53ed6e5c 100644 --- a/demo/flow-test.ts +++ b/demo/flow-test.ts @@ -65,7 +65,7 @@ async function main() { log('Error fetching WebID data:', e); return; } - + const umaServer = webIdData.getObjects(terms.agents.ruben, terms.solid.umaServer, null)[0].value; const configUrl = new URL('.well-known/uma2-configuration', umaServer); const umaConfig = await (await fetch(configUrl)).json(); @@ -84,8 +84,8 @@ Target Resource: ${terms.resources.smartwatch}`) log('To protect this data, a policy is added restricting access to a specific healthcare employee for the purpose of bariatric care.'); log(chalk.italic(`Note: Policy management is out of scope for POC1, right now they are just served from a public container on the pod. additionally, selecting relevant policies is not implemented at the moment, all policies are evaluated, but this is a minor fix in the AS.`)) - -const healthcare_patient_policy = + +const healthcare_patient_policy = `@prefix dcterms: . @prefix eu-gdpr: . @prefix oac: . @@ -94,29 +94,28 @@ const healthcare_patient_policy = @prefix ex: . - a odrl:Request ; - odrl:uid ex:HCPX-request ; + a odrl:Agreement ; + odrl:uid ex:HCPX-agreement ; odrl:profile oac: ; - dcterms:description "HCP X requests to read Alice's health data for bariatric care."; - odrl:permission . + odrl:permission . - a odrl:Permission ; + a odrl:Permission ; odrl:action odrl:read ; odrl:target ; odrl:assigner <${terms.agents.ruben}> ; odrl:assignee <${terms.agents.alice}> ; - odrl:constraint , - . + odrl:constraint , + . a odrl:AssetCollection; odrl:source <${terms.resources.collectionSource}> . - a odrl:Constraint ; + a odrl:Constraint ; odrl:leftOperand odrl:purpose ; # can also be oac:Purpose, to conform with OAC profile odrl:operator odrl:eq ; odrl:rightOperand ex:bariatric-care . - a odrl:Constraint ; + a odrl:Constraint ; odrl:leftOperand oac:LegalBasis ; odrl:operator odrl:eq ; odrl:rightOperand eu-gdpr:A9-2-a .` @@ -211,7 +210,7 @@ const healthcare_patient_policy = ], } ], // claims: [{ - claim_token: claim_token, + claim_token: claim_token, claim_token_format: "urn:solidlab:uma:claims:formats:jwt", // }], // UMA specific fields @@ -224,15 +223,15 @@ const healthcare_patient_policy = if (response2.failed) { throw new Error(`Resource request for ${terms.resources.smartwatch} should not have failed with claims: ${response}`) } - + const access_token = parseJwt(response2.access_token) - log(`The UMA server checks the claims with the relevant policy, and returns the agent an access token with the requested permissions.`, + log(`The UMA server checks the claims with the relevant policy, and returns the agent an access token with the requested permissions.`, JSON.stringify(access_token.permissions, null, 2)); - - log(`and the accompanying agreement:`, + + log(`and the accompanying agreement:`, JSON.stringify(access_token.contract, null, 2)); - + log(chalk.italic(`Future work: at a later stage, this agreements will be signed by both parties to form a binding contract.`)) const accessWithTokenResponse = await fetch(terms.resources.smartwatch, { @@ -242,7 +241,7 @@ const healthcare_patient_policy = log(`Now the doctor can retrieve the resource:`, await accessWithTokenResponse.text()); if (accessWithTokenResponse.status !== 200) { log(`Access with token failed...`); throw 0; } - + } main(); @@ -270,7 +269,7 @@ async function executeReadWithClaims(target: string, request: any, options: { to method: "GET", headers: { "content-type": "application/json" }, }); - + const umaHeader = await res.headers.get('WWW-Authenticate') log(`Resource request to ${target} results in ${umaHeader}`) @@ -287,7 +286,7 @@ async function executeReadWithClaims(target: string, request: any, options: { to headers: { "content-type": "application/json" }, body: JSON.stringify(request), }); - + // if (response.status !== 403) { log('Access request succeeded without claims...', await response.text()); throw 0; } const responseJSON = await response.json(); diff --git a/demo/flow.ts b/demo/flow.ts index d3cb0709..b88bca26 100644 --- a/demo/flow.ts +++ b/demo/flow.ts @@ -54,7 +54,7 @@ const policyContainer = 'http://localhost:3000/ruben/settings/policies/'; async function main() { const webIdData = new Store(parser.parse(await (await fetch(terms.agents.ruben)).text())); - + const umaServer = webIdData.getObjects(terms.agents.ruben, terms.solid.umaServer, null)[0].value; const configUrl = new URL('.well-known/uma2-configuration', umaServer); const umaConfig = await (await fetch(configUrl)).json(); @@ -73,8 +73,8 @@ Target Resource: ${terms.resources.smartwatch}`) log('To protect this data, a policy is added restricting access to a specific healthcare employee for the purpose of bariatric care.'); log(chalk.italic(`Note: Policy management is out of scope for POC1, right now they are just served from a public container on the pod. additionally, selecting relevant policies is not implemented at the moment, all policies are evaluated, but this is a minor fix in the AS.`)) - - const healthcare_patient_policy = + + const healthcare_patient_policy = `PREFIX dcterms: PREFIX eu-gdpr: PREFIX oac: @@ -83,26 +83,25 @@ PREFIX xsd: PREFIX ex: - a odrl:Request ; - odrl:uid ex:HCPX-request ; + a odrl:Agreement ; + odrl:uid ex:HCPX-agreement ; odrl:profile oac: ; - dcterms:description "HCP X requests to read Alice's health data for bariatric care."; - odrl:permission . + odrl:permission . - a odrl:Permission ; + a odrl:Permission ; odrl:action odrl:read ; odrl:target <${terms.resources.smartwatch}> ; odrl:assigner <${terms.agents.ruben}> ; odrl:assignee <${terms.agents.alice}> ; - odrl:constraint , - . + odrl:constraint , + . - a odrl:Constraint ; + a odrl:Constraint ; odrl:leftOperand odrl:purpose ; # can also be oac:Purpose, to conform with OAC profile odrl:operator odrl:eq ; odrl:rightOperand ex:bariatric-care . - a odrl:Constraint ; + a odrl:Constraint ; odrl:leftOperand oac:LegalBasis ; odrl:operator odrl:eq ; odrl:rightOperand eu-gdpr:A9-2-a .` @@ -129,7 +128,7 @@ on the condition of the purpose of the request being "http://example.org/bariatr method: "GET", headers: { "content-type": "application/json" }, }); - + const umaHeader = await res.headers.get('WWW-Authenticate') log(`First, a resource request is done without authorization that results in a 403 response and accompanying UMA ticket in the WWW-Authenticate header according to the UMA specification: @@ -164,12 +163,12 @@ ${umaHeader}`) headers: { "content-type": "application/json" }, body: JSON.stringify(smartWatchAccessRequestNoClaimsODRL), }); - + if (doctor_needInfoResponse.status !== 403) { log('Access request succeeded without claims...', await doctor_needInfoResponse.text()); throw 0; } const { ticket: ticket2, required_claims: doctor_claims } = await doctor_needInfoResponse.json(); ticket = ticket2 - + log(`Based on the policy set above, the Authorization Server requests the following claims from the doctor:`); doctor_claims.claim_token_format[0].forEach((format: string) => log(` - ${format}`)) log(`accompanied by an updated ticket: ${ticket}.`) @@ -225,7 +224,7 @@ ${umaHeader}`) ], } ], // claims: [{ - claim_token: claim_token, + claim_token: claim_token, claim_token_format: "urn:solidlab:uma:claims:formats:jwt", // }], // UMA specific fields @@ -235,7 +234,7 @@ ${umaHeader}`) log('Together with the UMA grant_type and ticket requirements, these are bundled as an ODRL Request and sent back to the Authorization Server') log(JSON.stringify(smartWatchAccessRequestODRL, null, 2)) - + log(chalk.italic(`Note: the ODRL Request constraints are not yet evaluated as claims, only the passed claim token is. There are two main points of work here: right now the claim token gathers all claims internally, as only a single token can be passed. This is problematic when claims and OIDC tokens have to be passed. It might be worth looking deeper into ODRL requests to carry these claims instead of an UMA token.`)) @@ -246,19 +245,19 @@ This is problematic when claims and OIDC tokens have to be passed. It might be w body: JSON.stringify(smartWatchAccessRequestODRL) }); - if (accessGrantedResponse.status !== 200) { - log('Access request failed despite policy...', JSON.stringify(await accessGrantedResponse.text(), null, 2)); throw 0; + if (accessGrantedResponse.status !== 200) { + log('Access request failed despite policy...', JSON.stringify(await accessGrantedResponse.text(), null, 2)); throw 0; } const tokenParams = await accessGrantedResponse.json(); const access_token = parseJwt(tokenParams.access_token) - log(`The UMA server checks the claims with the relevant policy, and returns the agent an access token with the requested permissions.`, + log(`The UMA server checks the claims with the relevant policy, and returns the agent an access token with the requested permissions.`, JSON.stringify(access_token.permissions, null, 2)); - - log(`and the accompanying agreement:`, + + log(`and the accompanying agreement:`, JSON.stringify(access_token.contract, null, 2)); - + log(chalk.italic(`Future work: at a later stage, this agreements will be signed by both parties to form a binding contract.`)) const accessWithTokenResponse = await fetch(terms.resources.smartwatch, { @@ -268,7 +267,7 @@ This is problematic when claims and OIDC tokens have to be passed. It might be w log(`Now the doctor can retrieve the resource:`, await accessWithTokenResponse.text()); if (accessWithTokenResponse.status !== 200) { log(`Access with token failed...`); throw 0; } - + } main(); @@ -301,4 +300,3 @@ async function initContainer(policyContainer: string): Promise { } } } - diff --git a/packages/uma/config/demo.json b/packages/uma/config/demo.json index 878290dc..5ba92ebd 100644 --- a/packages/uma/config/demo.json +++ b/packages/uma/config/demo.json @@ -33,10 +33,7 @@ "@id": "urn:uma:default:AllAuthorizer" } } - ], - "fallback": { - "@id": "urn:uma:default:PolicyBasedAuthorizer" - } + ] } }, { From 60950b0b3113048a2013ee60f22d4b00d1ca7554 Mon Sep 17 00:00:00 2001 From: Wouter Termont Date: Tue, 22 Apr 2025 11:15:34 +0200 Subject: [PATCH 12/15] fix: override default authorizer globally Signed-off-by: Wouter Termont --- packages/css/config/uma/default.json | 1 + .../uma/overrides/authorization-handler.json | 9 +----- .../css/config/uma/overrides/authorizer.json | 30 +++++++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 packages/css/config/uma/overrides/authorizer.json diff --git a/packages/css/config/uma/default.json b/packages/css/config/uma/default.json index 59fffc44..61ddfd5f 100644 --- a/packages/css/config/uma/default.json +++ b/packages/css/config/uma/default.json @@ -6,6 +6,7 @@ "uma-css:config/uma/overrides/account-seeding.json", "uma-css:config/uma/overrides/account-store.json", "uma-css:config/uma/overrides/authorization-handler.json", + "uma-css:config/uma/overrides/authorizer.json", "uma-css:config/uma/overrides/jwks.json", "uma-css:config/uma/overrides/token-extractor.json", "uma-css:config/uma/overrides/www-auth.json", diff --git a/packages/css/config/uma/overrides/authorization-handler.json b/packages/css/config/uma/overrides/authorization-handler.json index b3a4d4a8..05fbc48f 100644 --- a/packages/css/config/uma/overrides/authorization-handler.json +++ b/packages/css/config/uma/overrides/authorization-handler.json @@ -18,14 +18,7 @@ "credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" }, "modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" }, "permissionReader": { "@id": "urn:solid-server:default:PermissionReader" }, - "authorizer": { - "comment": "Requests UMA ticket when authorization fails.", - "@id": "urn:solid-server:default:UmaAuthorizer", - "@type": "UmaAuthorizer", - "authorizer": { "@id": "urn:solid-server:default:Authorizer" }, - "umaClient": { "@id": "urn:solid-server:default:UmaClient" }, - "ownerUtil": { "@id": "urn:solid-server:default:OwnerUtil" } - }, + "authorizer": { "@id": "urn:solid-server:default:Authorizer" }, "operationHandler": { "@id": "urn:solid-server:default:OperationHandler" } } } diff --git a/packages/css/config/uma/overrides/authorizer.json b/packages/css/config/uma/overrides/authorizer.json new file mode 100644 index 00000000..10a337a6 --- /dev/null +++ b/packages/css/config/uma/overrides/authorizer.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld", + "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma-css/^0.0.0/components/context.jsonld" + ], + "@graph": [ + { + "@id": "urn:solid-server:overrides:AuthorizerOverride", + "@type": "Override", + "overrideInstance": { + "@id": "urn:solid-server:default:Authorizer" + }, + "overrideParameters": { + "@type": "UmaAuthorizer", + "authorizer": { + "@type": "PermissionBasedAuthorizer", + "resourceSet": { + "@id": "urn:solid-server:default:CachedResourceSet" + } + }, + "umaClient": { + "@id": "urn:solid-server:default:UmaClient" + }, + "ownerUtil": { + "@id": "urn:solid-server:default:OwnerUtil" + } + } + } + ] +} From f656a048701942d5adc206c210e8f12e55d7ec6c Mon Sep 17 00:00:00 2001 From: "Wouter Termont (imec)" Date: Tue, 22 Apr 2025 11:39:08 +0200 Subject: [PATCH 13/15] build: prevent CI from overwriting lockfile --- .github/workflows/push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index c8dc3c3b..301f703e 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -31,4 +31,4 @@ jobs: uses: actions/checkout@v4 - name: Yarn install - run: yarn install + run: yarn install --frozen-lockfile From b9909468e4294cd00e5d9999a7570c5263d41186 Mon Sep 17 00:00:00 2001 From: "Wouter Termont (imec)" Date: Tue, 22 Apr 2025 12:15:02 +0200 Subject: [PATCH 14/15] build: checkout branch before corepack --- .github/workflows/push.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 301f703e..4988221c 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -19,6 +19,9 @@ jobs: steps: + - name: Checkout main branch + uses: actions/checkout@v4 + - name: Enable Node.js Corepack run: corepack enable @@ -27,8 +30,5 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Checkout main branch - uses: actions/checkout@v4 - - name: Yarn install - run: yarn install --frozen-lockfile + run: yarn install From 7a3d5a93a798f64096a70cbc8d839d9101790ce7 Mon Sep 17 00:00:00 2001 From: Wouter Termont Date: Tue, 22 Apr 2025 12:57:27 +0200 Subject: [PATCH 15/15] build: bump node versions Signed-off-by: Wouter Termont --- .github/workflows/push.yml | 6 +++--- .nvmrc | 2 +- .yarnrc.yml | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 4988221c..efcaea13 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -13,9 +13,9 @@ jobs: fail-fast: false matrix: node-version: - - 18.x # Maintenance - - 20.x # Active - - 21.x # Current + - 20.x # Maintenance + - 22.x # Active + - 23.x # Current steps: diff --git a/.nvmrc b/.nvmrc index 2edeafb0..2bd5a0a9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 \ No newline at end of file +22 diff --git a/.yarnrc.yml b/.yarnrc.yml index 348a705c..4e256646 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,30 +1,30 @@ -nodeLinker: "node-modules" +nodeLinker: node-modules # packageExtensions: # "@comunica/actor-abstract-parse@*": # dependencies: -# "@types/node": ^18 +# "@types/node": ^20 # "@types/readable-stream": ^4 # "@comunica/types@*": # dependencies: -# "@types/node": ^18 +# "@types/node": ^20 # "@solid/community-server@*": # dependencies: # "@types/koa": ^2 -# rdf-js: ^2 +# "rdf-js": ^2 # asynciterator@*: # dependencies: -# "@types/node": ^18 +# "@types/node": ^20 # rdf-parse@*: # dependencies: -# "@types/node": ^18 +# "@types/node": ^20 # "@types/readable-stream": ^4 -# rdf-js: ^2 +# "rdf-js": ^2 # undici-types@*: # dependencies: -# "@types/node": ^18 +# "@types/node": ^20 # winston-transport@*: # dependencies: -# "@types/node": ^18 +# "@types/node": ^20 # winston@*: # dependencies: -# "@types/node": ^18 \ No newline at end of file +# "@types/node": ^20