refactor: Split HttpHandler behaviour over multiple classes

This allows easier reuse of certain reoccurring behaviours,
such as authorization.
The AuthenticatedLdpHandler is no longer required
since it is a combination of parsing and authorization.
This did require a small change to the OperationHandler interface.
This commit is contained in:
Joachim Van Herwegen
2021-09-24 15:49:56 +02:00
parent 8f5d61911d
commit bb7e88b137
28 changed files with 483 additions and 574 deletions

View File

@@ -0,0 +1,84 @@
import type { CredentialSet } from '../authentication/Credentials';
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
import type { Authorizer } from '../authorization/Authorizer';
import type { PermissionReader } from '../authorization/PermissionReader';
import type { ResponseDescription } from '../ldp/http/response/ResponseDescription';
import type { ModesExtractor } from '../ldp/permissions/ModesExtractor';
import { getLoggerFor } from '../logging/LogUtil';
import type { OperationHttpHandlerInput } from './OperationHttpHandler';
import { OperationHttpHandler } from './OperationHttpHandler';
export interface AuthorizingHttpHandlerArgs {
/**
* Extracts the credentials from the incoming request.
*/
credentialsExtractor: CredentialsExtractor;
/**
* Extracts the required modes from the generated Operation.
*/
modesExtractor: ModesExtractor;
/**
* Reads the permissions available for the Operation.
*/
permissionReader: PermissionReader;
/**
* Verifies if the requested operation is allowed.
*/
authorizer: Authorizer;
/**
* Handler to call if the operation is authorized.
*/
operationHandler: OperationHttpHandler;
}
/**
* Handles all the necessary steps for an authorization.
* Errors if authorization fails, otherwise passes the parameter to the operationHandler handler.
* The following steps are executed:
* - Extracting credentials from the request.
* - Extracting the required permissions.
* - Reading the allowed permissions for the credentials.
* - Validating if this operation is allowed.
*/
export class AuthorizingHttpHandler extends OperationHttpHandler {
private readonly logger = getLoggerFor(this);
private readonly credentialsExtractor: CredentialsExtractor;
private readonly modesExtractor: ModesExtractor;
private readonly permissionReader: PermissionReader;
private readonly authorizer: Authorizer;
private readonly operationHandler: OperationHttpHandler;
public constructor(args: AuthorizingHttpHandlerArgs) {
super();
this.credentialsExtractor = args.credentialsExtractor;
this.modesExtractor = args.modesExtractor;
this.permissionReader = args.permissionReader;
this.authorizer = args.authorizer;
this.operationHandler = args.operationHandler;
}
public async handle(input: OperationHttpHandlerInput): Promise<ResponseDescription | undefined> {
const { request, operation } = input;
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
const modes = await this.modesExtractor.handleSafe(operation);
this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
const permissionSet = await this.permissionReader.handleSafe({ credentials, identifier: operation.target });
this.logger.verbose(`Available permissions are ${JSON.stringify(permissionSet)}`);
try {
await this.authorizer.handleSafe({ credentials, identifier: operation.target, modes, permissionSet });
operation.permissionSet = permissionSet;
} catch (error: unknown) {
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
throw error;
}
this.logger.verbose(`Authorization succeeded, calling source handler`);
return this.operationHandler.handleSafe(input);
}
}

View File

@@ -0,0 +1,16 @@
import type { ResponseDescription } from '../ldp/http/response/ResponseDescription';
import type { Operation } from '../ldp/operations/Operation';
import { AsyncHandler } from '../util/handlers/AsyncHandler';
import type { HttpHandlerInput } from './HttpHandler';
export interface OperationHttpHandlerInput extends HttpHandlerInput {
operation: Operation;
}
/**
* An HTTP handler that makes use of an already parsed Operation.
* Can either return a ResponseDescription to be resolved by the calling class,
* or undefined if this class handles the response itself.
*/
export abstract class OperationHttpHandler
extends AsyncHandler<OperationHttpHandlerInput, ResponseDescription | undefined> {}

View File

@@ -2,20 +2,23 @@ import type { ErrorHandler } from '../ldp/http/ErrorHandler';
import type { RequestParser } from '../ldp/http/RequestParser';
import type { ResponseDescription } from '../ldp/http/response/ResponseDescription';
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
import type { Operation } from '../ldp/operations/Operation';
import type { OperationMetadataCollector } from '../ldp/operations/metadata/OperationMetadataCollector';
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import { getLoggerFor } from '../logging/LogUtil';
import { assertError } from '../util/errors/ErrorUtil';
import type { HttpHandlerInput } from './HttpHandler';
import { HttpHandler } from './HttpHandler';
import type { HttpRequest } from './HttpRequest';
import type { HttpResponse } from './HttpResponse';
import type { OperationHttpHandler } from './OperationHttpHandler';
export interface BaseHttpHandlerArgs {
export interface ParsingHttpHandlerArgs {
/**
* Parses the incoming requests.
*/
requestParser: RequestParser;
/**
* Generates generic operation metadata that is required for a response.
*/
metadataCollector: OperationMetadataCollector;
/**
* Converts errors to a serializable format.
*/
@@ -24,25 +27,33 @@ export interface BaseHttpHandlerArgs {
* Writes out the response of the operation.
*/
responseWriter: ResponseWriter;
/**
* Handler to send the operation to.
*/
operationHandler: OperationHttpHandler;
}
/**
* Parses requests and sends the resulting Operation to the abstract `handleOperation` function.
* Parses requests and sends the resulting Operation to wrapped operationHandler.
* Errors are caught and handled by the Errorhandler.
* In case the `handleOperation` function returns a result it will be sent to the ResponseWriter.
* In case the operationHandler returns a result it will be sent to the ResponseWriter.
*/
export abstract class BaseHttpHandler extends HttpHandler {
protected readonly logger = getLoggerFor(this);
export class ParsingHttpHandler extends HttpHandler {
private readonly logger = getLoggerFor(this);
protected readonly requestParser: RequestParser;
protected readonly errorHandler: ErrorHandler;
protected readonly responseWriter: ResponseWriter;
private readonly requestParser: RequestParser;
private readonly errorHandler: ErrorHandler;
private readonly responseWriter: ResponseWriter;
private readonly metadataCollector: OperationMetadataCollector;
private readonly operationHandler: OperationHttpHandler;
protected constructor(args: BaseHttpHandlerArgs) {
public constructor(args: ParsingHttpHandlerArgs) {
super();
this.requestParser = args.requestParser;
this.errorHandler = args.errorHandler;
this.responseWriter = args.responseWriter;
this.metadataCollector = args.metadataCollector;
this.operationHandler = args.operationHandler;
}
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
@@ -52,7 +63,12 @@ export abstract class BaseHttpHandler extends HttpHandler {
try {
const operation = await this.requestParser.handleSafe(request);
({ preferences } = operation);
result = await this.handleOperation(operation, request, response);
result = await this.operationHandler.handleSafe({ operation, request, response });
if (result?.metadata) {
await this.metadataCollector.handleSafe({ operation, metadata: result.metadata });
}
this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`);
} catch (error: unknown) {
assertError(error);
@@ -63,10 +79,4 @@ export abstract class BaseHttpHandler extends HttpHandler {
await this.responseWriter.handleSafe({ response, result });
}
}
/**
* Handles the operation. Should return a ResponseDescription if it does not handle the response itself.
*/
protected abstract handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse):
Promise<ResponseDescription | undefined>;
}