diff --git a/.gitignore b/.gitignore index e541974f..b3c11572 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ npm-debug.log* .yarn/build-state.yml .yarn/install-state.gz .yarn-integrity +.pnp/ .pnp.* .pnpm-debug.log* yarn-debug.log* @@ -44,7 +45,10 @@ componentsjs-error-state.json # Logs logs -*.log +*.log* + +# Build data +build/ # Runtime data pids @@ -56,3 +60,10 @@ pids .env .env.local .env.*.local + +# Temporary folders & files +tmp +*.tmp + +# Misc +.DS_Store diff --git a/README.md b/README.md index db30a3da..67ab303b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,11 @@ You can then execute the following flows: `yarn script:flow` runs all flows in sequence. +## Demonstration + +A more extensive example of a real life use case has been implemented as described in [./demo/README.md](./demo/README.md). + + ## 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 ... diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 00000000..48f916d4 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,36 @@ + +# 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/.internal/accounts/data/c5b28411-2340-4820-8f4f-62c209c20172$.json b/demo/data/.internal/accounts/data/c5b28411-2340-4820-8f4f-62c209c20172$.json new file mode 100644 index 00000000..b57d48d4 --- /dev/null +++ b/demo/data/.internal/accounts/data/c5b28411-2340-4820-8f4f-62c209c20172$.json @@ -0,0 +1 @@ +{"key":"accounts/data/c5b28411-2340-4820-8f4f-62c209c20172","payload":{"linkedLoginsCount":1,"id":"c5b28411-2340-4820-8f4f-62c209c20172","authzServer":"http://localhost:4000/uma","**password**":{"934434d8-d44e-49c2-9618-694594059554":{"accountId":"c5b28411-2340-4820-8f4f-62c209c20172","email":"catalog@example.org","password":"$2a$10$8El17QwKSx3XaHjm.puBiOiNdNQv5t6JHPOVSvPnl8meQFE63CWo6","verified":true,"id":"934434d8-d44e-49c2-9618-694594059554"}},"**clientCredentials**":{},"**pod**":{"f1d42d48-8b96-4122-9e5d-f5803863a243":{"baseUrl":"http://localhost:3000/catalog/","accountId":"c5b28411-2340-4820-8f4f-62c209c20172","id":"f1d42d48-8b96-4122-9e5d-f5803863a243","**owner**":{"6bf4fe03-20c1-419d-9934-2b7533296edf":{"podId":"f1d42d48-8b96-4122-9e5d-f5803863a243","webId":"http://localhost:3000/catalog/profile/card#me","visible":false,"id":"6bf4fe03-20c1-419d-9934-2b7533296edf"}}}},"**webIdLink**":{"0c9522ea-b362-4991-bc72-fd1516834770":{"webId":"http://localhost:3000/catalog/profile/card#me","accountId":"c5b28411-2340-4820-8f4f-62c209c20172","id":"0c9522ea-b362-4991-bc72-fd1516834770"}}}} diff --git a/demo/data/.internal/accounts/data/d3156f11-ffb2-42f3-b928-b9752a9873ce$.json b/demo/data/.internal/accounts/data/d3156f11-ffb2-42f3-b928-b9752a9873ce$.json new file mode 100644 index 00000000..ca7bad89 --- /dev/null +++ b/demo/data/.internal/accounts/data/d3156f11-ffb2-42f3-b928-b9752a9873ce$.json @@ -0,0 +1 @@ +{"key":"accounts/data/d3156f11-ffb2-42f3-b928-b9752a9873ce","payload":{"linkedLoginsCount":1,"id":"d3156f11-ffb2-42f3-b928-b9752a9873ce","authzServer":"http://localhost:4000/uma","**password**":{"084fd63e-faf3-4169-a917-0cdeb768710d":{"accountId":"d3156f11-ffb2-42f3-b928-b9752a9873ce","email":"demo@example.org","password":"$2a$10$CRQGngKyURJztvqyDIdXfOuZMiE43z1kuV7BDwAJCmi/gL4TCcPJ2","verified":true,"id":"084fd63e-faf3-4169-a917-0cdeb768710d"}},"**clientCredentials**":{},"**pod**":{"eb3898e1-d409-41d7-b928-f11a2116f218":{"baseUrl":"http://localhost:3000/demo/","accountId":"d3156f11-ffb2-42f3-b928-b9752a9873ce","id":"eb3898e1-d409-41d7-b928-f11a2116f218","**owner**":{"63f475ea-e87c-472c-a224-1b918a9ae059":{"podId":"eb3898e1-d409-41d7-b928-f11a2116f218","webId":"http://localhost:3000/demo/profile/card#me","visible":false,"id":"63f475ea-e87c-472c-a224-1b918a9ae059"}}}},"**webIdLink**":{"ccd6dcae-8e4c-4e43-9888-cc3bdf49acbd":{"webId":"http://localhost:3000/demo/profile/card#me","accountId":"d3156f11-ffb2-42f3-b928-b9752a9873ce","id":"ccd6dcae-8e4c-4e43-9888-cc3bdf49acbd"}}}} \ No newline at end of file diff --git a/demo/data/.internal/accounts/data/f644f883-ef0f-4986-b5ff-df6866707cf6$.json b/demo/data/.internal/accounts/data/f644f883-ef0f-4986-b5ff-df6866707cf6$.json new file mode 100644 index 00000000..b79da7e9 --- /dev/null +++ b/demo/data/.internal/accounts/data/f644f883-ef0f-4986-b5ff-df6866707cf6$.json @@ -0,0 +1 @@ +{"key":"accounts/data/f644f883-ef0f-4986-b5ff-df6866707cf6","payload":{"linkedLoginsCount":1,"id":"f644f883-ef0f-4986-b5ff-df6866707cf6","authzServer":"http://localhost:4000/uma","**password**":{"126fe0d0-8189-4a51-954a-79e09ff88e18":{"accountId":"f644f883-ef0f-4986-b5ff-df6866707cf6","email":"ruben@example.org","password":"$2a$10$76sVaHi0nDwl46jKtXZR1uxwwIg8hp6gcfzgT7GCzEdKaOVZSnd1e","verified":true,"id":"126fe0d0-8189-4a51-954a-79e09ff88e18"}},"**clientCredentials**":{},"**pod**":{"b79c41e7-a00d-421d-9b57-009c99e7b0d5":{"baseUrl":"http://localhost:3000/ruben/","accountId":"f644f883-ef0f-4986-b5ff-df6866707cf6","id":"b79c41e7-a00d-421d-9b57-009c99e7b0d5","**owner**":{"173cb7a2-2b22-4b25-b4fb-6f61e0adbd35":{"podId":"b79c41e7-a00d-421d-9b57-009c99e7b0d5","webId":"http://localhost:3000/ruben/profile/card#me","visible":false,"id":"173cb7a2-2b22-4b25-b4fb-6f61e0adbd35"}}}},"**webIdLink**":{"fd6d91cf-4b8c-4769-962f-d9f667ee6ee9":{"webId":"http://localhost:3000/ruben/profile/card#me","accountId":"f644f883-ef0f-4986-b5ff-df6866707cf6","id":"fd6d91cf-4b8c-4769-962f-d9f667ee6ee9"}}}} diff --git a/demo/data/.internal/accounts/index/owner/173cb7a2-2b22-4b25-b4fb-6f61e0adbd35$.json b/demo/data/.internal/accounts/index/owner/173cb7a2-2b22-4b25-b4fb-6f61e0adbd35$.json new file mode 100644 index 00000000..5957f410 --- /dev/null +++ b/demo/data/.internal/accounts/index/owner/173cb7a2-2b22-4b25-b4fb-6f61e0adbd35$.json @@ -0,0 +1 @@ +{"key":"accounts/index/owner/173cb7a2-2b22-4b25-b4fb-6f61e0adbd35","payload":["f644f883-ef0f-4986-b5ff-df6866707cf6"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/owner/63f475ea-e87c-472c-a224-1b918a9ae059$.json b/demo/data/.internal/accounts/index/owner/63f475ea-e87c-472c-a224-1b918a9ae059$.json new file mode 100644 index 00000000..b1768574 --- /dev/null +++ b/demo/data/.internal/accounts/index/owner/63f475ea-e87c-472c-a224-1b918a9ae059$.json @@ -0,0 +1 @@ +{"key":"accounts/index/owner/63f475ea-e87c-472c-a224-1b918a9ae059","payload":["d3156f11-ffb2-42f3-b928-b9752a9873ce"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/owner/6bf4fe03-20c1-419d-9934-2b7533296edf$.json b/demo/data/.internal/accounts/index/owner/6bf4fe03-20c1-419d-9934-2b7533296edf$.json new file mode 100644 index 00000000..45d09cb0 --- /dev/null +++ b/demo/data/.internal/accounts/index/owner/6bf4fe03-20c1-419d-9934-2b7533296edf$.json @@ -0,0 +1 @@ +{"key":"accounts/index/owner/6bf4fe03-20c1-419d-9934-2b7533296edf","payload":["c5b28411-2340-4820-8f4f-62c209c20172"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/password/084fd63e-faf3-4169-a917-0cdeb768710d$.json b/demo/data/.internal/accounts/index/password/084fd63e-faf3-4169-a917-0cdeb768710d$.json new file mode 100644 index 00000000..4caef5d5 --- /dev/null +++ b/demo/data/.internal/accounts/index/password/084fd63e-faf3-4169-a917-0cdeb768710d$.json @@ -0,0 +1 @@ +{"key":"accounts/index/password/084fd63e-faf3-4169-a917-0cdeb768710d","payload":["d3156f11-ffb2-42f3-b928-b9752a9873ce"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/password/126fe0d0-8189-4a51-954a-79e09ff88e18$.json b/demo/data/.internal/accounts/index/password/126fe0d0-8189-4a51-954a-79e09ff88e18$.json new file mode 100644 index 00000000..8a46d2c9 --- /dev/null +++ b/demo/data/.internal/accounts/index/password/126fe0d0-8189-4a51-954a-79e09ff88e18$.json @@ -0,0 +1 @@ +{"key":"accounts/index/password/126fe0d0-8189-4a51-954a-79e09ff88e18","payload":["f644f883-ef0f-4986-b5ff-df6866707cf6"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/password/934434d8-d44e-49c2-9618-694594059554$.json b/demo/data/.internal/accounts/index/password/934434d8-d44e-49c2-9618-694594059554$.json new file mode 100644 index 00000000..9ecc6a52 --- /dev/null +++ b/demo/data/.internal/accounts/index/password/934434d8-d44e-49c2-9618-694594059554$.json @@ -0,0 +1 @@ +{"key":"accounts/index/password/934434d8-d44e-49c2-9618-694594059554","payload":["c5b28411-2340-4820-8f4f-62c209c20172"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/password/email/catalog@example.org$.json b/demo/data/.internal/accounts/index/password/email/catalog@example.org$.json new file mode 100644 index 00000000..286ddd5b --- /dev/null +++ b/demo/data/.internal/accounts/index/password/email/catalog@example.org$.json @@ -0,0 +1 @@ +{"key":"accounts/index/password/email/catalog%40example.org","payload":["c5b28411-2340-4820-8f4f-62c209c20172"]} diff --git a/demo/data/.internal/accounts/index/password/email/demo@example.org$.json b/demo/data/.internal/accounts/index/password/email/demo@example.org$.json new file mode 100644 index 00000000..5a421050 --- /dev/null +++ b/demo/data/.internal/accounts/index/password/email/demo@example.org$.json @@ -0,0 +1 @@ +{"key":"accounts/index/password/email/demo%40example.org","payload":["d3156f11-ffb2-42f3-b928-b9752a9873ce"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/password/email/ruben@example.org$.json b/demo/data/.internal/accounts/index/password/email/ruben@example.org$.json new file mode 100644 index 00000000..4a1aa393 --- /dev/null +++ b/demo/data/.internal/accounts/index/password/email/ruben@example.org$.json @@ -0,0 +1 @@ +{"key":"accounts/index/password/email/ruben%40example.org","payload":["f644f883-ef0f-4986-b5ff-df6866707cf6"]} diff --git a/demo/data/.internal/accounts/index/pod/b79c41e7-a00d-421d-9b57-009c99e7b0d5$.json b/demo/data/.internal/accounts/index/pod/b79c41e7-a00d-421d-9b57-009c99e7b0d5$.json new file mode 100644 index 00000000..e103f676 --- /dev/null +++ b/demo/data/.internal/accounts/index/pod/b79c41e7-a00d-421d-9b57-009c99e7b0d5$.json @@ -0,0 +1 @@ +{"key":"accounts/index/pod/b79c41e7-a00d-421d-9b57-009c99e7b0d5","payload":["f644f883-ef0f-4986-b5ff-df6866707cf6"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fcatalog%2F$.json b/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fcatalog%2F$.json new file mode 100644 index 00000000..0d660bc7 --- /dev/null +++ b/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fcatalog%2F$.json @@ -0,0 +1 @@ +{"key":"accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fcatalog%2F","payload":["c5b28411-2340-4820-8f4f-62c209c20172"]} diff --git a/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fdemo%2F$.json b/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fdemo%2F$.json new file mode 100644 index 00000000..62c3a104 --- /dev/null +++ b/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fdemo%2F$.json @@ -0,0 +1 @@ +{"key":"accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fdemo%2F","payload":["d3156f11-ffb2-42f3-b928-b9752a9873ce"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fruben%2F$.json b/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fruben%2F$.json new file mode 100644 index 00000000..ab4619d9 --- /dev/null +++ b/demo/data/.internal/accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fruben%2F$.json @@ -0,0 +1 @@ +{"key":"accounts/index/pod/baseUrl/http%3A%2F%2Flocalhost%3A3000%2Fruben%2F","payload":["f644f883-ef0f-4986-b5ff-df6866707cf6"]} diff --git a/demo/data/.internal/accounts/index/pod/eb3898e1-d409-41d7-b928-f11a2116f218$.json b/demo/data/.internal/accounts/index/pod/eb3898e1-d409-41d7-b928-f11a2116f218$.json new file mode 100644 index 00000000..7cab2026 --- /dev/null +++ b/demo/data/.internal/accounts/index/pod/eb3898e1-d409-41d7-b928-f11a2116f218$.json @@ -0,0 +1 @@ +{"key":"accounts/index/pod/eb3898e1-d409-41d7-b928-f11a2116f218","payload":["d3156f11-ffb2-42f3-b928-b9752a9873ce"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/pod/f1d42d48-8b96-4122-9e5d-f5803863a243$.json b/demo/data/.internal/accounts/index/pod/f1d42d48-8b96-4122-9e5d-f5803863a243$.json new file mode 100644 index 00000000..31a1b104 --- /dev/null +++ b/demo/data/.internal/accounts/index/pod/f1d42d48-8b96-4122-9e5d-f5803863a243$.json @@ -0,0 +1 @@ +{"key":"accounts/index/pod/f1d42d48-8b96-4122-9e5d-f5803863a243","payload":["c5b28411-2340-4820-8f4f-62c209c20172"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/webIdLink/0c9522ea-b362-4991-bc72-fd1516834770$.json b/demo/data/.internal/accounts/index/webIdLink/0c9522ea-b362-4991-bc72-fd1516834770$.json new file mode 100644 index 00000000..3492d1b3 --- /dev/null +++ b/demo/data/.internal/accounts/index/webIdLink/0c9522ea-b362-4991-bc72-fd1516834770$.json @@ -0,0 +1 @@ +{"key":"accounts/index/webIdLink/0c9522ea-b362-4991-bc72-fd1516834770","payload":["c5b28411-2340-4820-8f4f-62c209c20172"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/webIdLink/ccd6dcae-8e4c-4e43-9888-cc3bdf49acbd$.json b/demo/data/.internal/accounts/index/webIdLink/ccd6dcae-8e4c-4e43-9888-cc3bdf49acbd$.json new file mode 100644 index 00000000..0abedfbc --- /dev/null +++ b/demo/data/.internal/accounts/index/webIdLink/ccd6dcae-8e4c-4e43-9888-cc3bdf49acbd$.json @@ -0,0 +1 @@ +{"key":"accounts/index/webIdLink/ccd6dcae-8e4c-4e43-9888-cc3bdf49acbd","payload":["d3156f11-ffb2-42f3-b928-b9752a9873ce"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/webIdLink/fd6d91cf-4b8c-4769-962f-d9f667ee6ee9$.json b/demo/data/.internal/accounts/index/webIdLink/fd6d91cf-4b8c-4769-962f-d9f667ee6ee9$.json new file mode 100644 index 00000000..c3b3703c --- /dev/null +++ b/demo/data/.internal/accounts/index/webIdLink/fd6d91cf-4b8c-4769-962f-d9f667ee6ee9$.json @@ -0,0 +1 @@ +{"key":"accounts/index/webIdLink/fd6d91cf-4b8c-4769-962f-d9f667ee6ee9","payload":["f644f883-ef0f-4986-b5ff-df6866707cf6"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fcatalog%2Fprofile%2Fcard#me$.json b/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fcatalog%2Fprofile%2Fcard#me$.json new file mode 100644 index 00000000..43f90ca5 --- /dev/null +++ b/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fcatalog%2Fprofile%2Fcard#me$.json @@ -0,0 +1 @@ +{"key":"accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fcatalog%2Fprofile%2Fcard%23me","payload":["c5b28411-2340-4820-8f4f-62c209c20172"]} diff --git a/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fdemo%2Fprofile%2Fcard#me$.json b/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fdemo%2Fprofile%2Fcard#me$.json new file mode 100644 index 00000000..46e79bfd --- /dev/null +++ b/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fdemo%2Fprofile%2Fcard#me$.json @@ -0,0 +1 @@ +{"key":"accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fdemo%2Fprofile%2Fcard%23me","payload":["d3156f11-ffb2-42f3-b928-b9752a9873ce"]} \ No newline at end of file diff --git a/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fruben%2Fprofile%2Fcard#me$.json b/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fruben%2Fprofile%2Fcard#me$.json new file mode 100644 index 00000000..f1c210d9 --- /dev/null +++ b/demo/data/.internal/accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fruben%2Fprofile%2Fcard#me$.json @@ -0,0 +1 @@ +{"key":"accounts/index/webIdLink/webId/http%3A%2F%2Flocalhost%3A3000%2Fruben%2Fprofile%2Fcard%23me","payload":["f644f883-ef0f-4986-b5ff-df6866707cf6"]} diff --git a/demo/data/.internal/idp/keys/jwks$.json b/demo/data/.internal/idp/keys/jwks$.json new file mode 100644 index 00000000..355c6ef7 --- /dev/null +++ b/demo/data/.internal/idp/keys/jwks$.json @@ -0,0 +1 @@ +{"key":"idp/keys/jwks","payload":{"keys":[{"kty":"EC","x":"jmoT4I178SCHrtwUu2bFWG0my0o5zQkPhZyDPEyLE6g","y":"nDlReyfF7eyba7XmHXWTs_4Tlzs4ZL94WPaJii7gE90","crv":"P-256","d":"EdprrF79V4LHd5XkO8MHeFyYcXFAgZ_aXmFcuR6lNeU","alg":"ES256"}]}} \ No newline at end of file diff --git a/demo/data/.internal/setup/current-base-url$.json b/demo/data/.internal/setup/current-base-url$.json new file mode 100644 index 00000000..bc4ad5af --- /dev/null +++ b/demo/data/.internal/setup/current-base-url$.json @@ -0,0 +1 @@ +{"key":"setup/current-base-url","payload":"http://localhost:3000/"} \ No newline at end of file diff --git a/demo/data/.internal/setup/current-server-version$.json b/demo/data/.internal/setup/current-server-version$.json new file mode 100644 index 00000000..13e62be5 --- /dev/null +++ b/demo/data/.internal/setup/current-server-version$.json @@ -0,0 +1 @@ +{"key":"setup/current-server-version","payload":"7.0.2"} \ No newline at end of file diff --git a/demo/data/.internal/setup/rootInitialized$.json b/demo/data/.internal/setup/rootInitialized$.json new file mode 100644 index 00000000..6658e134 --- /dev/null +++ b/demo/data/.internal/setup/rootInitialized$.json @@ -0,0 +1 @@ +{"key":"setup/rootInitialized","payload":true} \ No newline at end of file diff --git a/demo/data/.internal/setup/v6-migration$.json b/demo/data/.internal/setup/v6-migration$.json new file mode 100644 index 00000000..a7768a28 --- /dev/null +++ b/demo/data/.internal/setup/v6-migration$.json @@ -0,0 +1 @@ +{"key":"setup/v6-migration","payload":true} \ No newline at end of file diff --git a/demo/data/.meta b/demo/data/.meta new file mode 100644 index 00000000..d1372b07 --- /dev/null +++ b/demo/data/.meta @@ -0,0 +1 @@ + a . diff --git a/demo/data/catalog/.meta b/demo/data/catalog/.meta new file mode 100644 index 00000000..c65fdc62 --- /dev/null +++ b/demo/data/catalog/.meta @@ -0,0 +1 @@ + a . diff --git a/packages/css/templates/pod/base/public/filters/age$.sparql b/demo/data/catalog/public/filters/age$.sparql similarity index 100% rename from packages/css/templates/pod/base/public/filters/age$.sparql rename to demo/data/catalog/public/filters/age$.sparql diff --git a/packages/css/templates/pod/base/public/filters/bday$.sparql b/demo/data/catalog/public/filters/bday$.sparql similarity index 100% rename from packages/css/templates/pod/base/public/filters/bday$.sparql rename to demo/data/catalog/public/filters/bday$.sparql diff --git a/demo/data/index.html b/demo/data/index.html new file mode 100644 index 00000000..b74cf1c7 --- /dev/null +++ b/demo/data/index.html @@ -0,0 +1,97 @@ + + + + + + Community Solid Server + + + +
+ [Solid logo] +

