Skip to content

Commit 40accde

Browse files
committed
feat: Support any kind of constraint
1 parent 350a1da commit 40accde

6 files changed

Lines changed: 61 additions & 73 deletions

File tree

packages/uma/src/policies/authorizers/OdrlAuthorizer.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PrioritizeProhibitionStrategy } from '../../ucp/policy/PrioritizeProhib
99
import { Strategy } from '../../ucp/policy/Strategy';
1010
import { UCPPolicy } from '../../ucp/policy/UsageControlPolicy';
1111
import { UCRulesStorage } from '../../ucp/storage/UCRulesStorage';
12+
import { isIri } from '../../util/ConvertUtil';
1213
import { Permission } from '../../views/Permission';
1314
import { Authorizer } from './Authorizer';
1415

@@ -76,9 +77,9 @@ export class OdrlAuthorizer implements Authorizer {
7677

7778
const subject = typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous';
7879
const claimContextConstraints: { subject: ReturnType<typeof blankNode>; quads: Quad[] }[] = [];
79-
for (const [ claimKey, leftOperand ] of Object.entries(claimOperandMap)) {
80-
const claimValue = claims[claimKey];
81-
if (typeof claimValue !== 'string') {
80+
for (const [ key, value ] of Object.entries(claims)) {
81+
const leftOperand = claimOperandMap[key] ?? key;
82+
if (!isIri(leftOperand) || typeof value !== 'string') {
8283
continue;
8384
}
8485
const claimSubject = blankNode();
@@ -88,7 +89,7 @@ export class OdrlAuthorizer implements Authorizer {
8889
quad(claimSubject, RDF.terms.type, ODRL.terms.Constraint),
8990
quad(claimSubject, ODRL.terms.leftOperand, namedNode(leftOperand)),
9091
quad(claimSubject, ODRL.terms.operator, ODRL.terms.eq),
91-
quad(claimSubject, ODRL.terms.rightOperand, namedNode(claimValue)),
92+
quad(claimSubject, ODRL.terms.rightOperand, namedNode(value)),
9293
],
9394
});
9495
}

packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const dateComparators: NodeJS.Dict<(a: Date, b: Date) => boolean> = {
2929

3030
const claimOperandMap: Record<string, string> = {
3131
[ODRL.deliveryChannel]: CLIENTID,
32-
[ODRL.purpose]: PURPOSE
3332
} as const;
3433

3534
/**
@@ -168,6 +167,8 @@ export class SimpleOdrlAuthorizer implements Authorizer {
168167
if (constraints.some(({ leftOperand, operator, rightOperand }) => !leftOperand || !operator || !rightOperand)) {
169168
return;
170169
}
170+
// TODO: would want middleware step where credentials and other stuff are already extracted into RDF values
171+
// so both ODRL authorizers don't have to bother with this
171172
for (const constraint of constraints) {
172173
// Return undefined if any of these are too complex or unknown
173174
if (constraint.leftOperand.equals(ODRL.terms.dateTime)) {
@@ -188,6 +189,11 @@ export class SimpleOdrlAuthorizer implements Authorizer {
188189
if (typeof claimValue !== 'string' || constraint.rightOperand.value !== claimValue) {
189190
return false;
190191
}
192+
} else if (typeof claims[constraint.leftOperand.value] === 'string'
193+
&& constraint.operator.equals(ODRL.terms.eq)) {
194+
if (constraint.rightOperand.value !== claims[constraint.leftOperand.value]) {
195+
return false;
196+
}
191197
} else {
192198
// Unsupported constraint
193199
return;

packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ describe('OdrlAuthorizer', (): void => {
9494
expect(evaluate).toHaveBeenCalledTimes(1);
9595
expect(evaluate).toHaveBeenLastCalledWith(
9696
policyStore.getQuads(null, null, null, null),
97-
[ ...new Store(requestQuads) ],
97+
// Also contains sotw data generated by claims being present
98+
expect.any(Array),
9899
sotw,
99100
);
100101
});
@@ -123,7 +124,7 @@ describe('OdrlAuthorizer', (): void => {
123124
]);
124125
});
125126

126-
it('adds purpose claim context using odrl:purpose', async(): Promise<void> => {
127+
it('adds other claims to constraints', async(): Promise<void> => {
127128
const claims = { [PURPOSE]: 'https://w3id.org/dpv#ScientificResearch' };
128129
const query: Permission[] = [{ resource_id: 'rid', resource_scopes: [ 'urn:example:css:modes:read' ] }];
129130

packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe('SimpleOdrlAuthorizer', () => {
166166
expect(fallback.permissions).not.toHaveBeenCalled();
167167
});
168168

169-
it('returns permission if purpose constraint is satisfied', async () => {
169+
it('returns permission if other constraints are satisfied', async () => {
170170
const rule = addRule({});
171171
addConstraint({
172172
rule,
@@ -182,7 +182,7 @@ describe('SimpleOdrlAuthorizer', () => {
182182
expect(fallback.permissions).not.toHaveBeenCalled();
183183
});
184184

185-
it('returns empty if claim constraint is not satisfied', async () => {
185+
it('returns empty if other constraints are not satisfied', async () => {
186186
const rule = addRule({});
187187
addConstraint({
188188
rule,

test/integration/Demo.test.ts

Lines changed: 44 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { App } from '@solid/community-server';
22
import { setGlobalLoggerFactory, WinstonLoggerFactory } from 'global-logger-factory';
3-
import { Parser, Store } from 'n3';
3+
import { UnsecuredJWT } from 'jose';
44
import * as path from 'node:path';
55
import { getDefaultCssVariables, getPorts, instantiateFromConfig } from '../util/ServerUtil';
6-
import { findTokenEndpoint, getToken, noTokenFetch, generateCredentials, tokenFetch, umaFetch } from '../util/UmaUtil';
6+
import { findTokenEndpoint, generateCredentials, getToken, noTokenFetch, tokenFetch, umaFetch } from '../util/UmaUtil';
77

88
const [ cssPort, umaPort ] = getPorts('Demo');
99

@@ -142,69 +142,40 @@ _:rename a solid:InsertDeletePatch;
142142
}.`,
143143
}, terms.agents.ruben);
144144
expect(response.status).toBe(205);
145-
146-
// TODO: Do I need this though
147-
// Add necessary triples to WebID
148-
response = await fetch(terms.agents.ruben, {
149-
method: 'PATCH',
150-
headers: { 'content-type': 'text/n3' },
151-
body: `
152-
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
153-
154-
_:rename a solid:InsertDeletePatch;
155-
solid:inserts {
156-
<${terms.agents.ruben}> solid:umaServer <http://localhost:${umaPort}/uma/>
157-
}.`,
158-
});
159-
expect(response.status).toBe(205);
160-
});
161-
162-
it('finds the UMA server of the user in their WebID.', async(): Promise<void> => {
163-
// TODO: what is the point of any of this? the as_uri response should have this data?
164-
// TODO: find out why it doesn't work though as the term does get added at the end of the previous test
165-
const response = await fetch(terms.agents.ruben, {
166-
headers: { 'accept': 'text/turtle' },
167-
});
168-
expect(response.status).toBe(200);
169-
const parser = new Parser({ baseIRI: terms.agents.ruben });
170-
const store = new Store(parser.parse(await response.text()));
171-
expect(store.countQuads(terms.agents.ruben, terms.solid.umaServer, null, null)).toBe(1);
172-
const umaServer = store.getObjects(terms.agents.ruben, terms.solid.umaServer, null)[0].value;
173145
});
174146

175147
it('can add a healthcare policy to the server.', async(): Promise<void> => {
176-
// TODO: policy currently not linking to constraints as these need to be added to the ODRL evaluator
177-
// odrl:constraint <http://example.org/HCPX-agreement-permission-purpose>,
178-
// <http://example.org/HCPX-agreement-permission-lb> .
179148
const healthcare_patient_policy =
180149
`PREFIX dcterms: <http://purl.org/dc/terms/>
181-
PREFIX eu-gdpr: <https://w3id.org/dpv/legal/eu/gdpr#>
182-
PREFIX oac: <https://w3id.org/oac#>
183-
PREFIX odrl: <http://www.w3.org/ns/odrl/2/>
184-
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
185-
186-
PREFIX ex: <http://example.org/>
187-
188-
<http://example.org/HCPX-agreement> a odrl:Agreement ;
189-
odrl:uid ex:HCPX-agreement ;
190-
odrl:profile oac: ;
191-
odrl:permission <http://example.org/HCPX-agreement-permission> .
192-
193-
<http://example.org/HCPX-agreement-permission> a odrl:Permission ;
194-
odrl:action odrl:read ;
195-
odrl:target <${terms.resources.smartwatch}> ;
196-
odrl:assigner <${terms.agents.ruben}> ;
197-
odrl:assignee <${terms.agents.alice}> .
198-
199-
<http://example.org/HCPX-agreement-permission-purpose> a odrl:Constraint ;
200-
odrl:leftOperand odrl:purpose ; # can also be oac:Purpose, to conform with OAC profile
201-
odrl:operator odrl:eq ;
202-
odrl:rightOperand ex:bariatric-care .
203-
204-
<http://example.org/HCPX-agreement-permission-lb> a odrl:Constraint ;
205-
odrl:leftOperand oac:LegalBasis ;
206-
odrl:operator odrl:eq ;
207-
odrl:rightOperand eu-gdpr:A9-2-a .`
150+
PREFIX eu-gdpr: <https://w3id.org/dpv/legal/eu/gdpr#>
151+
PREFIX oac: <https://w3id.org/oac#>
152+
PREFIX odrl: <http://www.w3.org/ns/odrl/2/>
153+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
154+
155+
PREFIX ex: <http://example.org/>
156+
157+
<http://example.org/HCPX-agreement> a odrl:Agreement ;
158+
odrl:uid ex:HCPX-agreement ;
159+
odrl:profile oac: ;
160+
odrl:permission <http://example.org/HCPX-agreement-permission> .
161+
162+
<http://example.org/HCPX-agreement-permission> a odrl:Permission ;
163+
odrl:action odrl:read ;
164+
odrl:target <${terms.resources.smartwatch}> ;
165+
odrl:assigner <${terms.agents.ruben}> ;
166+
odrl:assignee <${terms.agents.alice}> ;
167+
odrl:constraint <http://example.org/HCPX-agreement-permission-purpose>,
168+
<http://example.org/HCPX-agreement-permission-lb> .
169+
170+
<http://example.org/HCPX-agreement-permission-purpose> a odrl:Constraint ;
171+
odrl:leftOperand odrl:purpose ;
172+
odrl:operator odrl:eq ;
173+
odrl:rightOperand ex:bariatric-care .
174+
175+
<http://example.org/HCPX-agreement-permission-lb> a odrl:Constraint ;
176+
odrl:leftOperand oac:LegalBasis ;
177+
odrl:operator odrl:eq ;
178+
odrl:rightOperand eu-gdpr:A9-2-a .`
208179

209180
const medicalPolicyCreationResponse = await fetch(policyContainer, {
210181
method: 'POST',
@@ -215,15 +186,25 @@ PREFIX ex: <http://example.org/>
215186
});
216187

217188
it('requires authorized access for patient data.', async(): Promise<void> => {
218-
// TODO: should do the steps individually here so we can check the contents of the tokens/tickets
219189
// Parse ticket and UMA server URL from header
220190
const parsedHeader = await noTokenFetch(terms.resources.smartwatch);
221191

222192
// Find UMA server token endpoint
223193
const tokenEndpoint = await findTokenEndpoint(parsedHeader.as_uri);
224194

195+
const jwt = new UnsecuredJWT({
196+
'http://www.w3.org/ns/odrl/2/purpose': 'http://example.org/bariatric-care',
197+
'urn:solidlab:uma:claims:types:webid': terms.agents.alice,
198+
'https://w3id.org/oac#LegalBasis': 'https://w3id.org/dpv/legal/eu/gdpr#A9-2-a'
199+
}).encode();
200+
225201
// Send ticket request to UMA server and extract token from response
226-
const token = await getToken(parsedHeader.ticket, tokenEndpoint, terms.agents.alice);
202+
const token = await getToken(
203+
parsedHeader.ticket,
204+
tokenEndpoint,
205+
undefined,
206+
undefined,
207+
[{ claim_token: jwt, claim_token_format: 'urn:solidlab:uma:claims:formats:jwt' }]);
227208
const accessToken = JSON.parse(Buffer.from(token.access_token.split('.')[1], 'base64').toString());
228209
expect(accessToken).toMatchObject({
229210
permissions:[{
@@ -235,7 +216,7 @@ PREFIX ex: <http://example.org/>
235216
aud: 'solid',
236217
exp: expect.any(Number),
237218
jti: expect.any(String),
238-
})
219+
});
239220

240221
// Perform new call with token
241222
const response = await tokenFetch(token, terms.resources.smartwatch);

test/integration/Oidc.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ describe('A server supporting OIDC tokens', (): void => {
216216
});
217217
const endpoint = await findTokenEndpoint(as_uri);
218218

219-
// TODO: also add token that fails
220219
const jwk = await importJWK(privateKey, privateKey.alg);
221220
const jwt = await new SignJWT({ azp: client })
222221
.setSubject(sub)

0 commit comments

Comments
 (0)