Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions documentation/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ in the relevant policies that determine access.
The Solid identifier of the resource is irrelevant,
and not even known by the AS.
If the request is successful,
the AS responds with a 201 status code.
The location header contains the new identifier.
the AS responds with a 201 status code and the UMA identifier in the body.
The location header contains the URL needed to update the registration.
The RS stores this identifier, linked to the Solid identifier, for future use.

### About identifiers
Expand Down
2 changes: 1 addition & 1 deletion packages/uma/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@id": "urn:uma:default:HandlerServerConfigurator",
"@type": "HandlerServerConfigurator",
"handler": {
"@id": "urn:uma:default:NodeHttpRequestResponseHandler"
"@id": "urn:uma:default:HttpHandler"
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/uma/config/routes/tickets.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"handler": {
"@type": "TicketRequestHandler",
"ticketingStrategy": { "@id": "urn:uma:default:TicketingStrategy" },
"ticketStore": { "@id": "urn:uma:default:TicketStore" }
"ticketStore": { "@id": "urn:uma:default:TicketStore" },
"resourceStore": { "@id": "urn:uma:default:ResourceRegistrationStore" }
},
"path": "/uma/ticket"
}
Expand Down
2 changes: 2 additions & 0 deletions packages/uma/src/routes/ResourceRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createErrorMessage,
getLoggerFor,
InternalServerError,
joinUrl,
KeyValueStorage,
MethodNotAllowedHttpError,
NotFoundHttpError,
Expand Down Expand Up @@ -93,6 +94,7 @@ export class ResourceRegistrationRequestHandler extends HttpHandler {

return ({
status: 201,
headers: { location: `${joinUrl(request.url.href, encodeURIComponent(resource))}` },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you use the request URL as base, instead of the endpoint URL from config? I know they're probably the same, but source-of-truth-wise it feels a bit weird this way...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this was easier from a dependency point of view 😅

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙈

body: {
_id: resource,
user_access_policy_uri: 'TODO: implement policy UI',
Expand Down
15 changes: 15 additions & 0 deletions packages/uma/src/routes/Ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { HttpHandler, HttpHandlerContext, HttpHandlerResponse } from '../util/ht
import { verifyRequest } from '../util/HttpMessageSignatures';
import { array, reType } from '../util/ReType';
import { Permission } from '../views/Permission';
import { ResourceDescription } from '../views/ResourceDescription';

/**
* A TicketRequestHandler is tasked with implementing
Expand All @@ -25,6 +26,7 @@ export class TicketRequestHandler extends HttpHandler {
constructor(
protected readonly ticketingStrategy: TicketingStrategy,
protected readonly ticketStore: KeyValueStorage<string, Ticket>,
protected readonly resourceStore: KeyValueStorage<string, ResourceDescription>,
) {
super();
}
Expand All @@ -40,6 +42,19 @@ export class TicketRequestHandler extends HttpHandler {
throw new BadRequestHttpError(`Request has bad syntax: ${createErrorMessage(e)}`);
}

for (const { resource_id } of request.body) {
// https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-federated-authz-2.0.html#rfc.section.4.3
if (!await this.resourceStore.has(resource_id)) {
return {
status: 400,
body: {
error: 'invalid_resource_id',
error_description: `Unknown UMA ID ${resource_id}`,
}
}
}
}

const ticket = await this.ticketingStrategy.initializeTicket(request.body);
const resolved = await this.ticketingStrategy.resolveTicket(ticket);

Expand Down
4 changes: 4 additions & 0 deletions packages/uma/test/unit/routes/ResourceRegistration.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'jest-rdf';
import {
joinUrl,
KeyValueStorage,
MethodNotAllowedHttpError,
NotFoundHttpError,
Expand Down Expand Up @@ -96,6 +97,7 @@ describe('ResourceRegistration', (): void => {
it('registers the resource using the name as identifier.', async(): Promise<void> => {
await expect(handler.handle(input)).resolves.toEqual({
status: 201,
headers: { location: `http://example.com/foo/name` },
body: { _id: 'name', user_access_policy_uri: 'TODO: implement policy UI' },
});
expect(resourceStore.set).toHaveBeenCalledTimes(1);
Expand All @@ -109,6 +111,7 @@ describe('ResourceRegistration', (): void => {
input.request.body!.resource_defaults = { pred: [ 'scope' ], '@reverse': { 'rPred': [ 'otherScope' ]}};
await expect(handler.handle(input)).resolves.toEqual({
status: 201,
headers: { location: `http://example.com/foo/name` },
body: { _id: 'name', user_access_policy_uri: 'TODO: implement policy UI' },
});
expect(policies.addRule).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -144,6 +147,7 @@ describe('ResourceRegistration', (): void => {
input.request.body!.name = 'entry';
await expect(handler.handle(input)).resolves.toEqual({
status: 201,
headers: { location: `http://example.com/foo/entry` },
body: { _id: 'entry', user_access_policy_uri: 'TODO: implement policy UI' },
});
expect(policies.addRule).toHaveBeenCalledTimes(1);
Expand Down
20 changes: 19 additions & 1 deletion packages/uma/test/unit/routes/Ticket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TicketingStrategy } from '../../../src/ticketing/strategy/TicketingStra
import { Ticket } from '../../../src/ticketing/Ticket';
import { HttpHandlerContext } from '../../../src/util/http/models/HttpHandler';
import * as signatures from '../../../src/util/HttpMessageSignatures';
import { ResourceDescription } from '../../../src/views/ResourceDescription';

vi.mock('node:crypto', () => ({
randomUUID: vi.fn().mockReturnValue('1-2-3-4-5'),
Expand All @@ -16,6 +17,7 @@ describe('Ticket', (): void => {

let ticketingStrategy: Mocked<TicketingStrategy>;
let ticketStore: Mocked<KeyValueStorage<string, Ticket>>;
let resourceStore: Mocked<KeyValueStorage<string, ResourceDescription>>;
let handler: TicketRequestHandler;

beforeEach(async(): Promise<void> => {
Expand All @@ -33,11 +35,15 @@ describe('Ticket', (): void => {
validateClaims: vi.fn(),
};

resourceStore = {
has: vi.fn().mockResolvedValue(true),
} satisfies Partial<KeyValueStorage<string, ResourceDescription>> as any;

ticketStore = {
set: vi.fn(),
} satisfies Partial<KeyValueStorage<string, Ticket>> as any;

handler = new TicketRequestHandler(ticketingStrategy, ticketStore);
handler = new TicketRequestHandler(ticketingStrategy, ticketStore, resourceStore);
});

it('errors if the request is not authorized.', async(): Promise<void> => {
Expand All @@ -64,4 +70,16 @@ describe('Ticket', (): void => {
expect(ticketStore.set).toHaveBeenCalledTimes(1);
expect(ticketStore.set).toHaveBeenLastCalledWith('1-2-3-4-5', 'ticket');
});

it('returns with invalid_resource_id if one of the targets is unknown.', async(): Promise<void> => {
request.request.body = [
{ resource_id: 'id1', resource_scopes: [ 'scope1' ]},
{ resource_id: 'id2', resource_scopes: [ 'scope2' ]},
];
resourceStore.has.mockResolvedValueOnce(true);
resourceStore.has.mockResolvedValueOnce(false);
await expect(handler.handle(request)).resolves
.toEqual({ status: 400, body: { error: 'invalid_resource_id', error_description: 'Unknown UMA ID id2' }});
expect(ticketStore.set).toHaveBeenCalledTimes(0);
});
});