Community Solid Server

+
+
+

Welcome to Solid

+

+ This server implements + the Solid protocol + so you can create your own Solid Pod + and identity. +

+ +

Getting started as a user

+

+ Sign up for an account + to get started with your own Pod and WebID. +

+

+ The default configuration stores data only in memory. + If you want to keep data permanently, + choose a configuration that saves data to disk instead. +

+

+ To learn more about how this server can be used, + have a look at the + getting started tutorial. +

+ +

Getting started as a developer

+

+ The default configuration includes + the ready-to-use root Pod you're currently looking at. +
+ You can use any of the configurations in the config folder of the server + to set up an instance of this server with different features. + Besides the provided configurations, + you can also fine-tune your own custom configuration using the + configuration generator. +

+

+ You can easily choose any folder on your disk + to expose as the root Pod with file-based configurations. +
+ Use the --help switch to learn more. +

+

+ Due to certain restrictions in the Solid specification it is usually not allowed + to both allow data to be written to the root of the server, + and to enable the creation of new pods. + This configuration does allow both these options to allow a quick exploration of Solid, + but other configurations provided will only allow one of those two to be enabled. +

+ +

Have a wonderful Solid experience

+

+ Learn more about Solid + at solidproject.org. +

+

+ You are warmly invited + to share your experiences + and to report any bugs you encounter. +

