From e6de2a9b1bcbd50d739f326b7f00b0c8fe157ebc Mon Sep 17 00:00:00 2001 From: "Wouter Termont (imec)" Date: Wed, 14 Feb 2024 12:52:11 +0100 Subject: [PATCH 1/3] chore: minor update of reType (#28) Signed-off-by: Wouter Termont --- packages/uma/src/util/ReType.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/uma/src/util/ReType.ts b/packages/uma/src/util/ReType.ts index 29e335f..4bbeb06 100644 --- a/packages/uma/src/util/ReType.ts +++ b/packages/uma/src/util/ReType.ts @@ -31,14 +31,22 @@ export type Assertion = (value: unknown) => asserts value is T; export type ReType = Literal | Assertion | { [_: PropertyKey]: ReType }; -export type Type = - R extends { [_: PropertyKey]: ReType } ? { - [K in keyof R as undefined extends Type ? never : K]: Type - } & { - [K in keyof R as undefined extends Type ? K : never]?: Type - } : +type _Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; + +type _Required = { + [K in keyof R as undefined extends Type ? never : K]: Type +}; + +type _Optional = { + [K in keyof R as undefined extends Type ? K : never]?: Type +}; + +type _Type = + R extends { [_: PropertyKey]: ReType } ? _Required & _Optional : R extends Assertion ? T : R; + +export type Type = _Expand<_Type>; function isIn(key: PropertyKey, object: T): key is keyof T { return key in object; @@ -73,7 +81,6 @@ export function isType(value: unknown, assertion: R): value is return value === assertion; } - export const any: Assertion = () => {}; export const unknown: Assertion = () => {}; export const never: Assertion = () => { throw new Error() }; @@ -166,5 +173,3 @@ export const record = < export const dict = (records: T): Assertion>> => { return record(string, records); } - -// type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; // https://www.npmjs.com/package/type-expand From e01d61044ae4ed6ee6c2c323a3924158840aec0b Mon Sep 17 00:00:00 2001 From: "Wouter Termont (imec)" Date: Wed, 14 Feb 2024 12:56:17 +0100 Subject: [PATCH 2/3] docs: update next steps in README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8566850..4279814 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This repository contains SolidLab research artefacts on use of UMA in the Solid - [`@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. @@ -33,10 +34,12 @@ You can then execute the following flows: `yarn script:flow` runs all flows in sequence. + ## 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 ... + ### Usage control policy enforcement Used for creating a modular engine that calculates which access modes are granted based on: @@ -54,6 +57,4 @@ Then a read request is performed using the engine, which results in a list of gr ## Next steps -- [Wout Slabbinck](https://github.com/woutslabbinck) will look into custom [Authorizers](packages/uma/src/models/Authorizer.ts), in particular an integratation with Koreografeye for the research on Usage Control Patterns. - -- [Wouter Termont](https://github.com/termontwouter) will implement UMA Resource Registration (as specified in UMA 2.0 Federation), and integrate UMA AS coupling into the onboarding flow of the CSS. +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. From 3ace0e3711b3a435158baad8c3d8fe6b01bed89d Mon Sep 17 00:00:00 2001 From: Wouter Termont Date: Thu, 15 Feb 2024 13:00:07 +0100 Subject: [PATCH 3/3] chore: clean up scripts for demo Signed-off-by: Wouter Termont --- packages/css/src/util/OwnerUtil.ts | 2 +- scripts/test-private.ts | 71 ++++++++++++--------------- scripts/test-public.ts | 13 +++-- scripts/test-registration.ts | 32 +++++++------ scripts/test-uma-ucp.ts | 77 +++++++++++++----------------- 5 files changed, 90 insertions(+), 105 deletions(-) diff --git a/packages/css/src/util/OwnerUtil.ts b/packages/css/src/util/OwnerUtil.ts index 21cb4cb..9143781 100644 --- a/packages/css/src/util/OwnerUtil.ts +++ b/packages/css/src/util/OwnerUtil.ts @@ -50,7 +50,7 @@ export class OwnerUtil { this.logger.debug(`Looking up owners of pod ${pod.id}`); const as = await this.accountStore.getSetting(pod.accountId, ACCOUNT_SETTINGS_AUTHZ_SERVER); - this.logger.warn(`REAL AS is ${JSON.stringify(as)}`); + // this.logger.warn(`REAL AS is ${JSON.stringify(as)}`); const owners = await this.podStore.getOwners(pod.id); if (!owners) throw new Error(`Unable to find owners for pod ${storage.path}`); diff --git a/scripts/test-private.ts b/scripts/test-private.ts index 421f21b..8d52bbc 100644 --- a/scripts/test-private.ts +++ b/scripts/test-private.ts @@ -14,78 +14,69 @@ const request: RequestInit = { async function main() { - console.log(`3.1 Send request to protected resource (${privateResource}) without access token.`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.1 - // 3.1 Client Requests Resource Without Providing an Access Token + console.log('\n\n'); + + console.log(`=== Trying to create private resource <${privateResource}> without access token.\n`); + const noTokenResponse = await fetch(privateResource, request); - console.log("3.2 Resource Server Responds to Client's Tokenless Access Attempt"); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.2 - // 3.2 Resource Server Responds to Client's Tokenless Access Attempt - console.log(noTokenResponse.status); - console.log(await noTokenResponse.text()); const wwwAuthenticateHeader = noTokenResponse.headers.get("WWW-Authenticate")! - // Note: needs errorhandling when not present - console.log(wwwAuthenticateHeader); + + console.log(`= Status: ${noTokenResponse.status}\n`); + console.log(`= Www-Authenticate header: ${wwwAuthenticateHeader}\n`); + console.log(''); const { as_uri, ticket } = Object.fromEntries(wwwAuthenticateHeader.replace(/^UMA /,'').split(', ').map( param => param.split('=').map(s => s.replace(/"/g,'')) )); - console.log(as_uri); - console.log(ticket); const tokenEndpoint = as_uri + "/token" // should normally be retrieved from .well-known/uma2-configuration - // the claim that I am that person? - // const claim_token = "http://localhost:3000/alice/profile/card#me" const claim_token = "https://woslabbi.pod.knows.idlab.ugent.be/profile/card#me" - console.log(`3.3.1 Client Request to Authorization Server (${as_uri}) for RPT`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.1 - // 3.3.1 Client Request to Authorization Server for RPT - const body = JSON.stringify({ + const content = { grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', ticket, claim_token: encodeURIComponent(claim_token), claim_token_format: 'urn:solidlab:uma:claims:formats:webid', - }); - console.log("Token request body: ", body); + }; + + console.log(`=== Requesting token at ${tokenEndpoint} with ticket body:\n`); + console.log(content); + console.log(''); + const asRequestResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "content-type":"application/json" }, - body + body: JSON.stringify(content), }) + // For debugging: // console.log("Authorization Server response:", await asRequestResponse.text()); // throw 'stop' - const asResponse = await asRequestResponse.json() - console.log("Authorization Server response:", asResponse); - console.log(`3.3.5 Authorization Server Response to Client on Authorization Success:`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.5 or https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.6 - // 3.3.5 or 3.3.6 Authorization Server Response to Client on Authorization Success or Failure - // Note: it is required to have a debug uma server loaded + const asResponse = await asRequestResponse.json(); const decodedToken = parseJwt(asResponse.access_token); - console.log("Access token decoded:",decodedToken) - for (const permission of decodedToken.permissions) { - console.log(`Permissioned scopes for resource ${permission.resource_id}:`, permission.resource_scopes) + console.log(`= Status: ${asRequestResponse.status}\n`); + console.log(`= Body (decoded):\n`); + console.log({ ...asResponse, access_token: asResponse.access_token.slice(0,10).concat('...') }); + console.log('\n'); - } + // for (const permission of decodedToken.permissions) { + // console.log(`Permissioned scopes for resource ${permission.resource_id}:`, permission.resource_scopes) + // } - console.log(`3.4 Client Requests Resource and Provides an RPT`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.4 - // 3.4 Client Requests Resource and Provides an RPT - // Only in happy flow (when we get a success 3.3.5) + console.log(`=== Trying to create private resource <${privateResource}> WITH access token.\n`); + request.headers = { 'Authorization': `${asResponse.token_type} ${asResponse.access_token}` }; + const tokenResponse = await fetch(privateResource, request); - console.log(`3.5 Resource Server Responds to Client's RPT-Accompanied Resource Request:`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.5 - // 3.5 Resource Server Responds to Client's RPT-Accompanied Resource Request - console.log(tokenResponse.status); + console.log(`= Status: ${tokenResponse.status}\n`); } -main() \ No newline at end of file + +main(); diff --git a/scripts/test-public.ts b/scripts/test-public.ts index 0241aea..6813f0b 100644 --- a/scripts/test-public.ts +++ b/scripts/test-public.ts @@ -3,12 +3,15 @@ import { fetch } from 'cross-fetch' const publicResource = "http://localhost:3000/alice/profile/card" async function main() { - console.log(`=== Trying to read public resource <${publicResource}> without access token.`); + + console.log('\n\n'); + + console.log(`=== Trying to read public resource <${publicResource}> without access token.\n`); const publicResponse = await fetch(publicResource, { method: "GET" }); - - console.log(`= Status: ${publicResponse.status}`); - console.log(`= Body: \n${await publicResponse.text()}`); + + console.log(`= Status: ${publicResponse.status}\n`); + console.log(`= Body:\n \n${await publicResponse.text()}\n`); } -main(); \ No newline at end of file +main(); diff --git a/scripts/test-registration.ts b/scripts/test-registration.ts index 7fb591e..e25b2b6 100644 --- a/scripts/test-registration.ts +++ b/scripts/test-registration.ts @@ -1,46 +1,48 @@ import { fetch } from 'cross-fetch' -const container = "http://localhost:3000/alice/public/" -const slug = "resource.txt" +const container = "http://localhost:3000/alice/public/"; +const slug = "resource.txt"; +const body = "This is a resource."; async function main() { - console.log("=== Creating container (if needed) ...") + console.log(`=== PUT container <${container}>\n`); const containerResponse = await fetch(container, { method: "PUT", }) - console.log(`= Status: ${containerResponse.status}`); + console.log(`= Status: ${containerResponse.status}\n`); + console.log('\n'); - console.log("=== Creating resource ...") + console.log(`=== POST to <${container}> with slug '${slug}': "${body}"\n`) const createResponse = await fetch(container, { method: "POST", headers: { slug }, - body: "This is a resource." + body }) - console.log(`= Status: ${createResponse.status}`); + console.log(`= Status: ${createResponse.status}\n`); + console.log('\n'); - console.log("=== Creating resource ...") + console.log(`=== GET <${container + slug}>\n`); const readResponse = await fetch(container + slug, { method: "GET", }) - console.log(`= Status: ${readResponse.status}`); - console.log(`= Body: \n${await readResponse.text()}`); - - console.log("=== Deleting resource ...") + console.log(`= Status: ${readResponse.status}\n`); + console.log(`= Body: "${await readResponse.text()}"\n`); + console.log('\n'); + console.log(`=== DELETE <${container + slug}>\n`); const deleteResponse = await fetch(container + slug, { method: "DELETE", }) - console.log(`= Status: ${deleteResponse.status}`); - + console.log(`= Status: ${deleteResponse.status}\n`); } -main(); \ No newline at end of file +main(); diff --git a/scripts/test-uma-ucp.ts b/scripts/test-uma-ucp.ts index 63707a5..5367878 100644 --- a/scripts/test-uma-ucp.ts +++ b/scripts/test-uma-ucp.ts @@ -1,7 +1,7 @@ import { fetch } from 'cross-fetch' // Resource and WebID as set in config/rules/policy/policy0.ttl -const resource = "http://localhost:3000/alice/other/resource.txt" +const resource = "http://localhost:3000/alice/other/resource.txt"; const webid = "https://woslabbi.pod.knows.idlab.ugent.be/profile/card#me"; function parseJwt (token:string) { @@ -16,78 +16,67 @@ const request: RequestInit = { async function main() { - console.log(`3.1 Send request to protected resource (${resource}) without access token.`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.1 - // 3.1 Client Requests Resource Without Providing an Access Token + console.log('\n\n'); + + console.log(`=== Trying to create private resource <${resource}> without access token.\n`); + const noTokenResponse = await fetch(resource, request); - console.log("3.2 Resource Server Responds to Client's Tokenless Access Attempt"); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.2 - // 3.2 Resource Server Responds to Client's Tokenless Access Attempt - console.log(noTokenResponse.status); - console.log(await noTokenResponse.text()); const wwwAuthenticateHeader = noTokenResponse.headers.get("WWW-Authenticate")! - // Note: needs errorhandling when not present - console.log(wwwAuthenticateHeader); + + console.log(`= Status: ${noTokenResponse.status}\n`); + console.log(`= Www-Authenticate header: ${wwwAuthenticateHeader}\n`); + console.log(''); const { as_uri, ticket } = Object.fromEntries(wwwAuthenticateHeader.replace(/^UMA /,'').split(', ').map( param => param.split('=').map(s => s.replace(/"/g,'')) )); - console.log(as_uri); - console.log(ticket); const tokenEndpoint = as_uri + "/token" // should normally be retrieved from .well-known/uma2-configuration - // the claim that I am that person? - // const claim_token = "http://localhost:3000/alice/profile/card#me" - const claim_token = webid; - - console.log(`3.3.1 Client Request to Authorization Server (${as_uri}) for RPT`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.1 - // 3.3.1 Client Request to Authorization Server for RPT - const body = JSON.stringify({ + const content = { grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', ticket, - claim_token: encodeURIComponent(claim_token), + claim_token: encodeURIComponent(webid), claim_token_format: 'urn:solidlab:uma:claims:formats:webid', - }); - console.log("Token request body: ", body); + }; + + console.log(`=== Requesting token at ${tokenEndpoint} with ticket body:\n`); + console.log(content); + console.log(''); + const asRequestResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "content-type":"application/json" }, - body + body: JSON.stringify(content), }) + // For debugging: // console.log("Authorization Server response:", await asRequestResponse.text()); // throw 'stop' - const asResponse = await asRequestResponse.json() - console.log("Authorization Server response:", asResponse); - console.log(`3.3.5 Authorization Server Response to Client on Authorization Success:`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.5 or https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.6 - // 3.3.5 or 3.3.6 Authorization Server Response to Client on Authorization Success or Failure - // Note: it is required to have a debug uma server loaded + const asResponse = await asRequestResponse.json() const decodedToken = parseJwt(asResponse.access_token); - console.log("Access token decoded:",decodedToken) - for (const permission of decodedToken.permissions) { - console.log(`Permissioned scopes for resource ${permission.resource_id}:`, permission.resource_scopes) + console.log(`= Status: ${asRequestResponse.status}\n`); + console.log(`= Body (decoded):\n`); + console.log({ ...asResponse, access_token: asResponse.access_token.slice(0,10).concat('...') }); + console.log('\n'); + + // for (const permission of decodedToken.permissions) { + // console.log(`Permissioned scopes for resource ${permission.resource_id}:`, permission.resource_scopes) + // } - } + console.log(`=== Trying to create private resource <${resource}> WITH access token.\n`); - console.log(`3.4 Client Requests Resource and Provides an RPT`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.4 - // 3.4 Client Requests Resource and Provides an RPT - // Only in happy flow (when we get a success 3.3.5) request.headers = { 'Authorization': `${asResponse.token_type} ${asResponse.access_token}` }; + const tokenResponse = await fetch(resource, request); - console.log(`3.5 Resource Server Responds to Client's RPT-Accompanied Resource Request:`); - // https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html#rfc.section.3.3.5 - // 3.5 Resource Server Responds to Client's RPT-Accompanied Resource Request - console.log(tokenResponse.status); + console.log(`= Status: ${tokenResponse.status}\n`); } -main() + +main();