Skip to content

Commit 3236a1f

Browse files
committed
feat: Generate partial trust envelopes when possible
1 parent 1e6e6f4 commit 3236a1f

4 files changed

Lines changed: 263 additions & 1 deletion

File tree

packages/css/config/uma/overrides/authorization-handler.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,21 @@
1414
"overrideParameters": {
1515
"@type": "ParsingHttpHandler",
1616
"operationHandler": {
17+
"@id": "urn:uma:default:AuthorizingHttpHandler",
1718
"@type": "AuthorizingHttpHandler",
1819
"credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
1920
"modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
2021
"permissionReader": { "@id": "urn:solid-server:default:PermissionReader" },
2122
"authorizer": { "@id": "urn:solid-server:default:Authorizer" },
22-
"operationHandler": { "@id": "urn:solid-server:default:OperationHandler" }
23+
"operationHandler": {
24+
"@id": "urn:uma:default:TrustEnvelopeHttpHandler",
25+
"@type": "TrustEnvelopeHttpHandler",
26+
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
27+
"operationHandler": { "@id": "urn:solid-server:default:OperationHandler" },
28+
"umaClient": { "@id": "urn:solid-server:default:UmaClient" },
29+
"ownerUtil": { "@id": "urn:solid-server:default:OwnerUtil" },
30+
"fetcher": { "@id": "urn:solid-server:default:UmaFetcher" }
31+
}
2332
}
2433
}
2534
}

packages/css/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export * from './init/UmaSeededAccountInitializer';
1515

1616
export * from './server/middleware/JwksHandler';
1717

