diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6d68aeaf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.gitignore b/.gitignore index b3c11572..257bbd16 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ tmp # Misc .DS_Store +.vscode/ \ No newline at end of file diff --git a/.syncpackrc b/.syncpackrc new file mode 100644 index 00000000..e1fa2c98 --- /dev/null +++ b/.syncpackrc @@ -0,0 +1,10 @@ +{ + "versionGroups": [ + { + "label": "Use workspace protocol when developing local packages", + "dependencies": ["@solidlab/uma-css", "@solidlab/ucp", "@solidlab/uma"], + "dependencyTypes": ["prod", "dev"], + "pinVersion": "workspace:^" + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3f500cc0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:20.0.0 +ENV NODE_ENV=production +WORKDIR /usr/src/app +# COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"] +# RUN npm install -g yarn + +COPY . . + +ENV YARN_VERSION 4.0.0 +RUN yarn policies set-version $YARN_VERSION + +RUN corepack enable yarn +RUN yarn install +# COPY . . + +RUN yarn build + +EXPOSE 3000 +EXPOSE 4000 +EXPOSE 4444 +EXPOSE 5123 +EXPOSE 8201 +EXPOSE 8202 +EXPOSE 8203 + +RUN chown -R node /usr/src/app +USER node +CMD ["yarn", "start:demo"] diff --git a/README.md b/README.md index 67ab303b..e4d87ba8 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,26 @@ - -# SolidLab's User Managed Access +# SolidLab's User Managed Access This repository contains SolidLab research artefacts on use of UMA in the Solid ecosystem. ## Packages -- [`@solidlab/uma`](packages/uma): Experimental and opinionated implementation of [UMA Grants](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html) and [UMA Federation](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-federated-authz-2.0.html). +- [`@solidlab/uma`](packages/uma): Experimental and opinionated implementation of [UMA Grants](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html) and [UMA Federation](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-federated-authz-2.0.html). -- [`@solidlab/uma-css`](packages/css): UMA modules for the [Community Solid Server](https://github.com/CommunitySolidServer/CommunitySolidServer/). +- [`@solidlab/uma-css`](packages/css): UMA modules for the [Community Solid Server](https://github.com/CommunitySolidServer/CommunitySolidServer/). - [`@solidlab/ucp`](packages/ucp): Usage Control Policy decision/enforcement component. - ## Getting started -In order to run this project you need to perform the following steps. +In order to run this project you need to perform the following steps. 1. Ensure that you are using Node.js 20 or higher, e.g. by running `nvm use`. (see [.nvmrc](./.nvmrc)) -1. Enable Node.js Corepack with `corepack enable`. -1. Run `yarn install` in the project root (this will automatically call `yarn build:all`). -1. Run `yarn start:all`. +2. Enable Node.js Corepack with `corepack enable`. +3. Run `yarn install` in the project root (this will automatically call `yarn build`). +4. Run `yarn start`. -This will boot up a UMA server and compatible Community Solid Server instance. +This will boot up a UMA server and compatible Community Solid Server instance. You can then execute the following flows: @@ -34,16 +32,22 @@ You can then execute the following flows: `yarn script:flow` runs all flows in sequence. +As we are still in the progress of documenting everything, +the above scripts are the best way to learn about how everything works. ## Demonstration -A more extensive example of a real life use case has been implemented as described in [./demo/README.md](./demo/README.md). - +Instead of running `yarn start`, you can run `yarn start:demo` to start the server with an alternative configuration. +With this configuration you can run the `script:demo`, +which runs with experimental contracts. ## Implemented features -The packages in this project currently only support a fixed UMA AS per CSS RS, and contain only the trivial [AllAuthorizer](packages/uma/src/models/AllAuthorizer.ts) that allows all access. More useful features are coming soon ... - +The packages in this project currently only support a fixed UMA AS per CSS RS. +Authorization can be done with a simple, unverified, WebID embedded in the ticket +using the [WebIdAuthorizer](packages/uma/src/policies/authorizers/WebIdAuthorizer.ts) +or the [PolicyBasedAuthorizer](packages/uma/src/policies/authorizers/PolicyBasedAuthorizer.ts) +which supports simple ODRL policies. ### Usage control policy enforcement @@ -56,10 +60,10 @@ Used for creating a modular engine that calculates which access modes are grante For more information, you can check out its [own repository](https://github.com/woutslabbinck/ucp-enforcement) which has three engines that use [ODRL rules](https://www.w3.org/TR/odrl-model/). A test script is provided for a CRUD ODRL engine: `yarn script:ucp-enforcement`. -In the [script](./scripts/test-ucp-enforcement.ts) a read Usage Control Rule (in ODRL) is present together with N3 interpretation rules. +In the [script](./scripts/test-ucp-enforcement.ts) a read Usage Control Rule (in ODRL) is present together with N3 interpretation rules. Then a read request is performed using the engine, which results in a list of grants. This list is then printed to the console. ## Next steps -Have a look at the [milestones](https://github.com/SolidLabResearch/user-managed-access/milestones) we set for ourselves, and other [issues](https://github.com/SolidLabResearch/user-managed-access/issues) we would like to solve. +More advanced ODRL evaluation can be found in the `feat/ODRL-evaluator` branch. diff --git a/Requirements.md b/Requirements.md new file mode 100644 index 00000000..fdff2758 --- /dev/null +++ b/Requirements.md @@ -0,0 +1,119 @@ +# TODOs for end-to-end requirements: + +## Final sprint + +- [ ] load generic and instantiated policies in auth frontend +- [ ] update continuation screens in the shop frontend +- [ ] MAKE THE VIDEO + - [ ] show credential, policies -> buy item -> show instantiation that has been added for the user -> show auditing trail +- [ ] Write setup requirements +- [ ] Create new SolidLabResearch repository that links to the e2e/setup branch + +### To Fix By Demo + +- [ ] Add Policy Screen update +- [ ] Final fixes generic policy +- [ ] Change trust display on auditing screen + - [ ] Change contract to "Instantiated Policy" + - [ ] Instantiated Policy -> Trusted instead of verified, age keep verified +- [ ] Auth app -> My pod app + - [ ] My Data + - [ ] My Policies + - [ ] Relevant linking? +- [X] Login information on every App: + - [X] Green -> You are logged in\ + - [X] Red -> You are not logged in + - [X] Blue -> Auditer 3 is logged in +- [ ] Store login buttons: + - [ ] Remove its'me option + - [ ] Continue as Ruben -> Share WebID link (with profile avatar) (This is not a Login!) + + + + + +### HAS TO HAPPEN +- [X] VC and token validation on the auditing frontend + - [X] Represent this with green checkmarks in the frontend +- [ ] Check policy models + +### If there is time +- [ ] Check policy evaluation system + - [ ] Do time related policies work? + - [ ] Can we include wrong purposes that fail? + - [ ] Can we do a check on store registration +- [ ] Store decision to give purchase access or not in the audit entry? + +### If there is a lot of time +- [ ] Pod-based logging (not super necessary atm?) +- [ ] Can we model accesses by 2 different people? + +## Assignment minimum requirements +- [X] The system needs to facilitate the exchange of the data (date of birth). + - [X] A date of birth must be available at some location in the dataspace +- [X] The system needs to provide the store with the trust that the data is correct. + - [X] The stored DOB must be a verifiable credential + - [X] The stored credential must be verifiable on the store backend +- [X] The system needs to provide the person with the trust that their data will only be used for age checking. + - [X] The policy system must be able to handle a purpose +- [ ] The system allows the person to specify in advance the generic policy that “all Belgian stores are allowed to read my date of birth”. + - [X] The system needs to be able to store a generic policy + - [X] An interface needs to be available to store this policy + - [ ] The policy must be modeled in an appropriate way +- [X] The system automatically instantiates the above generic policy into the concrete case that “MyBelgianWineStore is allowed to use my date of birth from 2024-03-01 to 2024-03-15 for the purpose of age verification for purchases” + - [ ] MOCKED -> double check though +- [X] The system allows the above interaction to take place without the person having to click on any dialogs. + - [X] The interaction is automatic after a WebID button is clicked to show what is happening. +- [ ] The system allows the store to prove that they were allowed to perform the age verification. + - [X] A backend storage must be in place for the store + - [X] The store website must forward data storage and checks to the backend +- [X] The system allows the person to check that their data was used correctly. + - [X] An auditing routine must be built in the store backend + - [X] An auditing routine must be built as a frontend interface +- [ ] The Government VC Service + - [X] Must be able to create a VC + - [X] VC must be transfered to demo pod storage -> Not required for Demo because of fixed keypair seed + - [ ] VCs can be validated on the backend of the store +- [ ] The Auditing use-case + - [X] The store backend provides the option to retrieve all required data to audit + - [ ] This can be represented in an auditing browser app that shows colors when verified (token + VC) + + +Small note with using the UMA server token signature as the contract signature. +We can only trace this back to the UMA Server, and cannot reliably check the connection between the WebID and the UMA Server + +Another idea: preemptive auditing: +- The store has to advertise who is auditing them +- The contract has to be signed both ways +- upon agreement, the data is sent to the store AND to the auditing service. +- on auditing, the service can check if the store is withholding information + + + +## Demonstrator requirements +- [ ] Protocol message modelling + - [ ] claim request messages + - [ ] claim provision messages +- [ ] Logging system (no hard requirement) + - [X] Create logging interface + - [ ] Log Instantiated Policies + - [ ] Log Access Grants + - [ ] Log Operations +- [ ] Authorization system + - [ ] include logging endpoint + - [ ] include authorization endpoint + - [ ] include policy management endpoint +- [X] Mock Policy instantiation + - [ ] Write out policy model that works for demo + - [X] ??? Discover existing policies to instantly grant some access + - [ ] Link generic - instantiated - grant - operation +- [x] Negotiation implementations + - [X] Return instantiated policy requirements from ticket resolving function to create a signed instantiated policy to return +- [ ] Signatures + - [ ] Create a VC form an instantiated policy - I use the return JWT as a free signature + - [ ] Create verification endpoint for issued VCs +- [ ] Government mockup + - [ ] Create verification endpoint for issued VCs (can be mocked) +- [ ] Client + - [ ] Make some mock-up of how storage could be handled in a way that allows for auditing + - [ ] Recurring requests make use of the same grant? \ No newline at end of file diff --git a/demo/README.md b/demo/README.md deleted file mode 100644 index 48f916d4..00000000 --- a/demo/README.md +++ /dev/null @@ -1,36 +0,0 @@ - -# Demonstration - -Using the UMA server implemented in this repository, we set up an extensive demonstration of a real life use case: age verification for online shops selling age-restricted goods, such as alcoholic beverages. - -To experiment with the demo, first build the necessary extra code with `build:demo`, then start the demo by running `start:demo`. This starts the CSS and UMA servers with the right configurations, and spins up two websites: an online shop on `http://localhost:5001`, and a policy manager on `http://localhost:5002`. - -The context "story" of the demonstration is the following. This "story" can be either run through via the graphical interfaces of the websites, or by running the script `yarn script:demo`. - -- Ruben V., a.k.a. ``, has some private data in ``. Of course, he does not want everyone to be able to see all of his private data when they need just one aspect of it. Therefore, Ruben has installed two **Views** on his data, based on SPARQL filters from a public **Catalog**. (When and how this is done is out-of-scope for now.) - -- Discovery of views is currently a very crude mechanism based on a public index in the WebID document. (A cleaner mechanism using the UMA server as central hub is underway.) Using this discovery mechanism, we can find the following views on Ruben's private data: - - 1. `` filters out his birth date, according to the `` filter; - 2. `` derives his age, according to the `` filter. - -- Access to Ruben's data is based on policies, which he manages through his Authz Companion app, and which are stored in ``. (This is, of course, not publicly known.) To request access to Ruben's data, an agent will need to negotiate with Ruben's UMA Authorization Server, which his WebID document identifies as ``. Via the Well-Known endpoint ``, we can discover the Token Endpoint ``. - -- Having discovered both the location of the UMA server and of the desired data, an agent can request the former for access to the latter. We get different results depending on the situation: - - - Without a policy allowing the access, the access is denied. - - However, the UMA server enables multiple flows in which such a policy can be added, for example by notifying the resource owner. (This is out-of-scope for this demo.) Having been notified in some way of the access request, Ruben could go to his Authz Companion app, and add a policy allowing the requested access.` - - - If a policy has been set (and perhaps the agent has been notified in some way to retry the access request), the UMA server will request the following claims from the agent, based on that policy: `http://www.w3.org/ns/odrl/2/purpose` and `urn:solidlab:uma:claims:types:webid`. - - - When the agent has gathered the necessary claims (the manner in which is out-of-scope for this demo), it can send them to the UMA server as a JWT: - - ``` - { - "http://www.w3.org/ns/odrl/2/purpose": "urn:solidlab:uma:claims:purpose:age-verification", - "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/demo/public/vendor" - } - ``` - -- Only when a policy is in place and the agent provides the UMA server with the relevant claims, an access token is produced, with which the agent can access the desired data at the Resource Server. diff --git a/demo/data/ruben/medical/smartwatch.ttl b/demo/data/ruben/medical/smartwatch.ttl new file mode 100644 index 00000000..7d146435 --- /dev/null +++ b/demo/data/ruben/medical/smartwatch.ttl @@ -0,0 +1 @@ + . \ No newline at end of file diff --git a/demo/flow-test.ts b/demo/flow-test.ts new file mode 100644 index 00000000..db9f999f --- /dev/null +++ b/demo/flow-test.ts @@ -0,0 +1,323 @@ +/* eslint-disable max-len */ + +import { fetch } from 'cross-fetch'; +import { Parser, Writer, Store } from 'n3'; +import { randomUUID } from 'crypto'; +import chalk from 'chalk' + +// import * as jsonld from 'jsonld'; + +// import vc from '@digitalcredentials/vc'; + +// // Required to set up a suite instance with private key +// import {Ed25519VerificationKey2020} from +// '@digitalcredentials/ed25519-verification-key-2020'; +// import {Ed25519Signature2020} from '@digitalcredentials/ed25519-signature-2020'; + + + +const parser = new Parser(); +const writer = new Writer(); + +const terms = { + solid: { + umaServer: 'http://www.w3.org/ns/solid/terms#umaServer', + viewIndex: 'http://www.w3.org/ns/solid/terms#viewIndex', + entry: 'http://www.w3.org/ns/solid/terms#entry', + filter: 'http://www.w3.org/ns/solid/terms#filter', + location: 'http://www.w3.org/ns/solid/terms#location', + }, + filters: { + bday: 'http://localhost:3000/catalog/public/filters/bday', + age: 'http://localhost:3000/catalog/public/filters/age', + }, + views: { + bday: 'http://localhost:3000/ruben/private/derived/bday', + age: 'http://localhost:3000/ruben/private/derived/age', + }, + resources: { + collectionSource: 'http://localhost:3000/ruben/medical/', + smartwatch: 'http://localhost:3000/ruben/medical/smartwatch.ttl', + heartrate: 'http://localhost:3000/ruben/medical/heartRate.ttl', + }, + agents: { + ruben: 'http://localhost:3000/ruben/profile/card#me', + alice: 'http://localhost:3000/alice/profile/card#me', + vendor: 'http://localhost:3000/demo/public/vendor', + present: 'http://localhost:3000/demo/public/bday-app', + }, + scopes: { + read: 'urn:example:css:modes:read', + } +} + +const policyContainer = 'http://localhost:3000/ruben/settings/policies/'; + +async function main() { + + log('') + log('=================== UMA prototype flow ======================') + + let webIdData; + try { + webIdData = new Store(parser.parse(await (await fetch(terms.agents.ruben)).text())); + } catch (e) { + 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(); + const tokenEndpoint = umaConfig.token_endpoint; + + log("This flow defines the retrieval by a doctor of a patient resource.") + log( +`Doctor WebID: ${terms.agents.alice} +Patient WebID: ${terms.agents.ruben} +Target Resource: ${terms.resources.smartwatch}`) + +/************************* + * Setting up the policy * + *************************/ + + 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 = + `@prefix dcterms: . + @prefix eu-gdpr: . + @prefix oac: . + @prefix odrl: . + @prefix xsd: . + + @prefix ex: . + + a odrl:Request ; + odrl:uid ex:HCPX-request ; + odrl:profile oac: ; + dcterms:description "HCP X requests to read Alice's health data for bariatric care."; + odrl:permission . + + a odrl:Permission ; + odrl:action odrl:read ; + odrl:target ; + odrl:assigner <${terms.agents.ruben}> ; + odrl:assignee <${terms.agents.alice}> ; + odrl:constraint , + . + + a odrl:AssetCollection; + odrl:source <${terms.resources.collectionSource}> . + + 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 ; + odrl:leftOperand oac:LegalBasis ; + odrl:operator odrl:eq ; + odrl:rightOperand eu-gdpr:A9-2-a .` + + await addPolicy(healthcare_patient_policy, policyContainer) + + log(`The policy assigns read permissions for the personal doctor ${terms.agents.alice} of the patient for the smartwatch resource on the condition + of the purpose of the request being "http://example.org/bariatric-care" and the legal basis being "https://w3id.org/dpv/legal/eu/gdpr#A9-2-a".`) + + +/********************** + * Reading a resource * + **********************/ + log(chalk.bold("The doctor now tries to access the private smartwatch resource.")) + + + const smartWatchAccessRequestNoClaimsODRL = { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Request", + profile: { "@id": "https://w3id.org/oac#" }, + uid: `http://example.org/HCPX-request/${randomUUID()}`, + description: "HCP X requests to read Alice's health data for bariatric care.", + permission: [ { + "@type": "Permission", + "uid": `http://example.org/HCPX-request-permission/${randomUUID()}`, + assigner: terms.agents.ruben, + assignee: terms.agents.alice, + action: { "@id": "https://w3id.org/oac#read" }, + target: terms.resources.smartwatch, + } ], + grant_type: "urn:ietf:params:oauth:grant-type:uma-ticket", + } + + const response = await executeReadWithClaims(terms.resources.smartwatch, smartWatchAccessRequestNoClaimsODRL, { tokenEndpoint }) + + if (response.succeed) { + throw new Error(`Resource request for ${terms.resources.smartwatch} should not have succeeded without claims: ${response}`) + } + + log(`Based on the policy set above, the Authorization Server requests the following claims from the doctor:`); + response.required_claims.claim_token_format[0].forEach((format: string) => log(` - ${format}`)) + log(`accompanied by an updated ticket: ${response.ticket}.`) + + // JWT (HS256; secret: "ceci n'est pas un secret") + // { + // "http://www.w3.org/ns/odrl/2/purpose": "http://example.org/bariatric-care", + // "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/alice/profile/card#me", + // "https://w3id.org/oac#LegalBasis": "https://w3id.org/dpv/legal/eu/gdpr#A9-2-a" + // } + const claim_token = "eyJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vd3d3LnczLm9yZy9ucy9vZHJsLzIvcHVycG9zZSI6Imh0dHA6Ly9leGFtcGxlLm9yZy9iYXJpYXRyaWMtY2FyZSIsInVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnR5cGVzOndlYmlkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL2FsaWNlL3Byb2ZpbGUvY2FyZCNtZSIsImh0dHBzOi8vdzNpZC5vcmcvb2FjI0xlZ2FsQmFzaXMiOiJodHRwczovL3czaWQub3JnL2Rwdi9sZWdhbC9ldS9nZHByI0E5LTItYSJ9.nT55jaXNDsHgAo_zcRMsbJqcNj4FVdW_-xjcwNam-1M" + + const claims: any = { + "http://www.w3.org/ns/odrl/2/purpose": "http://example.org/bariatric-care", + "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/alice/profile/card#me", + "https://w3id.org/oac#LegalBasis": "https://w3id.org/dpv/legal/eu/gdpr#A9-2-a" + } + + log(`The doctor's client now gathers the necessary claims (how is out-of-scope for this demo)`, claims) + + log(`and bundles them as an UMA-compliant JWT.`, { + claim_token: claim_token, + claim_token_format: "urn:solidlab:uma:claims:formats:jwt" + }) + + const smartWatchAccessRequestODRL = { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Request", + profile: { "@id": "https://w3id.org/oac#" }, + uid: `http://example.org/HCPX-request/${randomUUID()}`, + description: "HCP X requests to read Alice's health data for bariatric care.", + permission: [ { + "@type": "Permission", + "@id": `http://example.org/HCPX-request-permission/${randomUUID()}`, + target: terms.resources.smartwatch, + action: { "@id": "https://w3id.org/oac#read" }, + assigner: terms.agents.ruben, + assignee: terms.agents.alice, + constraint: [ + { + "@type": "Constraint", + "@id": `http://example.org/HCPX-request-permission-purpose/${randomUUID()}`, + leftOperand: "purpose", + operator: "eq", + rightOperand: { "@id": "http://example.org/bariatric-care" }, + }, { + "@type": "Constraint", + "@id": `http://example.org/HCPX-request-permission-purpose/${randomUUID()}`, + leftOperand: { "@id": "https://w3id.org/oac#LegalBasis" }, + operator: "eq", + rightOperand: {"@id": "https://w3id.org/dpv/legal/eu/gdpr#A9-2-a" }, + } + ], + } ], + // claims: [{ + claim_token: claim_token, + claim_token_format: "urn:solidlab:uma:claims:formats:jwt", + // }], + // UMA specific fields + grant_type: "urn:ietf:params:oauth:grant-type:uma-ticket", + // ticket: response.ticket, + } + + const response2 = await executeReadWithClaims(terms.resources.smartwatch, smartWatchAccessRequestODRL, { tokenEndpoint }) + + 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.`, + JSON.stringify(access_token.permissions, null, 2)); + + 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, { + headers: { 'Authorization': `${response2.token_type} ${response2.access_token}` } + }); + + log(`Now the doctor can retrieve the resource:`, await accessWithTokenResponse.text()); + + if (accessWithTokenResponse.status !== 200) { log(`Access with token failed...`); throw 0; } + +} + +main(); + + +/* Helper functions */ + +function parseJwt (token:string) { + return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); +} + +function log(msg: string, obj?: any) { + console.log(''); + console.log(msg); + if (obj) { + console.log('\n'); + console.log(obj); + } +} + + +async function executeReadWithClaims(target: string, request: any, options: { tokenEndpoint: string }) { + + const res = await fetch(target, { + method: "GET", + headers: { "content-type": "application/json" }, + }); + + const umaHeader = await res.headers.get('WWW-Authenticate') + + log(`Resource request to ${target} results in ${umaHeader}`) + + let ticket = umaHeader?.split('ticket=')[1].replace(/"/g, '') + + // setting ticket from resource request + request.ticket = ticket; + + log(`To the discovered AS, we now send a request for read permission to the target resource`, request) + + const response = await fetch(options.tokenEndpoint, { + method: "POST", + 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(); + if (responseJSON.required_claims) { + responseJSON.status = false; + return responseJSON + } else { + responseJSON.status = true; + return responseJSON + } +} + +async function executeWriteWithClaims(target: string, claims: any) { + +} + +async function addPolicy(policy: string, location: string) { + + + const medicalPolicyCreationResponse = await fetch(location, { + method: 'POST', + headers: { 'content-type': 'text/turtle' }, + body: policy, + }); + + log("The following policy is set for the AS:") + log("----------------------------------------------------") + log(policy) + log("----------------------------------------------------") + + if (medicalPolicyCreationResponse.status !== 201) { log('Adding a policy did not succeed...'); throw 0; } + +} diff --git a/demo/flow.ts b/demo/flow.ts index 41d06353..d3cb0709 100644 --- a/demo/flow.ts +++ b/demo/flow.ts @@ -2,7 +2,19 @@ import { fetch } from 'cross-fetch'; import { Parser, Writer, Store } from 'n3'; -import { demoPolicy } from "./policyCreation"; +import { randomUUID } from 'crypto'; +import chalk from 'chalk' + +import * as jsonld from 'jsonld'; + +import vc from '@digitalcredentials/vc'; + +// Required to set up a suite instance with private key +import {Ed25519VerificationKey2020} from + '@digitalcredentials/ed25519-verification-key-2020'; +import {Ed25519Signature2020} from '@digitalcredentials/ed25519-signature-2020'; + + const parser = new Parser(); const writer = new Writer(); @@ -23,8 +35,12 @@ const terms = { bday: 'http://localhost:3000/ruben/private/derived/bday', age: 'http://localhost:3000/ruben/private/derived/age', }, + resources: { + smartwatch: 'http://localhost:3000/ruben/medical/smartwatch.ttl' + }, agents: { ruben: 'http://localhost:3000/ruben/profile/card#me', + alice: 'http://localhost:3000/alice/profile/card#me', vendor: 'http://localhost:3000/demo/public/vendor', present: 'http://localhost:3000/demo/public/bday-app', }, @@ -33,125 +49,226 @@ const terms = { } } -async function main() { - - log(`Alright, so, for the demo ...`); - - log(`Ruben V., a.k.a. <${terms.agents.ruben}>, has some private data in .`); +const policyContainer = 'http://localhost:3000/ruben/settings/policies/'; - log(`Of course, he does not want everyone to be able to see all of his private data when they need just one aspect of it. Therefore, Ruben has installed two Views on his data, based on SPARQL filters from a public Catalog. (When and how this is done is out-of-scope for now.)`); +async function main() { const webIdData = new Store(parser.parse(await (await fetch(terms.agents.ruben)).text())); - const viewIndex = webIdData.getObjects(terms.agents.ruben, terms.solid.viewIndex, null)[0].value; - const views = Object.fromEntries(webIdData.getObjects(viewIndex, terms.solid.entry, null).map(entry => { - const filter = webIdData.getObjects(entry, terms.solid.filter, null)[0].value; - const location = webIdData.getObjects(entry, terms.solid.location, null)[0].value; - return [filter, location]; - })); - - log(`Discovery of views is currently a very crude mechanism based on a public index in the WebID document. (A cleaner mechanism using the UMA server as central hub is being devised.) Using the discovery mechanism, we find the following views on Ruben's private data.`) - - log(`(1) <${views[terms.filters.bday]}> filters out his birth date, according to the <${terms.filters.bday}> filter`); - log(`(2) <${views[terms.filters.age]}> derives his age, according to the <${terms.filters.bday}> filter`); - - const policyContainer = 'http://localhost:3000/ruben/settings/policies/'; - - log(`Access to Ruben's data is based on policies he manages through his Authz Companion app, and which are stored in <${policyContainer}>. (This is, of course, not publicly known.)`); - + 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(); const tokenEndpoint = umaConfig.token_endpoint; - log(`To request access to Ruben's data, an agent will need to negotiate with Ruben's Authorization Server, which his WebID document identifies as <${umaServer}>.`); - log(`Via the Well-Known endpoint <${configUrl.href}>, we can discover the Token Endpoint <${tokenEndpoint}>.`); - log(`Now, having discovered both the location of the UMA server and of the desired data, an agent can request the former for access to the latter.`); + log('') + log('=================== UMA prototype flow ======================') - const accessRequest = { - permissions: [{ - resource_id: terms.views.age, - resource_scopes: [ terms.scopes.read ], - }] - }; + log("This flow defines the retrieval by a doctor of a patient resource.") + log( +`Doctor WebID: ${terms.agents.alice} +Patient WebID: ${terms.agents.ruben} +Target Resource: ${terms.resources.smartwatch}`) - const accessDeniedResponse = await fetch(tokenEndpoint, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(accessRequest), + 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 = + `PREFIX dcterms: +PREFIX eu-gdpr: +PREFIX oac: +PREFIX odrl: +PREFIX xsd: + +PREFIX ex: + + a odrl:Request ; + odrl:uid ex:HCPX-request ; + odrl:profile oac: ; + dcterms:description "HCP X requests to read Alice's health data for bariatric care."; + 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 , + . + + 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 ; + odrl:leftOperand oac:LegalBasis ; + odrl:operator odrl:eq ; + odrl:rightOperand eu-gdpr:A9-2-a .` + + const medicalPolicyCreationResponse = await fetch(policyContainer, { + method: 'POST', + headers: { 'content-type': 'text/turtle' }, + body: healthcare_patient_policy, }); - if (accessDeniedResponse.status !== 403) { log('Access request succeeded without policy...'); throw 0; } + log("The following policy is set for the AS:") + log("----------------------------------------------------") + log(healthcare_patient_policy) + log("----------------------------------------------------") - log(`Without a policy allowing the access, the access is denied.`); - log(`However, the UMA server enables multiple flows in which such a policy can be added, for example by notifying the resource owner. (This is out-of-scope for this demo.)`); - - log(`...`); + if (medicalPolicyCreationResponse.status !== 201) { log('Adding a policy did not succeed...'); throw 0; } - log(`Having been notified in some way of the access request, Ruben could go to his Authz Companion app, and add a policy allowing the requested access.`); + log(`The policy assigns read permissions for the personal doctor ${terms.agents.alice} of the patient for the smartwatch resource +on the condition of the purpose of the request being "http://example.org/bariatric-care" and the legal basis being "https://w3id.org/dpv/legal/eu/gdpr#A9-2-a".`) - const startDate = new Date(); - const endDate = new Date(startDate.valueOf() + 14 * 24 * 60 * 60 * 1000); - const purpose = 'urn:solidlab:uma:claims:purpose:age-verification' - const policy = demoPolicy(terms.views.age, terms.agents.vendor, { startDate, endDate, purpose }) + log(chalk.bold("The doctor now tries to access the private smartwatch resource.")) - // create container if it does not exist yet - await initContainer(policyContainer) - const policyCreationResponse = await fetch(policyContainer, { - method: 'POST', - headers: { 'content-type': 'text/turtle' }, - body: writer.quadsToString(policy.representation.getQuads(null, null, null, null)) + const res = await fetch(terms.resources.smartwatch, { + method: "GET", + headers: { "content-type": "application/json" }, }); - - if (policyCreationResponse.status !== 201) { log('Adding a policy did not succeed...'); throw 0; } - - log(`Now that the policy has been set, and the agent has possibly been notified in some way, the agent can try the access request again.`); - const needInfoResponse = await fetch(tokenEndpoint, { + 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: +${umaHeader}`) + + let ticket = umaHeader?.split('ticket=')[1].replace(/"/g, '') + + // todo: this should be a resource request. Maybe something broke but I couldn't get a ticket via a direct resource request. + + const smartWatchAccessRequestNoClaimsODRL = { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Request", + profile: { "@id": "https://w3id.org/oac#" }, + uid: `http://example.org/HCPX-request/${randomUUID()}`, + description: "HCP X requests to read Alice's health data for bariatric care.", + permission: [ { + "@type": "Permission", + "uid": `http://example.org/HCPX-request-permission/${randomUUID()}`, + assigner: terms.agents.ruben, + assignee: terms.agents.alice, + action: { "@id": "https://w3id.org/oac#read" }, + target: terms.resources.smartwatch, + } ], + grant_type: "urn:ietf:params:oauth:grant-type:uma-ticket", + ticket, + } + + log(`To the discovered AS, we now send a request for read permission to the target resource`, smartWatchAccessRequestNoClaimsODRL) + + const doctor_needInfoResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify(accessRequest), + body: JSON.stringify(smartWatchAccessRequestNoClaimsODRL), }); - if (needInfoResponse.status !== 403) { log('Access request succeeded without claims...'); throw 0; } + if (doctor_needInfoResponse.status !== 403) { log('Access request succeeded without claims...', await doctor_needInfoResponse.text()); throw 0; } - const { ticket, required_claims } = await needInfoResponse.json(); - - log(`Based on the policy, the UMA server requests the following claims from the agent:`); - required_claims.claim_token_format[0].forEach((format: string) => log(` - ${format}`)) + 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}.`) // JWT (HS256; secret: "ceci n'est pas un secret") // { - // "http://www.w3.org/ns/odrl/2/purpose": "urn:solidlab:uma:claims:purpose:age-verification", - // "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/demo/public/vendor" + // "http://www.w3.org/ns/odrl/2/purpose": "http://example.org/bariatric-care", + // "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/alice/profile/card#me", + // "https://w3id.org/oac#LegalBasis": "https://w3id.org/dpv/legal/eu/gdpr#A9-2-a" // } - const claim_token = "eyJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vd3d3LnczLm9yZy9ucy9vZHJsLzIvcHVycG9zZSI6InVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnB1cnBvc2U6YWdlLXZlcmlmaWNhdGlvbiIsInVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnR5cGVzOndlYmlkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL2RlbW8vcHVibGljL3ZlbmRvciJ9.Px7G3zl1ZpTy1lk7ziRMvNv12Enb0uhup9kiVI6Ot3s" + const claim_token = "eyJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vd3d3LnczLm9yZy9ucy9vZHJsLzIvcHVycG9zZSI6Imh0dHA6Ly9leGFtcGxlLm9yZy9iYXJpYXRyaWMtY2FyZSIsInVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnR5cGVzOndlYmlkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL2FsaWNlL3Byb2ZpbGUvY2FyZCNtZSIsImh0dHBzOi8vdzNpZC5vcmcvb2FjI0xlZ2FsQmFzaXMiOiJodHRwczovL3czaWQub3JnL2Rwdi9sZWdhbC9ldS9nZHByI0E5LTItYSJ9.nT55jaXNDsHgAo_zcRMsbJqcNj4FVdW_-xjcwNam-1M" - log(`The agent gathers the necessary claims (the manner in which is out-of-scope for this demo), and sends them to the UMA server as a JWT.`) + const claims: any = { + "http://www.w3.org/ns/odrl/2/purpose": "http://example.org/bariatric-care", + "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/alice/profile/card#me", + "https://w3id.org/oac#LegalBasis": "https://w3id.org/dpv/legal/eu/gdpr#A9-2-a" + } + + log(`The doctor's client now gathers the necessary claims (how is out-of-scope for this demo)`, claims) + + log(`and bundles them as an UMA-compliant JWT.`, { + claim_token: claim_token, + claim_token_format: "urn:solidlab:uma:claims:formats:jwt" + }) + + const smartWatchAccessRequestODRL = { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Request", + profile: { "@id": "https://w3id.org/oac#" }, + uid: `http://example.org/HCPX-request/${randomUUID()}`, + description: "HCP X requests to read Alice's health data for bariatric care.", + permission: [ { + "@type": "Permission", + "@id": `http://example.org/HCPX-request-permission/${randomUUID()}`, + target: terms.resources.smartwatch, + action: { "@id": "https://w3id.org/oac#read" }, + assigner: terms.agents.ruben, + assignee: terms.agents.alice, + constraint: [ + { + "@type": "Constraint", + "@id": `http://example.org/HCPX-request-permission-purpose/${randomUUID()}`, + leftOperand: "purpose", + operator: "eq", + rightOperand: { "@id": "http://example.org/bariatric-care" }, + }, { + "@type": "Constraint", + "@id": `http://example.org/HCPX-request-permission-purpose/${randomUUID()}`, + leftOperand: { "@id": "https://w3id.org/oac#LegalBasis" }, + operator: "eq", + rightOperand: {"@id": "https://w3id.org/dpv/legal/eu/gdpr#A9-2-a" }, + } + ], + } ], + // claims: [{ + claim_token: claim_token, + claim_token_format: "urn:solidlab:uma:claims:formats:jwt", + // }], + // UMA specific fields + grant_type: "urn:ietf:params:oauth:grant-type:uma-ticket", + ticket, + } + + 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.`)) const accessGrantedResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ - ...accessRequest, - ticket, - claim_token_format: 'urn:solidlab:uma:claims:formats:jwt', - claim_token, - }) + body: JSON.stringify(smartWatchAccessRequestODRL) }); - if (accessGrantedResponse.status !== 200) { log('Access request failed despite policy...'); throw 0; } + if (accessGrantedResponse.status !== 200) { + log('Access request failed despite policy...', JSON.stringify(await accessGrantedResponse.text(), null, 2)); throw 0; + } - log(`The UMA server checks the claims with the relevant policy, and returns the agent an access token with the requested permissions.`); - const tokenParams = await accessGrantedResponse.json(); - const accessWithTokenResponse = await fetch(terms.views.age, { + 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.`, + JSON.stringify(access_token.permissions, null, 2)); + + 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, { headers: { 'Authorization': `${tokenParams.token_type} ${tokenParams.access_token}` } }); - if (accessWithTokenResponse.status !== 200) { log('Access with token failed...'); throw 0; } + log(`Now the doctor can retrieve the resource:`, await accessWithTokenResponse.text()); - log(`The agent can then use this access token at the Resource Server to perform the desired action.`); + if (accessWithTokenResponse.status !== 200) { log(`Access with token failed...`); throw 0; } + } main(); diff --git a/demo/package.json b/demo/package.json deleted file mode 100644 index 9b56caa4..00000000 --- a/demo/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@solidlab/uma-demos", - "version": "0.1.0", - "private": true, - "devDependencies": { - "@emotion/react": "^11.11.3", - "@emotion/styled": "^11.11.0", - "@mui/material": "^5.15.14", - "@types/react-dom": "^18.2.18", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.10.0", - "react-scripts": "5.0.1", - "serve": "^14.2.1" - }, - "workspaces": [ - "sites/*" - ], - "scripts": { - "build:demo": "yarn workspaces foreach --include 'sites/*' -A -pi run build", - "start:demo": "yarn workspaces foreach --include 'sites/*' -A -pi run start" - } -} diff --git a/demo/policyCreation.ts b/demo/policyCreation.ts deleted file mode 100644 index 0bf8d21c..00000000 --- a/demo/policyCreation.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SimplePolicy, UCPPolicy, basicPolicy } from '@solidlab/ucp' - -/** - * Create demo ODRL policy: - * - * Read access for requestingparty to target under constraints (temporal + purpose) - * @param targetIRI - an IRI representing the target -> the resource - * @param requestingPartyIRI - an IRI representing the entity requesting access - * @param constraints - the temporal and purpuse constraints on the usage of the data - */ -export function demoPolicy( - targetIRI: string, - requestingPartyIRI: string, - constraints?: { - startDate?: Date, - endDate?: Date, - purpose?: string - } -): SimplePolicy { - const constraintList: any[] = []; - - if (constraints?.startDate) constraintList.push({ - type: 'temporal', - operator: 'http://www.w3.org/ns/odrl/2/gt', - value: constraints?.startDate, - }); - - if (constraints?.endDate) constraintList.push({ - type: 'temporal', - operator: 'http://www.w3.org/ns/odrl/2/lt', - value: constraints?.endDate, - }); - - if (constraints?.purpose) constraintList.push({ - type: 'purpose', - operator: 'http://www.w3.org/ns/odrl/2/eq', - value: constraints?.purpose, - }); - - const policy: UCPPolicy = { - rules: [{ - resource: targetIRI, - action: "http://www.w3.org/ns/odrl/2/read", // ODRL action - requestingParty: requestingPartyIRI, - // owner: "https://pod.woutslabbinck.com/profile/card#me", // might error - constraints: constraintList - }] - } - - const policyObject = basicPolicy(policy); - - return policyObject -} diff --git a/demo/problem/context.ttl b/demo/problem/context.ttl deleted file mode 100644 index c7657d4d..00000000 --- a/demo/problem/context.ttl +++ /dev/null @@ -1,4 +0,0 @@ - - ; - ; - . diff --git a/demo/problem/policy1.ttl b/demo/problem/policy1.ttl deleted file mode 100644 index 665fde13..00000000 --- a/demo/problem/policy1.ttl +++ /dev/null @@ -1,19 +0,0 @@ -@prefix odrl: . -@prefix xsd: . - - a odrl:Agreement; - odrl:permission . - a odrl:Permission; - odrl:action odrl:read; - odrl:target ; - odrl:assignee ; - odrl:constraint , , . - odrl:leftOperand odrl:dateTime; - odrl:operator odrl:gt; - odrl:rightOperand "2024-03-15T14:52:09.755Z"^^xsd:dateTime. - odrl:leftOperand odrl:dateTime; - odrl:operator odrl:lt; - odrl:rightOperand "2024-03-29T14:52:09.755Z"^^xsd:dateTime. - odrl:leftOperand odrl:purpose; - odrl:operator odrl:eq; - odrl:rightOperand "age-verification". diff --git a/demo/problem/policy2.ttl b/demo/problem/policy2.ttl deleted file mode 100644 index 19ff91ff..00000000 --- a/demo/problem/policy2.ttl +++ /dev/null @@ -1,19 +0,0 @@ -@prefix odrl: . -@prefix xsd: . - - a odrl:Agreement; - odrl:permission . - a odrl:Permission; - odrl:action odrl:read; - odrl:target ; - odrl:assignee ; - odrl:constraint , , . - odrl:leftOperand odrl:dateTime; - odrl:operator odrl:gt; - odrl:rightOperand "2024-03-15T14:52:09.755Z"^^xsd:dateTime. - odrl:leftOperand odrl:dateTime; - odrl:operator odrl:lt; - odrl:rightOperand "2024-03-29T14:52:09.755Z"^^xsd:dateTime. - odrl:leftOperand odrl:purpose; - odrl:operator odrl:eq; - odrl:rightOperand "age-verification". diff --git a/demo/problem/policy3.ttl b/demo/problem/policy3.ttl deleted file mode 100644 index 9e03bab2..00000000 --- a/demo/problem/policy3.ttl +++ /dev/null @@ -1,19 +0,0 @@ -@prefix odrl: . -@prefix xsd: . - - a odrl:Agreement; - odrl:permission . - a odrl:Permission; - odrl:action odrl:read; - odrl:target ; - odrl:assignee ; - odrl:constraint , , . - odrl:leftOperand odrl:dateTime; - odrl:operator odrl:gt; - odrl:rightOperand "2024-03-15T14:52:09.755Z"^^xsd:dateTime. - odrl:leftOperand odrl:dateTime; - odrl:operator odrl:lt; - odrl:rightOperand "2024-03-29T14:52:09.755Z"^^xsd:dateTime. - odrl:leftOperand odrl:purpose; - odrl:operator odrl:eq; - odrl:rightOperand "age-verification". diff --git a/demo/problem/rules_crud.n3 b/demo/problem/rules_crud.n3 deleted file mode 100644 index 2c240210..00000000 --- a/demo/problem/rules_crud.n3 +++ /dev/null @@ -1,163 +0,0 @@ -@prefix odrl: . -@prefix : . -@prefix acl: . -@prefix fno: . -@prefix log: . -@prefix string: . -@prefix list: . - -# Read ODRL rule -{ - ?permission a odrl:Permission; - odrl:action ?action ; - odrl:target ?targetResource ; - # odrl:assigner ?resourceOwner ; - odrl:assignee ?requestedParty. - - ?action list:in (odrl:use odrl:read) . # multiple options - - - # context of a request - ?context - # :resourceOwner ?resourceOwner; - :requestingParty ?requestedParty; - :target ?targetResource; - :requestPermission . - - :uuid5 log:uuid ?uuidStringdataUsagePolicyExecution. - ( "urn:uuid:" ?uuidStringdataUsagePolicyExecution) string:concatenation ?urnUuidStringdataUsagePolicyExecution. - ?dataUsagePolicyExecution log:uri ?urnUuidStringdataUsagePolicyExecution . - - # Constraint checking - # No odrl:constraints may be present - ?SCOPE log:notIncludes { ?permission odrl:constraint ?anything }. -} => -{ - ?dataUsagePolicyExecution a fno:Execution; - fno:executes ; - :accessModesAllowed . -}. - -# Append ODRL Rule -{ - ?permission a odrl:Permission; - odrl:action ?action ; - odrl:target ?targetResource ; - # odrl:assigner ?resourceOwner ; - odrl:assignee ?requestedParty. - - ?action list:in (odrl:use odrl:modify). # multiple options - - # context of a request - ?context - # :resourceOwner ?resourceOwner; - :requestingParty ?requestedParty; - :target ?targetResource; - :requestPermission . - - :uuid6 log:uuid ?uuidStringdataUsagePolicyExecution. - ( "urn:uuid:" ?uuidStringdataUsagePolicyExecution) string:concatenation ?urnUuidStringdataUsagePolicyExecution. - ?dataUsagePolicyExecution log:uri ?urnUuidStringdataUsagePolicyExecution . - - # Constraint checking - # No odrl:constraints may be present - ?SCOPE log:notIncludes { ?permission odrl:constraint ?anything }. -} => -{ - ?dataUsagePolicyExecution a fno:Execution; - fno:executes ; - :accessModesAllowed . -}. - -# Write ODRL Rule -{ - ?permission a odrl:Permission; - odrl:action ?action ; - odrl:target ?targetResource ; - # odrl:assigner ?resourceOwner ; - odrl:assignee ?requestedParty. - - ?action list:in (odrl:use odrl:modify). # multiple options - - # context of a request - ?context - # :resourceOwner ?resourceOwner; - :requestingParty ?requestedParty; - :target ?targetResource; - :requestPermission . - - :uuid6 log:uuid ?uuidStringdataUsagePolicyExecution. - ( "urn:uuid:" ?uuidStringdataUsagePolicyExecution) string:concatenation ?urnUuidStringdataUsagePolicyExecution. - ?dataUsagePolicyExecution log:uri ?urnUuidStringdataUsagePolicyExecution . - - # Constraint checking - # No odrl:constraints may be present - ?SCOPE log:notIncludes { ?permission odrl:constraint ?anything }. -} => -{ - ?dataUsagePolicyExecution a fno:Execution; - fno:executes ; - :accessModesAllowed . -}. - -# Create ODRL Rule -{ - ?permission a odrl:Permission; - odrl:action ?action ; - odrl:target ?targetResource ; - # odrl:assigner ?resourceOwner ; - odrl:assignee ?requestedParty . - - ?action list:in (odrl:use odrl:modify). # multiple options - - # context of a request - ?context - # :resourceOwner ?resourceOwner; - :requestingParty ?requestedParty; - :target ?targetResource; - :requestPermission . - - :uuid6 log:uuid ?uuidStringdataUsagePolicyExecution. - ( "urn:uuid:" ?uuidStringdataUsagePolicyExecution) string:concatenation ?urnUuidStringdataUsagePolicyExecution. - ?dataUsagePolicyExecution log:uri ?urnUuidStringdataUsagePolicyExecution . - - # Constraint checking - # No odrl:constraints may be present - ?SCOPE log:notIncludes { ?permission odrl:constraint ?anything }. -} => -{ - ?dataUsagePolicyExecution a fno:Execution; - fno:executes ; - :accessModesAllowed . -}. - -# Delete ODRL Rule -{ - ?permission a odrl:Permission; - odrl:action ?action ; - odrl:target ?targetResource ; - # odrl:assigner ?resourceOwner ; - odrl:assignee ?requestedParty. - - ?action list:in (odrl:use odrl:delete). # multiple options - - # context of a request - ?context - # :resourceOwner ?resourceOwner; - :requestingParty ?requestedParty; - :target ?targetResource; - :requestPermission . - - :uuid6 log:uuid ?uuidStringdataUsagePolicyExecution. - ( "urn:uuid:" ?uuidStringdataUsagePolicyExecution) string:concatenation ?urnUuidStringdataUsagePolicyExecution. - ?dataUsagePolicyExecution log:uri ?urnUuidStringdataUsagePolicyExecution . - - # Constraint checking - # No odrl:constraints may be present - ?SCOPE log:notIncludes { ?permission odrl:constraint ?anything }. -} => -{ - ?dataUsagePolicyExecution a fno:Execution; - fno:executes ; - :accessModesAllowed . -}. diff --git a/demo/problem/rules_purpose.n3 b/demo/problem/rules_purpose.n3 deleted file mode 100644 index d6e08fa2..00000000 --- a/demo/problem/rules_purpose.n3 +++ /dev/null @@ -1,79 +0,0 @@ - -@prefix xsd: . -@prefix odrl: . -@prefix : . -@prefix acl: . -@prefix fno: . -@prefix log: . -@prefix string: . -@prefix list: . -@prefix time: . -@prefix math: . - -# ... -{ :currentTime :is ?currentTime } <= { "" time:localTime ?currentTime }. - -# Read ODRL rule -{ - ?permission a odrl:Permission; - odrl:action ?action ; - odrl:target ?targetResource ; - # odrl:assigner ?resourceOwner ; - odrl:assignee ?requestedParty. - - ?action list:in (odrl:use odrl:read) . # multiple options - - # context of a request - ?context - # :resourceOwner ?resourceOwner; - :requestingParty ?requestedParty; - :target ?targetResource; - :requestPermission . - - :uuid5 log:uuid ?uuidStringdataUsagePolicyExecution. - ( "urn:uuid:" ?uuidStringdataUsagePolicyExecution) string:concatenation ?urnUuidStringdataUsagePolicyExecution. - ?dataUsagePolicyExecution log:uri ?urnUuidStringdataUsagePolicyExecution . - - # Constraint checking - - # number of constraints must be two (temporal needs lower and upper bound) - (?template {?permission odrl:constraint _:s} ?L) log:collectAllIn ?SCOPE. - ?L list:length 3 . - - :currentTime :is ?currentTime . - - # lower bound - ?permission odrl:constraint ?lowerBoundIRI . - ?lowerBoundIRI - odrl:leftOperand odrl:dateTime ; - odrl:operator odrl:gt ; - odrl:rightOperand ?lowerBound . - - # greater bound - ?permission odrl:constraint ?upperBoundIRI . - ?upperBoundIRI - odrl:leftOperand odrl:dateTime ; - odrl:operator odrl:lt ; - odrl:rightOperand ?upperBound . - - # ?lowerBound < ?currentTime < ?upperBound - ?currentTime math:greaterThan ?lowerBound . - ?currentTime math:lessThan ?upperBound . - - # purpose constraint - ?permission odrl:constraint ?purposeConstraint . - ?purposeConstraint - odrl:leftOperand odrl:purpose ; - odrl:operator odrl:eq ; - odrl:rightOperand ?purposeValue . - # Note: nothing is done with the purpose right now TODO: needs checking -} => -{ - ?dataUsagePolicyExecution a fno:Execution; - fno:executes ; - :accessModesAllowed ; - ?currentTime . - -}. - -# No ODRL rules for other access modes (`odrl:write` and `odrl:append` are deprecated) diff --git a/demo/problem/test.ts b/demo/problem/test.ts deleted file mode 100644 index 111a7280..00000000 --- a/demo/problem/test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { readFileSync } from "fs"; -import { EyeJsReasoner } from "koreografeye"; -import { Store, Parser, Writer } from "n3"; -import path from "path"; - -const parser = new Parser(); -const rules = new Array(); -const facts = new Store(); - -facts.addQuads(parser.parse(readFileSync(path.join(__dirname, 'context.ttl')).toString())) -facts.addQuads(parser.parse(readFileSync(path.join(__dirname, 'policy1.ttl')).toString())) -// facts.addQuads(parser.parse(readFileSync(path.join(__dirname, 'policy2.ttl')).toString())) -// facts.addQuads(parser.parse(readFileSync(path.join(__dirname, 'policy3.ttl')).toString())) - -rules.push(readFileSync(path.join(__dirname, 'rules_purpose.n3')).toString()); -rules.push(readFileSync(path.join(__dirname, 'rules_crud.n3')).toString()); - -(async () => { - - console.log('>>>>> BEFORE'); - const reasoner1 = new EyeJsReasoner(["--quiet", "--nope", "--pass"]); - await reasoner1.reason(facts, rules); - console.log('>>>>> BETWEEN'); - const reasoner2 = new EyeJsReasoner(["--quiet", "--nope", "--pass"]); - await reasoner2.reason(facts, rules); - console.log('>>>>> AFTER'); - -})(); diff --git a/demo/sites/authorizationsite/package.json b/demo/sites/authorizationsite/package.json deleted file mode 100644 index 1193845d..00000000 --- a/demo/sites/authorizationsite/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@solidlab/uma-demo-authz", - "version": "0.1.0", - "private": true, - "dependencies": { - "@comunica/query-sparql": "^2.6.9", - "@inrupt/solid-client-authn-browser": "^1.14.0", - "n3": "^1.17.3", - "uuid": "^9.0.1" - }, - "scripts": { - "start": "yarn run -T serve -s build -l 5001", - "build": "yarn run -T react-scripts build" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} diff --git a/demo/sites/authorizationsite/public/index.html b/demo/sites/authorizationsite/public/index.html deleted file mode 100644 index 1aaf9e4c..00000000 --- a/demo/sites/authorizationsite/public/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - Authorization Companion - - - -
- - diff --git a/demo/sites/authorizationsite/src/App.css b/demo/sites/authorizationsite/src/App.css deleted file mode 100644 index e247a5ff..00000000 --- a/demo/sites/authorizationsite/src/App.css +++ /dev/null @@ -1,39 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - diff --git a/demo/sites/authorizationsite/src/App.tsx b/demo/sites/authorizationsite/src/App.tsx deleted file mode 100644 index 236d3ee5..00000000 --- a/demo/sites/authorizationsite/src/App.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from "react"; -import { handleIncomingRedirect, getDefaultSession } from '@inrupt/solid-client-authn-browser'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { useEffect, useState} from 'react'; - -import './App.css'; - -import Home from './components/Home'; -import Navigate from './components/Navigate'; -import SolidAuth from './components/SolidAuth' - -const rubenWebID = 'http://localhost:3000/ruben/profile/card#me' - -export default function App() { - - // Ophalen van het Solid session object. - const session = getDefaultSession() - - // De loggedIn variabele houdt de login status bij, - // en update de pagina wanneer de status verandert. - const [loggedIn, setLoggedIn] = useState(session.info.isLoggedIn) - - // De checkingLogin variabele houdt bij of onze initiële - // check voor login informatie is afgerond. - const [checkingLogin, setCheckingLogin] = useState(true) - - // Deze functie voert uit bij het updaten van de component. - useEffect(() => { - // Forceer hernieuwen van de pagina bij het veranderen van de login status. - session.onLogin(() => setLoggedIn(true)) - session.onLogout(() => setLoggedIn(false)) - - // Deze functie gaat na of we teruggestuurd zijn - // naar de huidige pagina door de Solid login pagina. - handleIncomingRedirect({ restorePreviousSession: true }) - .then((info) => { - // Update de status van de component voor - // de login status en de login check status - // op basis van het resultaat van de functie. - // Voor meer informatie kan je de documentatie bekijken op - // https://docs.inrupt.com/developer-tools/api/javascript/solid-client-authn-browser/functions.html#handleincomingredirect - let status = info?.isLoggedIn || false - if (status !== loggedIn) setLoggedIn(status) - if (info) setCheckingLogin(false) - }) - .catch(console.error) - }) - - // return ( - //
- // { - // checkingLogin - // ? - //

Loading Session information ...

- // : ( - //
- // - // {loggedIn && - // - // - // - // } /> - // } /> - // - // - // } - //
- // ) - // } - //
- // ) - - return ( -
- - - - - } /> - - -
- ) -} \ No newline at end of file diff --git a/demo/sites/authorizationsite/src/components/DatePicker.tsx b/demo/sites/authorizationsite/src/components/DatePicker.tsx deleted file mode 100644 index e6171ae6..00000000 --- a/demo/sites/authorizationsite/src/components/DatePicker.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { useRef, useState } from 'react'; - -function getDateCompatString(date: Date) { - return date.toISOString().split('T')[0] -} - -const DatePicker = (props: any) => { - const [date, setDate] = useState(props.value) - const dateInputRef = useRef(null); - - const onChange = (e: any) => { - const date = new Date(e.target.value) - setDate(date); - props.onChange(date) - }; - - return ( -
- -
- ); -}; -export default DatePicker - - \ No newline at end of file diff --git a/demo/sites/authorizationsite/src/components/FormModal.tsx b/demo/sites/authorizationsite/src/components/FormModal.tsx deleted file mode 100644 index 3e7ae1a1..00000000 --- a/demo/sites/authorizationsite/src/components/FormModal.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useState }from 'react'; -import Backdrop from '@mui/material/Backdrop'; -import Box from '@mui/material/Box'; -import Modal from '@mui/material/Modal'; -import Fade from '@mui/material/Fade'; -import DatePicker from './DatePicker'; -import { PolicyFormData, terms } from '../util/PolicyManagement'; - -const style = { - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 600, - height: 600, - bgcolor: 'background.paper', - border: '2px solid #000', - boxShadow: 24, - p: 4, -}; - -const purposeValues: Map = new Map([ - ["age-verification", 'urn:solidlab:uma:claims:purpose:age-verification'], - ["some-random-purpose", 'urn:solidlab:uma:claims:purpose:some-random-purpose'] -]) - -const PolicyFormModal = (props: any) => { - const [open, setOpen] = useState(false); - - let now = new Date() - let end = new Date() - end.setDate(end.getDate() + 7) - - const [target, setTarget] = useState(terms.views.age); - const [assignee, setAssignee] = useState(terms.agents.vendor); - const [startDate, setStartDate] = useState(now); - const [endDate, setEndDate] = useState(end); - const [purpose, setPurpose] = useState('urn:solidlab:uma:claims:purpose:age-verification'); - const [description, setDescription] = useState('Age verification for food store'); - - const handleOpen = () => setOpen(true); - const handleClose = () => { - setOpen(false) - }; - - function commitPolicy(e: any) { - e.preventDefault(); - handleClose() - props.addPolicy({target, assignee, startDate, endDate, purpose, description} as PolicyFormData); - } - - return ( -
- - {/* */} - - - -

Add policy

-
- -
- -
- -
- -
- -
- - -
-
- -
-
-
-
-
- ); -} - -// targetIRI: string, -// requestingPartyIRI: string, -// constraints?: { -// startDate?: Date, -// endDate?: Date, -// purpose?: string -// } - - -export default PolicyFormModal; diff --git a/demo/sites/authorizationsite/src/components/Home.tsx b/demo/sites/authorizationsite/src/components/Home.tsx deleted file mode 100644 index 38349b7d..00000000 --- a/demo/sites/authorizationsite/src/components/Home.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect, useState } from "react"; -import { createAndSubmitPolicy, doPolicyFlowFromString, - readPolicy, readPolicyDirectory } from "../util/PolicyManagement"; -import PolicyFormModal from "./FormModal" -import { SimplePolicy } from "../util/policyCreation"; - -export default function Home() { - - const [policyList, setPolicyList] = useState([]) - const [selectedPolicy, setSelectedPolicy] = useState(null) - - useEffect(() => { - async function getPolicies() { - let policies = await readPolicyDirectory(); - setPolicyList(policies) - } - getPolicies() - }, []) - - async function addPolicyFromText(policyText: string) { - console.log('Adding the following policy:') - console.log(policyText) - await doPolicyFlowFromString(policyText) - const policyObject = await readPolicy(policyText) - if(policyObject) setPolicyList(policyList.concat(policyObject)) - } - - async function addPolicyFromFormdata(formdata: any) { - console.log('Adding the following policy:') - console.log(formdata) - const policyObject = await createAndSubmitPolicy(formdata) - if(policyObject) setPolicyList(policyList.concat(policyObject)) - } - - function renderPolicy(policy: SimplePolicy) { - return ( -
setSelectedPolicy(policy.policyIRI)}> -

id: {policy.policyIRI}

-

{policy.description}

-
- ) - } - - const selectedPolicyText = selectedPolicy - ? policyList.filter(p => p.policyIRI === selectedPolicy)[0]?.policyText || '' - : '' - - return ( -
-
-
-
- { - policyList.map(renderPolicy) - } -
- -
-
-