diff --git a/packages/uma/config/routes/resources.json b/packages/uma/config/routes/resources.json index 51feb14..2256577 100644 --- a/packages/uma/config/routes/resources.json +++ b/packages/uma/config/routes/resources.json @@ -15,14 +15,14 @@ { "@id": "urn:uma:default:ResourceRegistrationRoute", "@type": "HttpHandlerRoute", - "methods": [ "POST" ], + "methods": [ "GET", "POST" ], "handler": { "@id": "urn:uma:default:ResourceRegistrationHandler" }, "path": "/uma/resources/" }, { "@id": "urn:uma:default:ResourceRegistrationOpsRoute", "@type": "HttpHandlerRoute", - "methods": [ "PUT", "DELETE" ], + "methods": [ "GET", "PUT", "DELETE" ], "handler": { "@id": "urn:uma:default:ResourceRegistrationHandler" }, "path": "/uma/resources/{id}" } diff --git a/packages/uma/src/routes/ResourceRegistration.ts b/packages/uma/src/routes/ResourceRegistration.ts index fcb2601..e3c4dd9 100644 --- a/packages/uma/src/routes/ResourceRegistration.ts +++ b/packages/uma/src/routes/ResourceRegistration.ts @@ -61,6 +61,7 @@ export class ResourceRegistrationRequestHandler extends HttpHandler { const { owner } = await this.validator.handleSafe({ request }); switch (request.method) { + case 'GET': return this.handleGet(request, owner); case 'POST': return this.handlePost(request, owner); case 'PUT': return this.handlePut(request, owner); case 'DELETE': return this.handleDelete(request, owner); @@ -68,6 +69,24 @@ export class ResourceRegistrationRequestHandler extends HttpHandler { } } + protected async handleGet(request: HttpHandlerRequest, owner: string): Promise { + const id = request.parameters?.id; + if (id) { + const registration = await this.registrationStore.get(id); + if (!registration) { + throw new NotFoundHttpError(); + } + if (registration.owner !== owner) { + throw new ForbiddenHttpError() + } + return { status: 200, body: registration.description }; + } + + // No ID so return the list of all owned resources + const identifiers = await this.ownershipStore.get(owner) ?? []; + return { status: 200, body: identifiers }; + } + protected async handlePost(request: HttpHandlerRequest, owner: string): Promise { const { body } = request; diff --git a/packages/uma/test/unit/routes/ResourceRegistration.test.ts b/packages/uma/test/unit/routes/ResourceRegistration.test.ts index fa215ca..f3ea388 100644 --- a/packages/uma/test/unit/routes/ResourceRegistration.test.ts +++ b/packages/uma/test/unit/routes/ResourceRegistration.test.ts @@ -23,6 +23,7 @@ vi.mock('node:crypto', () => ({ describe('ResourceRegistration', (): void => { const owner = 'owner'; + const resource = 'http://example.com/resource'; let input: HttpHandlerContext; let policyStore: Store; @@ -61,7 +62,7 @@ describe('ResourceRegistration', (): void => { } satisfies Partial> as any; ownershipStore = { - get: vi.fn().mockResolvedValue([]), + get: vi.fn().mockResolvedValue([ resource ]), set: vi.fn(), delete: vi.fn(), } satisfies Partial> as any; @@ -80,9 +81,38 @@ describe('ResourceRegistration', (): void => { }); it('throws an error if the method is not allowed.', async(): Promise => { + input.request.method = 'PATCH'; await expect(handler.handle(input)).rejects.toThrow(MethodNotAllowedHttpError); }); + describe('with GET requests', (): void => { + it('can return a list of owned resource identifiers.', async(): Promise => { + await expect(handler.handle(input)).resolves.toEqual({ status: 200, body: [ resource ] }); + expect(ownershipStore.get).toHaveBeenCalledExactlyOnceWith(owner); + }); + + it('can return the details of a single resource.', async(): Promise => { + input.request.parameters = { id: resource }; + await expect(handler.handle(input)).resolves + .toEqual({ status: 200, body: input.request.body }); + expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource); + }); + + it('returns a 404 for unknown resource identifiers.', async(): Promise => { + input.request.parameters = { id: resource }; + registrationStore.get.mockResolvedValueOnce(undefined); + await expect(handler.handle(input)).rejects.toThrow(NotFoundHttpError); + expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource); + }); + + it('returns a 403 if the user is not the actual owner.', async(): Promise => { + input.request.parameters = { id: resource }; + registrationStore.get.mockResolvedValueOnce({ owner: 'someone else' } as any); + await expect(handler.handle(input)).rejects.toThrow(ForbiddenHttpError); + expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource); + }); + }); + describe('with POST requests', (): void => { beforeEach(async(): Promise => { input.request.method = 'POST'; diff --git a/test/integration/Aggregation.test.ts b/test/integration/Aggregation.test.ts index be24600..80c5f1b 100644 --- a/test/integration/Aggregation.test.ts +++ b/test/integration/Aggregation.test.ts @@ -273,7 +273,7 @@ describe('An aggregation setup', (): void => { // Update registration with derivation ID const description: ResourceDescription = { name: `http://localhost:${aggregatorPort}/resource`, - resource_scopes: [ 'read' ], + resource_scopes: [ 'http://www.w3.org/ns/odrl/2/read' ], derived_from: [{ issuer: srcConfig.issuer, derivation_resource_id: derivationId, @@ -292,6 +292,28 @@ describe('An aggregation setup', (): void => { expect(response.status).toBe(200); }); + it('can read the resource registration on the AS.', async(): Promise => { + let response = await fetch(aggConfig.resource_registration_endpoint, { + headers: { authorization: pat } + }); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual([aggregatedResourceId]); + + const url = joinUrl(aggConfig.resource_registration_endpoint, encodeURIComponent(aggregatedResourceId)); + response = await fetch(url, { + headers: { authorization: pat }, + }); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + name: `http://localhost:${aggregatorPort}/resource`, + resource_scopes: [ 'http://www.w3.org/ns/odrl/2/read' ], + derived_from: [{ + issuer: srcConfig.issuer, + derivation_resource_id: derivationId, + }], + }); + }); + it('a client cannot read an aggregated resource without the necessary tokens.', async(): Promise => { // We don't have an actual aggregator server so simulating the request const body = [{ diff --git a/test/integration/Base.test.ts b/test/integration/Base.test.ts index d8daee1..7121171 100644 --- a/test/integration/Base.test.ts +++ b/test/integration/Base.test.ts @@ -210,7 +210,6 @@ describe('A server setup', (): void => { headers: { authorization: `WebID ${encodeURIComponent(owner)}`, 'content-type': 'text/turtle' }, body: policy, }); - console.log(await policyResponse.text()); expect(policyResponse.status).toBe(201); const putResponse = await fetch(collectionResource, { @@ -247,7 +246,6 @@ describe('A server setup', (): void => { headers: { authorization: `WebID ${encodeURIComponent(owner)}`, 'content-type': 'text/turtle' }, body: policy, }); - console.log(await policyResponse.text()); expect(policyResponse.status).toBe(201); const putResponse = await fetch(collectionResource, { @@ -255,7 +253,6 @@ describe('A server setup', (): void => { headers: { 'content-type': 'text/plain' }, body: 'Some new text!', }); - console.log(await putResponse.text()); expect(putResponse.status).toBe(205); const getResponse = await fetch(collectionResource);