18+
export * from './server/TrustEnvelopeHttpHandler';
19+
1820
export * from './uma/ResourceRegistrar';
1921
export * from './uma/UmaClient';
2022

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import {
2+
asyncToArray,
3+
BasicRepresentation,
4+
getLoggerFor,
5+
guardedStreamFrom,
6+
INTERNAL_QUADS,
7+
OperationHttpHandler,
8+
OperationHttpHandlerInput,
9+
PREFERRED_PREFIX_TERM,
10+
RDF,
11+
RepresentationConverter,
12+
ResponseDescription,
13+
SOLID_META,
14+
XSD
15+
} from '@solid/community-server';
16+
import { DataFactory as DF, Literal, Quad } from 'n3';
17+
import { UmaClaims, UmaClient } from '../uma/UmaClient';
18+
import type { Fetcher } from '../util/fetch/Fetcher';
19+
import { OwnerUtil } from '../util/OwnerUtil';
20+
import { DCTERMS, DPV, ODRL, TE } from '../util/Vocabularies';
21+
22+
/**
23+
* A handler that wraps the outgoing data in a trust envelope,
24+
* based on the contents of the UMA access token.
25+
* As the trust envelope is combined with the data response,
26+
* this only works with RDF response data.
27+
* It is also assumed the identifier of the resource is the main subject for the RDF response.
28+
*
29+
* As this class needs to have access to both authorization data and response data,
30+
* it is situated before the RepresentationStore stack.
31+
* This complicates the situation a bit as this means data conversion
32+
* can no longer be handled by the RepresentationStore.
33+
* An alternative solution where this is not the case,
34+
* would require the authorization data to somehow get passed along to the RepresentationStore request.
35+
* For example, as ephemeral metadata.
36+
* The issue there is that it might create issues that this metadata is mixed with the data,
37+
* which could be an example for PATCH, for example.
38+
*
39+
* Many assumptions are still made in the code.
40+
*/
41+
export class TrustEnvelopeHttpHandler extends OperationHttpHandler {
42+
protected readonly logger = getLoggerFor(this);
43+
44+
public constructor(
45+
protected readonly converter: RepresentationConverter,
46+
protected readonly operationHandler: OperationHttpHandler,
47+
protected readonly umaClient: UmaClient,
48+
protected readonly ownerUtil: OwnerUtil,
49+
protected readonly fetcher: Fetcher,
50+
) {
51+
super();
52+
}
53+
54+
public async canHandle(operation: OperationHttpHandlerInput): Promise<void> {
55+
return this.operationHandler.canHandle(operation);
56+
}
57+
58+
public async handle(input: OperationHttpHandlerInput): Promise<ResponseDescription> {
59+
if (input.operation.method !== 'GET') {
60+
return this.operationHandler.handle(input);
61+
}
62+
63+
// TODO: simultaneous promises for speedup
64+
const response = await this.operationHandler.handle(input);
65+
66+
const conversionArgs = {
67+
preferences: { type: {[INTERNAL_QUADS]: 1}},
68+
identifier: input.operation.target,
69+
representation: new BasicRepresentation(response.data!, response.metadata!),
70+
}
71+
try {
72+
await this.converter.canHandle(conversionArgs);
73+
} catch {
74+
return response;
75+
}
76+
77+
let owners: string[];
78+
let verified: UmaClaims | undefined;
79+
try {
80+
owners = await this.ownerUtil.findOwners(input.operation.target);
81+
verified = await this.introspect(input, owners);
82+
} catch {
83+
return response;
84+
}
85+
86+
if (!verified) {
87+
return response;
88+
}
89+
90+
const envelopeQuads = this.generateEnvelopeData(verified, owners);
91+
92+
const quadData = await this.converter.handle(conversionArgs);
93+
94+
// TODO: inefficient stream merging
95+
const mergedData = [
96+
...await asyncToArray(quadData.data),
97+
...envelopeQuads,
98+
];
99+
quadData.data = guardedStreamFrom(mergedData);
100+
101+
quadData.metadata.addQuad(DCTERMS.terms.namespace, PREFERRED_PREFIX_TERM, 'dcterms', SOLID_META.terms.ResponseMetadata);
102+
quadData.metadata.addQuad(DPV.terms.namespace, PREFERRED_PREFIX_TERM, 'dpv', SOLID_META.terms.ResponseMetadata);
103+
quadData.metadata.addQuad('http://example.com/ns/', PREFERRED_PREFIX_TERM, 'ex', SOLID_META.terms.ResponseMetadata);
104+
quadData.metadata.addQuad(ODRL.terms.namespace, PREFERRED_PREFIX_TERM, 'odrl', SOLID_META.terms.ResponseMetadata);
105+
quadData.metadata.addQuad(TE.terms.namespace, PREFERRED_PREFIX_TERM, 'te', SOLID_META.terms.ResponseMetadata);
106+
107+
const preferredType = input.operation.preferences.type ?? {[response.metadata!.contentType!]: 1};
108+
const originalType = await this.converter.handleSafe({
109+
preferences: { type: preferredType },
110+
identifier: input.operation.target,
111+
representation: quadData,
112+
});
113+
114+
return {
115+
data: originalType.data,
116+
metadata: originalType.metadata,
117+
statusCode: response.statusCode,
118+
};
119+
}
120+
121+
protected async introspect(input: OperationHttpHandlerInput, owners: string[]) {
122+
const authorization = input.request.headers.authorization;
123+
if (!authorization || !authorization.match(/.+ .+/)) {
124+
return;
125+
}
126+
const token = authorization.split(' ')[1];
127+
console.log('TOKEN', token, owners);
128+
if (owners.length === 0) {
129+
return;
130+
}
131+
const issuer = await this.ownerUtil.findIssuer(owners[0]);
132+
if (!issuer) {
133+
return;
134+
}
135+
136+
// TODO: the UMA client `verifyOpaqueToken` seems to combine two different conflicting ideas,
137+
// so just performing introspection here
138+
const config = await this.umaClient.fetchUmaConfig(issuer);
139+
const res = await this.fetcher.fetch(config.introspection_endpoint, {
140+
method: 'POST',
141+
headers: {
142+
'Content-Type': 'application/x-www-form-urlencoded',
143+
'Accept': 'application/json',
144+
},
145+
body: `token_type_hint=access_token&token=${token}`,
146+
});
147+
148+
if (res.status >= 400) {
149+
throw new Error(`Unable to introspect UMA RPT for Authorization Server '${config.issuer}'`);
150+
}
151+
152+
return res.json();
153+
}
154+
155+
protected generateEnvelopeData(verified: UmaClaims, owners?: string[]): Quad[] {
156+
const envelope = DF.namedNode('http://example.com/ns/envelope');
157+
const dataProv = DF.namedNode('http://example.com/ns/dataProvenance');
158+
const policyProv = DF.namedNode('http://example.com/ns/policyProvenance');
159+
160+
const signedEnvelope = DF.namedNode('http://example.com/ns/signedEnvelope');
161+
const signedDataProv = DF.namedNode('http://example.com/ns/signedDataProvenance');
162+
const signedPolicyProv = DF.namedNode('http://example.com/ns/signedPolicyProvenance');
163+
164+
const quads: Quad[] = [];
165+
166+
let date: Literal | undefined;
167+
if (verified.iat) {
168+
date = DF.literal(new Date(verified.iat).toISOString(), XSD.terms.dateTime);
169+
}
170+
171+
quads.push(
172+
DF.quad(envelope, RDF.terms.type, TE.terms.TrustEnvelope),
173+
DF.quad(envelope, TE.terms.provenance, dataProv),
174+
DF.quad(envelope, TE.terms.provenance, policyProv),
175+
DF.quad(envelope, TE.terms.sign, signedEnvelope),
176+
DF.quad(dataProv, RDF.terms.type, TE.terms.DataProvenance),
177+
DF.quad(dataProv, TE.terms.sign, signedDataProv),
178+
DF.quad(policyProv, RDF.terms.type, TE.terms.PolicyProvenance),
179+
DF.quad(policyProv, TE.terms.sign, signedPolicyProv),
180+
);
181+
182+
if (date) {
183+
quads.push(
184+
DF.quad(envelope, DCTERMS.terms.issued, date),
185+
DF.quad(dataProv, DCTERMS.terms.issued, date),
186+
DF.quad(policyProv, DCTERMS.terms.issued, date),
187+
);
188+
}
189+
if (verified.iss) {
190+
quads.push(DF.quad(dataProv, TE.terms.sender, DF.namedNode(verified.iss)))
191+
}
192+
for (const owner of owners ?? []) {
193+
quads.push(DF.quad(policyProv, TE.terms.rightsHolder, DF.namedNode(owner)));
194+
}
195+
for (const permission of verified.permissions ?? []) {
196+
// TODO: these need to linked together in a more correct way
197+
quads.push(
198+
// TODO: this is the UMA ID, need to convert to local ID
199+
DF.quad(envelope, DPV.terms.hasData, DF.namedNode(permission.resource_id)),
200+
DF.quad(dataProv, DPV.terms.hasDataSubject, DF.namedNode(permission.resource_id)),
201+
);
202+
for (const policy of permission.policies ?? []) {
203+
quads.push(
204+
DF.quad(envelope, ODRL.terms.hasPolicy, DF.namedNode(policy)),
205+
);
206+
}
207+
}
208+
209+
if (verified.requestClaims) {
210+
const webId = verified.requestClaims['urn:solidlab:uma:claims:types:webid'];
211+
const purpose = verified.requestClaims['http://www.w3.org/ns/odrl/2/purpose'];
212+
if (typeof webId === 'string') {
213+
quads.push(DF.quad(policyProv, TE.terms.recipient, DF.namedNode(webId)));
214+
}
215+
if (typeof purpose === 'string') {
216+
// TODO: purpose not actually part of trust envelope, just dumping this in here for now
217+
quads.push(DF.quad(policyProv, DF.namedNode(TE.namespace + 'TODO-purpose'), DF.namedNode(purpose)));
218+
}
219+
}
220+
221+
return quads;
222+
}
223+
}

packages/css/src/util/Vocabularies.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
import { createVocabulary } from '@solid/community-server';
22

3+
export const DCTERMS = createVocabulary(
4+
'http://purl.org/dc/terms/',
5+
'issued'
6+
);
7+
8+
export const DPV = createVocabulary(
9+
'https://w3id.org/dpv#',
10+
'hasData',
11+
'hasDataSubject',
12+
);
13+
14+
export const ODRL = createVocabulary(
15+
'http://www.w3.org/ns/odrl/2/',
16+
'hasPolicy'
17+
);
18+
19+
export const TE = createVocabulary(
20+
'https://w3id.org/trustenvelope#',
21+
'DataProvenance',
22+
'PolicyProvenance',
23+
'TrustEnvelope',
24+
'provenance',
25+
'recipient',
26+
'rightsHolder',
27+
'sender',
28+
'sign',
29+
);
30+
331
export const UMA = createVocabulary('http://www.w3.org/ns/solid/uma#',
432
// 'userMode',
533
// 'publicMode',

0 commit comments

Comments
 (0)