Skip to content

Commit

Permalink
Merge branch 'main' into feat/ucp-migration
Browse files Browse the repository at this point in the history
  • Loading branch information
termontwouter authored Feb 19, 2024
2 parents 2de4096 + 3ace0e3 commit ecb673a
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 117 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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.
2 changes: 1 addition & 1 deletion packages/css/src/util/OwnerUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
23 changes: 14 additions & 9 deletions packages/uma/src/util/ReType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@ export type Assertion<T> = (value: unknown) => asserts value is T;

export type ReType = Literal | Assertion<unknown> | { [_: PropertyKey]: ReType };

export type Type<R extends ReType> =
R extends { [_: PropertyKey]: ReType } ? {
[K in keyof R as undefined extends Type<R[K]> ? never : K]: Type<R[K]>
} & {
[K in keyof R as undefined extends Type<R[K]> ? K : never]?: Type<R[K]>
} :
type _Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

type _Required<R extends { [_: PropertyKey]: ReType }> = {
[K in keyof R as undefined extends Type<R[K]> ? never : K]: Type<R[K]>
};

type _Optional<R extends { [_: PropertyKey]: ReType }> = {
[K in keyof R as undefined extends Type<R[K]> ? K : never]?: Type<R[K]>
};

type _Type<R extends ReType> =
R extends { [_: PropertyKey]: ReType } ? _Required<R> & _Optional<R> :
R extends Assertion<infer T> ? T :
R;

export type Type<R extends ReType> = _Expand<_Type<R>>;

function isIn<T extends object>(key: PropertyKey, object: T): key is keyof T {
return key in object;
Expand Down Expand Up @@ -73,7 +81,6 @@ export function isType<R extends ReType>(value: unknown, assertion: R): value is
return value === assertion;
}


export const any: Assertion<any> = () => {};
export const unknown: Assertion<unknown> = () => {};
export const never: Assertion<never> = () => { throw new Error() };
Expand Down Expand Up @@ -166,5 +173,3 @@ export const record = <
export const dict = <T extends ReType> (records: T): Assertion<NodeJS.Dict<Type<T>>> => {
return record(string, records);
}

// type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never; // https://www.npmjs.com/package/type-expand
71 changes: 31 additions & 40 deletions scripts/test-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

main();
13 changes: 8 additions & 5 deletions scripts/test-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
main();
32 changes: 17 additions & 15 deletions scripts/test-registration.ts
Original file line number Diff line number Diff line change
@@ -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();
main();
77 changes: 33 additions & 44 deletions scripts/test-uma-ucp.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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();

0 comments on commit ecb673a

Please sign in to comment.