Skip to content

Commit cfc36df

Browse files
feat: check purpose constraints in negotiation (#38)
Signed-off-by: Wouter Termont <[email protected]>
1 parent 93412a3 commit cfc36df

28 files changed

+496
-344
lines changed

demo/data/demo/README$.markdown

Lines changed: 0 additions & 27 deletions
This file was deleted.

demo/data/demo/public/.meta

Lines changed: 0 additions & 7 deletions
This file was deleted.

demo/flow.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,6 @@ async function main() {
6969
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.`);
7070

7171
const accessRequest = {
72-
// grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket',
73-
// ticket,
74-
claim_token: encodeURIComponent(terms.agents.vendor),
75-
claim_token_format: 'urn:solidlab:uma:claims:formats:webid',
7672
permissions: [{
7773
resource_id: terms.views.age,
7874
resource_scopes: [ terms.scopes.read ],
@@ -85,7 +81,7 @@ async function main() {
8581
body: JSON.stringify(accessRequest),
8682
});
8783

88-
// if (accessDeniedResponse.status !== 403) { log('Access request succeeded without policy...'); throw 0; }
84+
if (accessDeniedResponse.status !== 403) { log('Access request succeeded without policy...'); throw 0; }
8985

9086
log(`Without a policy allowing the access, the access is denied.`);
9187
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.)`);
@@ -96,7 +92,7 @@ async function main() {
9692

9793
const startDate = new Date();
9894
const endDate = new Date(startDate.valueOf() + 14 * 24 * 60 * 60 * 1000);
99-
const purpose = 'age-verification'
95+
const purpose = 'urn:solidlab:uma:claims:purpose:age-verification'
10096
const policy = demoPolicy(terms.views.age, terms.agents.vendor, { startDate, endDate, purpose })
10197

10298
// create container if it does not exist yet
@@ -111,15 +107,42 @@ async function main() {
111107

112108
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.`);
113109

114-
const accessGrantedResponse = await fetch(tokenEndpoint, {
110+
const needInfoResponse = await fetch(tokenEndpoint, {
115111
method: "POST",
116112
headers: { "content-type": "application/json" },
117113
body: JSON.stringify(accessRequest),
118114
});
119115

116+
if (needInfoResponse.status !== 403) { log('Access request succeeded without claims...'); throw 0; }
117+
118+
const { ticket, required_claims } = await needInfoResponse.json();
119+
120+
log(`Based on the policy, the UMA server requests the following claims from the agent:`);
121+
required_claims.claim_token_format[0].forEach((format: string) => log(` - ${format}`))
122+
123+
// JWT (HS256; secret: "ceci n'est pas un secret")
124+
// {
125+
// "http://www.w3.org/ns/odrl/2/purpose": "urn:solidlab:uma:claims:purpose:age-verification",
126+
// "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/demo/public/vendor"
127+
// }
128+
const claim_token = "eyJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vd3d3LnczLm9yZy9ucy9vZHJsLzIvcHVycG9zZSI6InVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnB1cnBvc2U6YWdlLXZlcmlmaWNhdGlvbiIsInVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnR5cGVzOndlYmlkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL2RlbW8vcHVibGljL3ZlbmRvciJ9.Px7G3zl1ZpTy1lk7ziRMvNv12Enb0uhup9kiVI6Ot3s"
129+
130+
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.`)
131+
132+
const accessGrantedResponse = await fetch(tokenEndpoint, {
133+
method: "POST",
134+
headers: { "content-type": "application/json" },
135+
body: JSON.stringify({
136+
...accessRequest,
137+
ticket,
138+
claim_token_format: 'urn:solidlab:uma:claims:formats:jwt',
139+
claim_token,
140+
})
141+
});
142+
120143
if (accessGrantedResponse.status !== 200) { log('Access request failed despite policy...'); throw 0; }
121144

122-
log(`Based on the policy, the UMA server returns the agent an access token with the requested permissions.`);
145+
log(`The UMA server checks the claims with the relevant policy, and returns the agent an access token with the requested permissions.`);
123146

124147
const tokenParams = await accessGrantedResponse.json();
125148
const accessWithTokenResponse = await fetch(terms.views.age, {

demo/policyCreation.ts

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,45 @@ import { SimplePolicy, UCPPolicy, basicPolicy } from '@solidlab/ucp'
99
* @param constraints - the temporal and purpuse constraints on the usage of the data
1010
*/
1111
export function demoPolicy(
12-
targetIRI: string,
13-
requestingPartyIRI: string,
14-
constraints?: {
15-
startDate?: Date,
16-
endDate?: Date,
17-
purpose?: string
18-
}
12+
targetIRI: string,
13+
requestingPartyIRI: string,
14+
constraints?: {
15+
startDate?: Date,
16+
endDate?: Date,
17+
purpose?: string
18+
}
1919
): SimplePolicy {
20-
const constraintList: any[] = [];
20+
const constraintList: any[] = [];
2121

22-
if (constraints?.startDate) constraintList.push({
23-
type: 'temporal',
24-
operator: 'http://www.w3.org/ns/odrl/2/gt',
25-
value: constraints?.startDate,
26-
});
22+
if (constraints?.startDate) constraintList.push({
23+
type: 'temporal',
24+
operator: 'http://www.w3.org/ns/odrl/2/gt',
25+
value: constraints?.startDate,
26+
});
2727

28-
if (constraints?.endDate) constraintList.push({
29-
type: 'temporal',
30-
operator: 'http://www.w3.org/ns/odrl/2/lt',
31-
value: constraints?.endDate,
32-
});
28+
if (constraints?.endDate) constraintList.push({
29+
type: 'temporal',
30+
operator: 'http://www.w3.org/ns/odrl/2/lt',
31+
value: constraints?.endDate,
32+
});
3333

34-
if (constraints?.purpose) constraintList.push({
35-
type: 'purpose',
36-
operator: 'http://www.w3.org/ns/odrl/2/eq',
37-
value: constraints?.purpose,
38-
});
34+
if (constraints?.purpose) constraintList.push({
35+
type: 'purpose',
36+
operator: 'http://www.w3.org/ns/odrl/2/eq',
37+
value: constraints?.purpose,
38+
});
3939

40-
const policy: UCPPolicy = {
41-
rules: [{
42-
resource: targetIRI,
43-
action: "http://www.w3.org/ns/odrl/2/read", // ODRL action
44-
requestingParty: requestingPartyIRI,
45-
// owner: "https://pod.woutslabbinck.com/profile/card#me", // might error
46-
constraints: constraintList
47-
}]
48-
}
40+
const policy: UCPPolicy = {
41+
rules: [{
42+
resource: targetIRI,
43+
action: "http://www.w3.org/ns/odrl/2/read", // ODRL action
44+
requestingParty: requestingPartyIRI,
45+
// owner: "https://pod.woutslabbinck.com/profile/card#me", // might error
46+
constraints: constraintList
47+
}]
48+
}
4949

50-
const policyObject = basicPolicy(policy);
50+
const policyObject = basicPolicy(policy);
5151

52-
return policyObject
52+
return policyObject
5353
}

packages/ucp/src/Request.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
import { Store, DataFactory } from "n3";
2-
const { quad, namedNode } = DataFactory
2+
const { quad, namedNode, literal } = DataFactory
33

44
/**
55
*
6-
* @property {string} subject - The identifier of the entity that wants to execute an action on a resource (e.g. a {@link https://solid.github.io/webid-profile/ WebID})
7-
* @property {string} action - The type of action(s) that the entity wants to perform on the resource (e.g. a CRUD action)
8-
* @property {string} resource - The resource identifier that is governed by a usage control policy
9-
* @property {string} context - Extra information supplied (can be the purpose of use, extra claims, ...) | Note: currently not implemented yet
10-
* @property {string} owner - The owner/providerof the resource (e.g. a {@link https://solid.github.io/webid-profile/ WebID})
6+
* @property subject - The identifier of the entity that wants to execute an action on a resource (e.g. a {@link https://solid.github.io/webid-profile/ WebID})
7+
* @property action - The type of action(s) that the entity wants to perform on the resource (e.g. a CRUD action)
8+
* @property resource - The resource identifier that is governed by a usage control policy
9+
* @property claims - Extra information supplied (can be the purpose of use, extra claims, ...)
10+
* @property owner - The owner/providerof the resource (e.g. a {@link https://solid.github.io/webid-profile/ WebID})
1111
*/
1212
export interface UconRequest {
1313
subject: string;
1414
action: string[];
1515
resource: string;
16-
context?: string;
17-
owner?: string
16+
owner?: string;
17+
claims?: NodeJS.Dict<unknown>;
1818
}
19+
1920
/**
2021
* Creates an N3 Store based on the context of an UMA Access Request.
2122
* Currently, the access request also contain ACL access modes.
2223
* @param context
2324
*/
24-
2525
export function createContext(request: UconRequest): Store {
2626
const contextStore = new Store();
27-
const { owner, subject: requestingParty, action: requestedAccessModes, resource } = request;
27+
const { owner, subject: requestingParty, action: requestedAccessModes, resource, claims } = request;
2828
const contextIRI = 'http://example.org/context';
2929
contextStore.addQuads([
3030
quad(namedNode(contextIRI), namedNode('http://example.org/requestingParty'), namedNode(requestingParty)),
@@ -37,5 +37,17 @@ export function createContext(request: UconRequest): Store {
3737
for (const accessMode of requestedAccessModes) {
3838
contextStore.addQuad(namedNode(contextIRI), namedNode('http://example.org/requestPermission'), namedNode(accessMode));
3939
}
40+
41+
for (const [claim, value] of Object.entries(claims ?? {})) {
42+
if (typeof value !== 'string') {
43+
console.log(`[Request.createContext]: Skipping claim ${claim} because only string values are supported.`);
44+
continue; // TODO: support full RDF
45+
}
46+
47+
let object;
48+
try { object = namedNode(value) } catch { object = literal(value) }
49+
contextStore.addQuad(namedNode(contextIRI), namedNode(claim), object);
50+
}
51+
4052
return contextStore;
4153
}

0 commit comments

Comments
 (0)