+
+ + + + + diff --git a/demo/data/ruben/.meta b/demo/data/ruben/.meta new file mode 100644 index 00000000..2ae477cb --- /dev/null +++ b/demo/data/ruben/.meta @@ -0,0 +1 @@ + a . diff --git a/demo/data/ruben/private/.meta b/demo/data/ruben/private/.meta new file mode 100644 index 00000000..536a6faf --- /dev/null +++ b/demo/data/ruben/private/.meta @@ -0,0 +1,13 @@ +@prefix derived: . + +<> derived:derivedResource [ + derived:template "derived/bday"; + derived:selector <./data>; + derived:filter + ]. + +<> derived:derivedResource [ + derived:template "derived/age"; + derived:selector <./data>; + derived:filter + ]. diff --git a/demo/data/ruben/private/data$.ttl b/demo/data/ruben/private/data$.ttl new file mode 100644 index 00000000..4eef87e2 --- /dev/null +++ b/demo/data/ruben/private/data$.ttl @@ -0,0 +1,14 @@ +@prefix ruben: . +@prefix con: . +@prefix dbo: . +@prefix dbp: . +@prefix foaf: . +@prefix xsd: . + +ruben: a foaf:Person; + con:preferredURI "https://ruben.verborgh.org/profile/#me"; + foaf:familyName "Verborgh"@en, "Verborgh"@nl ; + foaf:givenName "Ruben"@en, "Ruben"@nl ; + dbo:birthDate "1987-02-28"^^xsd:date ; + dbo:birthPlace dbp:Ostend ; + foaf:gender "male"@en . diff --git a/demo/data/ruben/profile/card$.ttl b/demo/data/ruben/profile/card$.ttl new file mode 100644 index 00000000..54f84452 --- /dev/null +++ b/demo/data/ruben/profile/card$.ttl @@ -0,0 +1,27 @@ +@prefix rdfs: . +@prefix foaf: . +@prefix solid: . +@prefix filters: . +@prefix views: . +@prefix ruben: . + +<> a foaf:PersonalProfileDocument; + foaf:maker ruben:; + foaf:primaryTopic ruben:. + +ruben: a foaf:Person ; + foaf:name "Ruben Verborgh"@en, "Ruben Verborgh"@nl; + rdfs:label "Ruben Verborgh"@en, "Ruben Verborgh"@nl; + solid:umaServer "http://localhost:4000/uma/" ; + solid:oidcIssuer ; + solid:viewIndex <#index> . + +<#index> a solid:ViewIndex ; + solid:entry [ + solid:filter filters:bday ; + solid:location views:bday + ] ; + solid:entry [ + solid:filter filters:age ; + solid:location views:age + ] . diff --git a/demo/data/ruben/settings/policies/.meta b/demo/data/ruben/settings/policies/.meta new file mode 100644 index 00000000..e69de29b diff --git a/demo/flow.ts b/demo/flow.ts new file mode 100644 index 00000000..41d06353 --- /dev/null +++ b/demo/flow.ts @@ -0,0 +1,187 @@ +/* eslint-disable max-len */ + +import { fetch } from 'cross-fetch'; +import { Parser, Writer, Store } from 'n3'; +import { demoPolicy } from "./policyCreation"; + +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', + }, + agents: { + ruben: 'http://localhost:3000/ruben/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', + } +} + +async function main() { + + log(`Alright, so, for the demo ...`); + + log(`Ruben V., a.k.a. <${terms.agents.ruben}>, has some private data in .`); + + 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.)`); + + 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.`); + + const accessRequest = { + permissions: [{ + resource_id: terms.views.age, + resource_scopes: [ terms.scopes.read ], + }] + }; + + const accessDeniedResponse = await fetch(tokenEndpoint, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(accessRequest), + }); + + if (accessDeniedResponse.status !== 403) { log('Access request succeeded without policy...'); throw 0; } + + 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(`...`); + + 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.`); + + 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 }) + + // 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)) + }); + + 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, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(accessRequest), + }); + + if (needInfoResponse.status !== 403) { log('Access request succeeded without claims...'); 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}`)) + + // 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" + // } + const claim_token = "eyJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vd3d3LnczLm9yZy9ucy9vZHJsLzIvcHVycG9zZSI6InVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnB1cnBvc2U6YWdlLXZlcmlmaWNhdGlvbiIsInVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnR5cGVzOndlYmlkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL2RlbW8vcHVibGljL3ZlbmRvciJ9.Px7G3zl1ZpTy1lk7ziRMvNv12Enb0uhup9kiVI6Ot3s" + + 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 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, + }) + }); + + if (accessGrantedResponse.status !== 200) { log('Access request failed despite policy...'); 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, { + headers: { 'Authorization': `${tokenParams.token_type} ${tokenParams.access_token}` } + }); + + if (accessWithTokenResponse.status !== 200) { log('Access with token failed...'); throw 0; } + + log(`The agent can then use this access token at the Resource Server to perform the desired action.`); +} + +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); + } +} + +// creates the container if it does not exist yet (only when access is there) +async function initContainer(policyContainer: string): Promise { + const res = await fetch(policyContainer) + if (res.status === 404) { + const res = await fetch(policyContainer, { + method: 'PUT' + }) + if (res.status !== 201) { + log('Creating container at ' + policyContainer + ' not successful'); throw 0; + } + } +} + diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 00000000..9b56caa4 --- /dev/null +++ b/demo/package.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 00000000..0bf8d21c --- /dev/null +++ b/demo/policyCreation.ts @@ -0,0 +1,53 @@ +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 new file mode 100644 index 00000000..c7657d4d --- /dev/null +++ b/demo/problem/context.ttl @@ -0,0 +1,4 @@ + + ; + ; + . diff --git a/demo/problem/policy1.ttl b/demo/problem/policy1.ttl new file mode 100644 index 00000000..665fde13 --- /dev/null +++ b/demo/problem/policy1.ttl @@ -0,0 +1,19 @@ +@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 new file mode 100644 index 00000000..19ff91ff --- /dev/null +++ b/demo/problem/policy2.ttl @@ -0,0 +1,19 @@ +@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 new file mode 100644 index 00000000..9e03bab2 --- /dev/null +++ b/demo/problem/policy3.ttl @@ -0,0 +1,19 @@ +@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 new file mode 100644 index 00000000..2c240210 --- /dev/null +++ b/demo/problem/rules_crud.n3 @@ -0,0 +1,163 @@ +@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 new file mode 100644 index 00000000..d6e08fa2 --- /dev/null +++ b/demo/problem/rules_purpose.n3 @@ -0,0 +1,79 @@ + +@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 new file mode 100644 index 00000000..111a7280 --- /dev/null +++ b/demo/problem/test.ts @@ -0,0 +1,28 @@ +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 new file mode 100644 index 00000000..1193845d --- /dev/null +++ b/demo/sites/authorizationsite/package.json @@ -0,0 +1,27 @@ +{ + "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 new file mode 100644 index 00000000..1aaf9e4c --- /dev/null +++ b/demo/sites/authorizationsite/public/index.html @@ -0,0 +1,11 @@ + + + + + Authorization Companion + + + +
+ + diff --git a/demo/sites/authorizationsite/src/App.css b/demo/sites/authorizationsite/src/App.css new file mode 100644 index 00000000..e247a5ff --- /dev/null +++ b/demo/sites/authorizationsite/src/App.css @@ -0,0 +1,39 @@ +.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 new file mode 100644 index 00000000..236d3ee5 --- /dev/null +++ b/demo/sites/authorizationsite/src/App.tsx @@ -0,0 +1,84 @@ +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 new file mode 100644 index 00000000..e6171ae6 --- /dev/null +++ b/demo/sites/authorizationsite/src/components/DatePicker.tsx @@ -0,0 +1,30 @@ +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 new file mode 100644 index 00000000..3e7ae1a1 --- /dev/null +++ b/demo/sites/authorizationsite/src/components/FormModal.tsx @@ -0,0 +1,119 @@ +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 new file mode 100644 index 00000000..38349b7d --- /dev/null +++ b/demo/sites/authorizationsite/src/components/Home.tsx @@ -0,0 +1,67 @@ +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) + } +
+ +
+
+