Skip to content

Commit c0af5be

Browse files
committed
feat: Support GET on resource registration API
1 parent 844aaec commit c0af5be

4 files changed

Lines changed: 75 additions & 4 deletions

File tree

packages/uma/config/routes/resources.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
{
1616
"@id": "urn:uma:default:ResourceRegistrationRoute",
1717
"@type": "HttpHandlerRoute",
18-
"methods": [ "POST" ],
18+
"methods": [ "GET", "POST" ],
1919
"handler": { "@id": "urn:uma:default:ResourceRegistrationHandler" },
2020
"path": "/uma/resources/"
2121
},
2222
{
2323
"@id": "urn:uma:default:ResourceRegistrationOpsRoute",
2424
"@type": "HttpHandlerRoute",
25-
"methods": [ "PUT", "DELETE" ],
25+
"methods": [ "GET", "PUT", "DELETE" ],
2626
"handler": { "@id": "urn:uma:default:ResourceRegistrationHandler" },
2727
"path": "/uma/resources/{id}"
2828
}

packages/uma/src/routes/ResourceRegistration.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,32 @@ export class ResourceRegistrationRequestHandler extends HttpHandler {
6161
const { owner } = await this.validator.handleSafe({ request });
6262

6363
switch (request.method) {
64+
case 'GET': return this.handleGet(request, owner);
6465
case 'POST': return this.handlePost(request, owner);
6566
case 'PUT': return this.handlePut(request, owner);
6667
case 'DELETE': return this.handleDelete(request, owner);
6768
default: throw new MethodNotAllowedHttpError([ request.method ]);
6869
}
6970
}
7071

72+
protected async handleGet(request: HttpHandlerRequest, owner: string): Promise<HttpHandlerResponse> {
73+
const id = request.parameters?.id;
74+
if (id) {
75+
const registration = await this.registrationStore.get(id);
76+
if (!registration) {
77+
throw new NotFoundHttpError();
78+
}
79+
if (registration.owner !== owner) {
80+
throw new ForbiddenHttpError()
81+
}
82+
return { status: 200, body: registration.description };
83+
}
84+
85+
// No ID so return the list of all owned resources
86+
const identifiers = await this.ownershipStore.get(owner) ?? [];
87+
return { status: 200, body: identifiers };
88+
}
89+
7190
protected async handlePost(request: HttpHandlerRequest, owner: string): Promise<HttpHandlerResponse> {
7291
const { body } = request;
7392

packages/uma/test/unit/routes/ResourceRegistration.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ vi.mock('node:crypto', () => ({
2323

2424
describe('ResourceRegistration', (): void => {
2525
const owner = 'owner';
26+
const resource = 'http://example.com/resource';
2627
let input: HttpHandlerContext<ResourceDescription>;
2728
let policyStore: Store;
2829

@@ -61,7 +62,7 @@ describe('ResourceRegistration', (): void => {
6162
} satisfies Partial<KeyValueStorage<string, ResourceDescription>> as any;
6263

6364
ownershipStore = {
64-
get: vi.fn().mockResolvedValue([]),
65+
get: vi.fn().mockResolvedValue([ resource ]),
6566
set: vi.fn(),
6667
delete: vi.fn(),
6768
} satisfies Partial<KeyValueStorage<string, string[]>> as any;
@@ -80,9 +81,38 @@ describe('ResourceRegistration', (): void => {
8081
});
8182

8283
it('throws an error if the method is not allowed.', async(): Promise<void> => {
84+
input.request.method = 'PATCH';
8385
await expect(handler.handle(input)).rejects.toThrow(MethodNotAllowedHttpError);
8486
});
8587

88+
describe('with GET requests', (): void => {
89+
it('can return a list of owned resource identifiers.', async(): Promise<void> => {
90+
await expect(handler.handle(input)).resolves.toEqual({ status: 200, body: [ resource ] });
91+
expect(ownershipStore.get).toHaveBeenCalledExactlyOnceWith(owner);
92+
});
93+
94+
it('can return the details of a single resource.', async(): Promise<void> => {
95+
input.request.parameters = { id: resource };
96+
await expect(handler.handle(input)).resolves
97+
.toEqual({ status: 200, body: input.request.body });
98+
expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource);
99+
});
100+
101+
it('returns a 404 for unknown resource identifiers.', async(): Promise<void> => {
102+
input.request.parameters = { id: resource };
103+
registrationStore.get.mockResolvedValueOnce(undefined);
104+
await expect(handler.handle(input)).rejects.toThrow(NotFoundHttpError);
105+
expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource);
106+
});
107+
108+
it('returns a 403 if the user is not the actual owner.', async(): Promise<void> => {
109+
input.request.parameters = { id: resource };
110+
registrationStore.get.mockResolvedValueOnce({ owner: 'someone else' } as any);
111+
await expect(handler.handle(input)).rejects.toThrow(ForbiddenHttpError);
112+
expect(registrationStore.get).toHaveBeenCalledExactlyOnceWith(resource);
113+
});
114+
});
115+
86116
describe('with POST requests', (): void => {
87117
beforeEach(async(): Promise<void> => {
88118
input.request.method = 'POST';

test/integration/Aggregation.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ describe('An aggregation setup', (): void => {
273273
// Update registration with derivation ID
274274
const description: ResourceDescription = {
275275
name: `http://localhost:${aggregatorPort}/resource`,
276-
resource_scopes: [ 'read' ],
276+
resource_scopes: [ 'http://www.w3.org/ns/odrl/2/read' ],
277277
derived_from: [{
278278
issuer: srcConfig.issuer,
279279
derivation_resource_id: derivationId,
@@ -292,6 +292,28 @@ describe('An aggregation setup', (): void => {
292292
expect(response.status).toBe(200);
293293
});
294294

295+
it('can read the resource registration on the AS.', async(): Promise<void> => {
296+
let response = await fetch(aggConfig.resource_registration_endpoint, {
297+
headers: { authorization: pat }
298+
});
299+
expect(response.status).toBe(200);
300+
await expect(response.json()).resolves.toEqual([aggregatedResourceId]);
301+
302+
const url = joinUrl(aggConfig.resource_registration_endpoint, encodeURIComponent(aggregatedResourceId));
303+
response = await fetch(url, {
304+
headers: { authorization: pat },
305+
});
306+
expect(response.status).toBe(200);
307+
await expect(response.json()).resolves.toEqual({
308+
name: `http://localhost:${aggregatorPort}/resource`,
309+
resource_scopes: [ 'http://www.w3.org/ns/odrl/2/read' ],
310+
derived_from: [{
311+
issuer: srcConfig.issuer,
312+
derivation_resource_id: derivationId,
313+
}],
314+
});
315+
});
316+
295317
it('a client cannot read an aggregated resource without the necessary tokens.', async(): Promise<void> => {
296318
// We don't have an actual aggregator server so simulating the request
297319
const body = [{

0 commit comments

Comments
 (0)