diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts new file mode 100644 index 000000000..fabe53a46 --- /dev/null +++ b/src/ldp/AuthenticatedLdpHandler.ts @@ -0,0 +1,128 @@ +import { Authorizer } from '../authorization/Authorizer'; +import { Credentials } from '../authentication/Credentials'; +import { CredentialsExtractor } from '../authentication/CredentialsExtractor'; +import { HttpHandler } from '../server/HttpHandler'; +import { HttpRequest } from '../server/HttpRequest'; +import { HttpResponse } from '../server/HttpResponse'; +import { Operation } from './operations/Operation'; +import { OperationHandler } from './operations/OperationHandler'; +import { PermissionSet } from './permissions/PermissionSet'; +import { PermissionsExtractor } from './permissions/PermissionsExtractor'; +import { RequestParser } from './http/RequestParser'; +import { ResponseWriter } from './http/ResponseWriter'; + +/** + * Collection of handlers needed for {@link AuthenticatedLdpHandler} to function. + */ +export interface AuthenticatedLdpHandlerArgs { + /** + * Parses the incoming requests. + */ + requestParser: RequestParser; + /** + * Extracts the credentials from the incoming request. + */ + credentialsExtractor: CredentialsExtractor; + /** + * Extracts the required permissions from the generated Operation. + */ + permissionsExtractor: PermissionsExtractor; + /** + * Verifies if the requested operation is allowed. + */ + authorizer: Authorizer; + /** + * Executed the operation. + */ + operationHandler: OperationHandler; + /** + * Writes out the response of the operation. + */ + responseWriter: ResponseWriter; +} + +/** + * The central manager that connects all the necessary handlers to go from an incoming request to an executed operation. + */ +export class AuthenticatedLdpHandler extends HttpHandler { + private readonly requestParser: RequestParser; + + private readonly credentialsExtractor: CredentialsExtractor; + + private readonly permissionsExtractor: PermissionsExtractor; + + private readonly authorizer: Authorizer; + + private readonly operationHandler: OperationHandler; + + private readonly responseWriter: ResponseWriter; + + /** + * Creates the handler. + * @param args - The handlers required. None of them are optional. + */ + public constructor (args: AuthenticatedLdpHandlerArgs) { + super(); + Object.assign(this, args); + } + + /** + * Checks if the incoming request can be handled. The check is very non-restrictive and will usually be true. + * It is based on whether the incoming request can be parsed to an operation. + * @param input - Incoming request and response. Only the request will be used. + * + * @returns A promise resolving if this request can be handled, otherwise rejecting with an Error. + */ + public async canHandle (input: { request: HttpRequest; response: HttpResponse }): Promise { + return this.requestParser.canHandle(input.request); + } + + /** + * Handles the incoming request and writes out the response. + * This includes the following steps: + * - Parsing the request to an Operation. + * - Extracting credentials from the request. + * - Extracting the required permissions. + * - Validating if this operation is allowed. + * - Executing the operation. + * - Writing out the response. + * @param input - The incoming request and response object to write to. + * + * @returns A promise resolving when the handling is finished. + */ + public async handle (input: { request: HttpRequest; response: HttpResponse }): Promise { + let err: Error; + let operation: Operation; + + try { + operation = await this.runHandlers(input.request); + } catch (error) { + err = error; + } + + const writeData = { response: input.response, operation, error: err }; + + return this.responseWriter.handleSafe(writeData); + } + + /** + * Runs all handlers except writing the output to the response. + * This because any errors thrown here have an impact on the response. + * @param request - Incoming request. + * + * @returns A promise resolving to the generated Operation. + */ + private async runHandlers (request: HttpRequest): Promise { + const op: Operation = await this.requestParser.handleSafe(request); + + const credentials: Credentials = await this.credentialsExtractor.handleSafe(request); + + const permissions: PermissionSet = await this.permissionsExtractor.handleSafe(op); + + await this.authorizer.handleSafe({ credentials, identifier: op.target, permissions }); + + await this.operationHandler.handleSafe(op); + + return op; + } +} diff --git a/src/ldp/http/ResponseWriter.ts b/src/ldp/http/ResponseWriter.ts new file mode 100644 index 000000000..c6e9769de --- /dev/null +++ b/src/ldp/http/ResponseWriter.ts @@ -0,0 +1,9 @@ +import { AsyncHandler } from '../../util/AsyncHandler'; +import { HttpResponse } from '../../server/HttpResponse'; +import { Operation } from '../operations/Operation'; + +/** + * Writes to the HttpResponse. + * Response depends on the operation result and potentially which errors was thrown. + */ +export type ResponseWriter = AsyncHandler<{ response: HttpResponse; operation: Operation; error?: Error }>; diff --git a/test/unit/ldp/AuthenticatedLdpHandler.test.ts b/test/unit/ldp/AuthenticatedLdpHandler.test.ts new file mode 100644 index 000000000..a9d2e5e97 --- /dev/null +++ b/test/unit/ldp/AuthenticatedLdpHandler.test.ts @@ -0,0 +1,65 @@ +import { Authorizer } from '../../../src/authorization/Authorizer'; +import { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor'; +import { OperationHandler } from '../../../src/ldp/operations/OperationHandler'; +import { PermissionsExtractor } from '../../../src/ldp/permissions/PermissionsExtractor'; +import { RequestParser } from '../../../src/ldp/http/RequestParser'; +import { ResponseWriter } from '../../../src/ldp/http/ResponseWriter'; +import { StaticAsyncHandler } from '../../util/StaticAsyncHandler'; +import { AuthenticatedLdpHandler, AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler'; + +describe('An AuthenticatedLdpHandler', (): void => { + let args: AuthenticatedLdpHandlerArgs; + let responseFn: jest.Mock, [any]>; + + beforeEach(async (): Promise => { + const requestParser: RequestParser = new StaticAsyncHandler(true, 'parser' as any); + const credentialsExtractor: CredentialsExtractor = new StaticAsyncHandler(true, 'credentials' as any); + const permissionsExtractor: PermissionsExtractor = new StaticAsyncHandler(true, 'permissions' as any); + const authorizer: Authorizer = new StaticAsyncHandler(true, 'authorizer' as any); + const operationHandler: OperationHandler = new StaticAsyncHandler(true, 'operation' as any); + const responseWriter: ResponseWriter = new StaticAsyncHandler(true, 'response' as any); + + responseFn = jest.fn(async (input: any): Promise => { + if (!input) { + throw new Error('error'); + } + }); + responseWriter.canHandle = responseFn; + + args = { requestParser, credentialsExtractor, permissionsExtractor, authorizer, operationHandler, responseWriter }; + }); + + it('can be created.', async (): Promise => { + expect(new AuthenticatedLdpHandler(args)).toBeInstanceOf(AuthenticatedLdpHandler); + }); + + it('can check if it handles input.', async (): Promise => { + const handler = new AuthenticatedLdpHandler(args); + + await expect(handler.canHandle({ request: null, response: null })).resolves.toBeUndefined(); + }); + + it('can handle input.', async (): Promise => { + const handler = new AuthenticatedLdpHandler(args); + + await expect(handler.handle({ request: 'request' as any, response: 'response' as any })).resolves.toEqual('response'); + expect(responseFn).toHaveBeenCalledTimes(1); + expect(responseFn).toHaveBeenLastCalledWith({ response: 'response', operation: 'parser' as any }); + }); + + it('sends an error to the output if a handler does not support the input.', async (): Promise => { + args.requestParser = new StaticAsyncHandler(false, null); + const handler = new AuthenticatedLdpHandler(args); + + await expect(handler.handle({ request: 'request' as any, response: null })).resolves.toEqual('response'); + expect(responseFn).toHaveBeenCalledTimes(1); + expect(responseFn.mock.calls[0][0].error).toBeInstanceOf(Error); + }); + + it('errors if the response writer does not support the result.', async (): Promise< void> => { + args.responseWriter = new StaticAsyncHandler(false, null); + const handler = new AuthenticatedLdpHandler(args); + + await expect(handler.handle({ request: 'request' as any, response: null })).rejects.toThrow(Error); + }); +});