Skip to content

Commit

Permalink
chore: merge part of resource registrar in uma client (#34)
Browse files Browse the repository at this point in the history
Signed-off-by: Wouter Termont <[email protected]>
  • Loading branch information
termontwouter authored Mar 12, 2024
1 parent a90d9be commit 7e7a6c7
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 89 deletions.
8 changes: 4 additions & 4 deletions packages/css/config/uma/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
"baseUrl": {
"@id": "urn:solid-server:default:variable:baseUrl"
},
"umaIdStore": {
"@id": "urn:solid-server:default:UmaIdStore",
"@type": "MemoryMapStorage"
},
"keyGen": {
"@id": "urn:solid-server:default:JwkGenerator"
},
Expand Down Expand Up @@ -73,10 +77,6 @@
"store": {
"@id": "urn:solid-server:default:ResourceStore"
},
"umaIdStore": {
"@id": "urn:solid-server:default:UmaIdStore",
"@type": "MemoryMapStorage"
},
"ownerUtil": {
"@id": "urn:solid-server:default:OwnerUtil"
},
Expand Down
93 changes: 11 additions & 82 deletions packages/css/src/uma/ResourceRegistrar.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,38 @@
import type { ResourceIdentifier, MonitoringStore, KeyValueStorage } from '@solid/community-server';
import type { ResourceIdentifier, MonitoringStore } from '@solid/community-server';
import { AS, getLoggerFor, StaticHandler } from '@solid/community-server';
import { OwnerUtil } from '../util/OwnerUtil';
import { ResourceDescription } from '@solidlab/uma';
import type { UmaClient } from '../uma/UmaClient';

export class ResourceRegistrar extends StaticHandler {
protected readonly logger = getLoggerFor(this);

public constructor(
protected store: MonitoringStore,
protected umaIdStore: KeyValueStorage<string, string>,
protected ownerUtil: OwnerUtil,
protected umaClient: UmaClient,
) {
super();

store.on(AS.Create, async (resource: ResourceIdentifier): Promise<void> => {
const owners = await this.ownerUtil.findOwners(resource).catch(() => []);
for (const owner of owners) this.createResource(resource, owner);
for (const owner of await this.findOwners(resource)) {
this.umaClient.createResource(resource, await this.findIssuer(owner));
}
});

store.on(AS.Delete, async (resource: ResourceIdentifier): Promise<void> => {
const owners = await this.ownerUtil.findOwners(resource).catch(() => []);
for (const owner of owners) this.deleteResource(resource, owner);
for (const owner of await this.findOwners(resource)) {
this.umaClient.deleteResource(resource, await this.findIssuer(owner));
}
});
}

protected async createResource(resource: ResourceIdentifier, owner: string): Promise<void> {
const issuer = await this.ownerUtil.findIssuer(owner);

if (!issuer) throw new Error(`Could not find UMA AS for resource owner ${owner}`);

const { resource_registration_endpoint: endpoint } = await this.umaClient.fetchUmaConfig(issuer);

const description: ResourceDescription = {
resource_scopes: [
'urn:example:css:modes:read',
'urn:example:css:modes:append',
'urn:example:css:modes:create',
'urn:example:css:modes:delete',
'urn:example:css:modes:write',
]
};

this.logger.info(`Creating resource registration for <${resource.path}> at <${endpoint}>`);

const request = {
url: endpoint,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(description),
};

// do not await - registration happens in background to cope with errors etc.
this.umaClient.signedFetch(endpoint, request).then(async resp => {
if (resp.status !== 201) {
throw new Error (`Resource registration request failed. ${await resp.text()}`);
}

const { _id: umaId } = await resp.json();

if (!umaId || typeof umaId !== 'string') {
throw new Error ('Unexpected response from UMA server; no UMA id received.');
}

this.umaIdStore.set(resource.path, umaId);
}).catch(error => {
// TODO: Do something useful on error
this.logger.warn(
`Something went wrong during UMA resource registration to create ${resource.path}: ${(error as Error).message}`
);
});
private async findOwners(resource: ResourceIdentifier): Promise<string[]> {
return await this.ownerUtil.findOwners(resource).catch(() => []);
}

protected async deleteResource(resource: ResourceIdentifier, owner: string): Promise<void> {
private async findIssuer(owner: string): Promise<string> {
const issuer = await this.ownerUtil.findIssuer(owner);

if (!issuer) throw new Error(`Could not find UMA AS for resource owner ${owner}`);

const { resource_registration_endpoint: endpoint } = await this.umaClient.fetchUmaConfig(issuer);

this.logger.info(`Deleting resource registration for <${resource.path}> at <${endpoint}>`);

const umaId = await this.umaIdStore.get(resource.path);
const url = `${endpoint}/${umaId}`;

const request = {
url,
method: 'DELETE',
headers: {}
};

// do not await - registration happens in background to cope with errors etc.
this.umaClient.signedFetch(endpoint, request).then(async _resp => {
if (!umaId) throw new Error('Trying to delete unknown/unregistered resource; no UMA id found.');

await this.umaClient.signedFetch(url, request);
}).catch(error => {
// TODO: Do something useful on error
this.logger.warn(
`Something went wrong during UMA resource registration to delete ${resource.path}: ${(error as Error).message}`
);
});
return issuer;
}
}
84 changes: 81 additions & 3 deletions packages/css/src/uma/UmaClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { AccessMap, getLoggerFor, InternalServerError, JwkGenerator } from "@solid/community-server";
import type { KeyValueStorage, Representation, ResourceIdentifier } from "@solid/community-server";
import { AccessMap, getLoggerFor, InternalServerError, JwkGenerator, NotFoundHttpError } from "@solid/community-server";
import { JWTPayload, decodeJwt, createRemoteJWKSet, jwtVerify, JWTVerifyOptions } from "jose";
import { httpbis, type SigningKey, type Request as SignRequest } from 'http-message-signatures';
import { isString } from '../util/StringGuard';
import fetch from 'cross-fetch';
import type { Fetcher } from "../util/fetch/Fetcher";
import crypto from 'node:crypto';
import type { ResourceDescription } from "@solidlab/uma";

export interface Claims {
[key: string]: unknown;
Expand Down Expand Up @@ -70,8 +72,9 @@ export class UmaClient {
*/
constructor(
protected baseUrl: string,
protected keyGen: JwkGenerator,
protected fetcher: Fetcher,
protected keyGen: JwkGenerator,
protected umaIdStore: KeyValueStorage<string, string>,
protected options: UmaVerificationOptions = {},
) {}

Expand Down Expand Up @@ -118,8 +121,10 @@ export class UmaClient {

const body = [];
for (const [ target, modes ] of permissions.entrySets()) {
// const umaId = await this.umaIdStore.get(target.path);
// if (!umaId) throw new NotFoundHttpError();
body.push({
resource_id: target.path,
resource_id: target.path, // TODO: map to umaId ? (but raises problems on creation, discovery ...)
resource_scopes: Array.from(modes).map(mode => `urn:example:css:modes:${mode}`)
});
}
Expand Down Expand Up @@ -259,4 +264,77 @@ export class UmaClient {

return configuration;
}

public async createResource(resource: ResourceIdentifier, issuer: string): Promise<void> {
const { resource_registration_endpoint: endpoint } = await this.fetchUmaConfig(issuer);

const description: ResourceDescription = {
resource_scopes: [
'urn:example:css:modes:read',
'urn:example:css:modes:append',
'urn:example:css:modes:create',
'urn:example:css:modes:delete',
'urn:example:css:modes:write',
]
};

this.logger.info(`Creating resource registration for <${resource.path}> at <${endpoint}>`);

const request = {
url: endpoint,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(description),
};

// do not await - registration happens in background to cope with errors etc.
this.signedFetch(endpoint, request).then(async resp => {
if (resp.status !== 201) {
throw new Error (`Resource registration request failed. ${await resp.text()}`);
}

const { _id: umaId } = await resp.json();

if (!umaId || typeof umaId !== 'string') {
throw new Error ('Unexpected response from UMA server; no UMA id received.');
}

this.umaIdStore.set(resource.path, umaId);
}).catch(error => {
// TODO: Do something useful on error
this.logger.warn(
`Something went wrong during UMA resource registration to create ${resource.path}: ${(error as Error).message}`
);
});
}

public async deleteResource(resource: ResourceIdentifier, issuer: string): Promise<void> {
const { resource_registration_endpoint: endpoint } = await this.fetchUmaConfig(issuer);

this.logger.info(`Deleting resource registration for <${resource.path}> at <${endpoint}>`);

const umaId = await this.umaIdStore.get(resource.path);
const url = `${endpoint}/${umaId}`;

const request = {
url,
method: 'DELETE',
headers: {}
};

// do not await - registration happens in background to cope with errors etc.
this.signedFetch(endpoint, request).then(async _resp => {
if (!umaId) throw new Error('Trying to delete unknown/unregistered resource; no UMA id found.');

await this.signedFetch(url, request);
}).catch(error => {
// TODO: Do something useful on error
this.logger.warn(
`Something went wrong during UMA resource registration to delete ${resource.path}: ${(error as Error).message}`
);
});
}
}

0 comments on commit 7e7a6c7

Please sign in to comment.