mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
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:
parent
8f5d61911d
commit
bb7e88b137
@ -7,17 +7,22 @@
|
|||||||
{
|
{
|
||||||
"comment": "Handles everything related to the first-time server setup.",
|
"comment": "Handles everything related to the first-time server setup.",
|
||||||
"@id": "urn:solid-server:default:SetupHttpHandler",
|
"@id": "urn:solid-server:default:SetupHttpHandler",
|
||||||
"@type": "SetupHttpHandler",
|
"@type": "ParsingHttpHandler",
|
||||||
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
||||||
|
"args_metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
|
||||||
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||||
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
|
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
|
||||||
|
"args_operationHandler": {
|
||||||
|
"@type": "SetupHttpHandler",
|
||||||
"args_initializer": { "@id": "urn:solid-server:default:RootInitializer" },
|
"args_initializer": { "@id": "urn:solid-server:default:RootInitializer" },
|
||||||
"args_registrationManager": { "@id": "urn:solid-server:default:SetupRegistrationManager" },
|
"args_registrationManager": { "@id": "urn:solid-server:default:SetupRegistrationManager" },
|
||||||
"args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
"args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
||||||
"args_storageKey": "setupCompleted-1.0",
|
"args_storageKey": "setupCompleted-1.0",
|
||||||
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" },
|
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" },
|
||||||
"args_viewTemplate": "@css:templates/setup/index.html.ejs",
|
"args_viewTemplate": "@css:templates/setup/index.html.ejs",
|
||||||
"args_responseTemplate": "@css:templates/setup/response.html.ejs"
|
"args_responseTemplate": "@css:templates/setup/response.html.ejs",
|
||||||
|
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "Separate manager from the RegistrationHandler in case registration is disabled.",
|
"comment": "Separate manager from the RegistrationHandler in case registration is disabled.",
|
||||||
|
@ -15,14 +15,20 @@
|
|||||||
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
||||||
"args_allowedMethods": [ "*" ],
|
"args_allowedMethods": [ "*" ],
|
||||||
"args_allowedPathNames": [ "^/idp/.*", "^/\\.well-known/openid-configuration" ],
|
"args_allowedPathNames": [ "^/idp/.*", "^/\\.well-known/openid-configuration" ],
|
||||||
"args_handler": { "@id": "urn:solid-server:default:IdentityProviderHttpHandler" }
|
"args_handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"@id": "urn:solid-server:default:IdentityProviderParsingHandler",
|
||||||
|
"@type": "ParsingHttpHandler",
|
||||||
|
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
||||||
|
"args_metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
|
||||||
|
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||||
|
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
|
||||||
|
"args_operationHandler": {
|
||||||
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
"@id": "urn:solid-server:default:IdentityProviderHttpHandler",
|
||||||
"@type": "IdentityProviderHttpHandler",
|
"@type": "IdentityProviderHttpHandler",
|
||||||
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"args_idpPath": "/idp",
|
"args_idpPath": "/idp",
|
||||||
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
|
||||||
"args_providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
|
"args_providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
|
||||||
"args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
"args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
||||||
"args_interactionCompleter": {
|
"args_interactionCompleter": {
|
||||||
@ -30,8 +36,8 @@
|
|||||||
"@type": "InteractionCompleter",
|
"@type": "InteractionCompleter",
|
||||||
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
|
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
|
||||||
},
|
},
|
||||||
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }
|
||||||
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -12,16 +12,19 @@
|
|||||||
{
|
{
|
||||||
"comment": "The main entry point into the main Solid behaviour.",
|
"comment": "The main entry point into the main Solid behaviour.",
|
||||||
"@id": "urn:solid-server:default:LdpHandler",
|
"@id": "urn:solid-server:default:LdpHandler",
|
||||||
"@type": "AuthenticatedLdpHandler",
|
"@type": "ParsingHttpHandler",
|
||||||
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
||||||
|
"args_metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
|
||||||
|
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||||
|
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
|
||||||
|
"args_operationHandler": {
|
||||||
|
"@type": "AuthorizingHttpHandler",
|
||||||
"args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
|
"args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
|
||||||
"args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
|
"args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
|
||||||
"args_permissionReader": { "@id": "urn:solid-server:default:PermissionReader" },
|
"args_permissionReader": { "@id": "urn:solid-server:default:PermissionReader" },
|
||||||
"args_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
|
"args_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
|
||||||
"args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" },
|
"args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" }
|
||||||
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
}
|
||||||
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
|
|
||||||
"args_operationMetadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
||||||
import type { RequestParser } from '../ldp/http/RequestParser';
|
|
||||||
import { RedirectResponseDescription } from '../ldp/http/response/RedirectResponseDescription';
|
import { RedirectResponseDescription } from '../ldp/http/response/RedirectResponseDescription';
|
||||||
import { ResponseDescription } from '../ldp/http/response/ResponseDescription';
|
import { ResponseDescription } from '../ldp/http/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
|
||||||
import type { Operation } from '../ldp/operations/Operation';
|
import type { Operation } from '../ldp/operations/Operation';
|
||||||
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../ldp/representation/Representation';
|
import type { Representation } from '../ldp/representation/Representation';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import { BaseHttpHandler } from '../server/BaseHttpHandler';
|
|
||||||
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
|
||||||
import type { HttpRequest } from '../server/HttpRequest';
|
import type { HttpRequest } from '../server/HttpRequest';
|
||||||
import type { HttpResponse } from '../server/HttpResponse';
|
import type { OperationHttpHandlerInput } from '../server/OperationHttpHandler';
|
||||||
|
import { OperationHttpHandler } from '../server/OperationHttpHandler';
|
||||||
import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
|
import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
|
||||||
import { APPLICATION_JSON } from '../util/ContentTypes';
|
import { APPLICATION_JSON } from '../util/ContentTypes';
|
||||||
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
|
||||||
@ -25,11 +22,7 @@ import type { InteractionCompleter } from './interaction/util/InteractionComplet
|
|||||||
// Registration is not standardized within Solid yet, so we use a custom versioned API for now
|
// Registration is not standardized within Solid yet, so we use a custom versioned API for now
|
||||||
const API_VERSION = '0.2';
|
const API_VERSION = '0.2';
|
||||||
|
|
||||||
export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs {
|
export interface IdentityProviderHttpHandlerArgs {
|
||||||
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
|
|
||||||
requestParser: RequestParser;
|
|
||||||
errorHandler: ErrorHandler;
|
|
||||||
responseWriter: ResponseWriter;
|
|
||||||
/**
|
/**
|
||||||
* Base URL of the server.
|
* Base URL of the server.
|
||||||
*/
|
*/
|
||||||
@ -54,6 +47,10 @@ export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs {
|
|||||||
* Used for POST requests that need to be handled by the OIDC library.
|
* Used for POST requests that need to be handled by the OIDC library.
|
||||||
*/
|
*/
|
||||||
interactionCompleter: InteractionCompleter;
|
interactionCompleter: InteractionCompleter;
|
||||||
|
/**
|
||||||
|
* Used for converting output errors.
|
||||||
|
*/
|
||||||
|
errorHandler: ErrorHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,7 +65,7 @@ export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs {
|
|||||||
* This handler handles all requests since it assumes all those requests are relevant for the IDP interaction.
|
* This handler handles all requests since it assumes all those requests are relevant for the IDP interaction.
|
||||||
* A {@link RouterHandler} should be used to filter out other requests.
|
* A {@link RouterHandler} should be used to filter out other requests.
|
||||||
*/
|
*/
|
||||||
export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
export class IdentityProviderHttpHandler extends OperationHttpHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
@ -76,19 +73,21 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
|||||||
private readonly interactionRoutes: InteractionRoute[];
|
private readonly interactionRoutes: InteractionRoute[];
|
||||||
private readonly converter: RepresentationConverter;
|
private readonly converter: RepresentationConverter;
|
||||||
private readonly interactionCompleter: InteractionCompleter;
|
private readonly interactionCompleter: InteractionCompleter;
|
||||||
|
private readonly errorHandler: ErrorHandler;
|
||||||
|
|
||||||
private readonly controls: Record<string, string>;
|
private readonly controls: Record<string, string>;
|
||||||
|
|
||||||
public constructor(args: IdentityProviderHttpHandlerArgs) {
|
public constructor(args: IdentityProviderHttpHandlerArgs) {
|
||||||
// It is important that the RequestParser does not read out the Request body stream.
|
// It is important that the RequestParser does not read out the Request body stream.
|
||||||
// Otherwise we can't pass it anymore to the OIDC library when needed.
|
// Otherwise we can't pass it anymore to the OIDC library when needed.
|
||||||
super(args);
|
super();
|
||||||
// Trimming trailing slashes so the relative URL starts with a slash after slicing this off
|
// Trimming trailing slashes so the relative URL starts with a slash after slicing this off
|
||||||
this.baseUrl = trimTrailingSlashes(joinUrl(args.baseUrl, args.idpPath));
|
this.baseUrl = trimTrailingSlashes(joinUrl(args.baseUrl, args.idpPath));
|
||||||
this.providerFactory = args.providerFactory;
|
this.providerFactory = args.providerFactory;
|
||||||
this.interactionRoutes = args.interactionRoutes;
|
this.interactionRoutes = args.interactionRoutes;
|
||||||
this.converter = args.converter;
|
this.converter = args.converter;
|
||||||
this.interactionCompleter = args.interactionCompleter;
|
this.interactionCompleter = args.interactionCompleter;
|
||||||
|
this.errorHandler = args.errorHandler;
|
||||||
|
|
||||||
this.controls = Object.assign(
|
this.controls = Object.assign(
|
||||||
{},
|
{},
|
||||||
@ -99,7 +98,7 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
|||||||
/**
|
/**
|
||||||
* Finds the matching route and resolves the operation.
|
* Finds the matching route and resolves the operation.
|
||||||
*/
|
*/
|
||||||
protected async handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse):
|
public async handle({ operation, request, response }: OperationHttpHandlerInput):
|
||||||
Promise<ResponseDescription | undefined> {
|
Promise<ResponseDescription | undefined> {
|
||||||
// This being defined means we're in an OIDC session
|
// This being defined means we're in an OIDC session
|
||||||
let oidcInteraction: Interaction | undefined;
|
let oidcInteraction: Interaction | undefined;
|
||||||
|
@ -168,7 +168,6 @@ export * from './ldp/representation/RepresentationPreferences';
|
|||||||
export * from './ldp/representation/ResourceIdentifier';
|
export * from './ldp/representation/ResourceIdentifier';
|
||||||
|
|
||||||
// LDP
|
// LDP
|
||||||
export * from './ldp/AuthenticatedLdpHandler';
|
|
||||||
export * from './ldp/UnsecureWebSocketsProtocol';
|
export * from './ldp/UnsecureWebSocketsProtocol';
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
@ -211,14 +210,16 @@ export * from './pods/GeneratedPodManager';
|
|||||||
export * from './pods/PodManager';
|
export * from './pods/PodManager';
|
||||||
|
|
||||||
// Server
|
// Server
|
||||||
export * from './server/BaseHttpHandler';
|
export * from './server/AuthorizingHttpHandler';
|
||||||
export * from './server/BaseHttpServerFactory';
|
export * from './server/BaseHttpServerFactory';
|
||||||
export * from './server/HttpHandler';
|
export * from './server/HttpHandler';
|
||||||
export * from './server/HttpRequest';
|
export * from './server/HttpRequest';
|
||||||
export * from './server/HttpResponse';
|
export * from './server/HttpResponse';
|
||||||
export * from './server/HttpServerFactory';
|
export * from './server/HttpServerFactory';
|
||||||
export * from './server/WebSocketServerFactory';
|
export * from './server/OperationHttpHandler';
|
||||||
|
export * from './server/ParsingHttpHandler';
|
||||||
export * from './server/WebSocketHandler';
|
export * from './server/WebSocketHandler';
|
||||||
|
export * from './server/WebSocketServerFactory';
|
||||||
|
|
||||||
// Server/Middleware
|
// Server/Middleware
|
||||||
export * from './server/middleware/CorsHandler';
|
export * from './server/middleware/CorsHandler';
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import type { RegistrationParams,
|
import type { RegistrationParams,
|
||||||
RegistrationManager } from '../../identity/interaction/email-password/util/RegistrationManager';
|
RegistrationManager } from '../../identity/interaction/email-password/util/RegistrationManager';
|
||||||
import type { ErrorHandler } from '../../ldp/http/ErrorHandler';
|
import type { ErrorHandler } from '../../ldp/http/ErrorHandler';
|
||||||
import type { RequestParser } from '../../ldp/http/RequestParser';
|
|
||||||
import { ResponseDescription } from '../../ldp/http/response/ResponseDescription';
|
import { ResponseDescription } from '../../ldp/http/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from '../../ldp/http/ResponseWriter';
|
|
||||||
import type { Operation } from '../../ldp/operations/Operation';
|
import type { Operation } from '../../ldp/operations/Operation';
|
||||||
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import type { BaseHttpHandlerArgs } from '../../server/BaseHttpHandler';
|
import type { OperationHttpHandlerInput } from '../../server/OperationHttpHandler';
|
||||||
import { BaseHttpHandler } from '../../server/BaseHttpHandler';
|
import { OperationHttpHandler } from '../../server/OperationHttpHandler';
|
||||||
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||||
import { APPLICATION_JSON, TEXT_HTML } from '../../util/ContentTypes';
|
import { APPLICATION_JSON, TEXT_HTML } from '../../util/ContentTypes';
|
||||||
@ -38,12 +36,7 @@ export interface SetupInput extends Record<string, any>{
|
|||||||
registration?: boolean;
|
registration?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetupHttpHandlerArgs extends BaseHttpHandlerArgs {
|
export interface SetupHttpHandlerArgs {
|
||||||
// BaseHttpHandler args
|
|
||||||
requestParser: RequestParser;
|
|
||||||
errorHandler: ErrorHandler;
|
|
||||||
responseWriter: ResponseWriter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for registering a pod during setup.
|
* Used for registering a pod during setup.
|
||||||
*/
|
*/
|
||||||
@ -73,6 +66,10 @@ export interface SetupHttpHandlerArgs extends BaseHttpHandlerArgs {
|
|||||||
* Template to show when setup was completed successfully.
|
* Template to show when setup was completed successfully.
|
||||||
*/
|
*/
|
||||||
responseTemplate: string;
|
responseTemplate: string;
|
||||||
|
/**
|
||||||
|
* Used for converting output errors.
|
||||||
|
*/
|
||||||
|
errorHandler: ErrorHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,7 +82,7 @@ export interface SetupHttpHandlerArgs extends BaseHttpHandlerArgs {
|
|||||||
* After successfully completing a POST request this handler will disable itself and become unreachable.
|
* After successfully completing a POST request this handler will disable itself and become unreachable.
|
||||||
* All other methods will be rejected.
|
* All other methods will be rejected.
|
||||||
*/
|
*/
|
||||||
export class SetupHttpHandler extends BaseHttpHandler {
|
export class SetupHttpHandler extends OperationHttpHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly registrationManager?: RegistrationManager;
|
private readonly registrationManager?: RegistrationManager;
|
||||||
@ -95,11 +92,12 @@ export class SetupHttpHandler extends BaseHttpHandler {
|
|||||||
private readonly storage: KeyValueStorage<string, boolean>;
|
private readonly storage: KeyValueStorage<string, boolean>;
|
||||||
private readonly viewTemplate: string;
|
private readonly viewTemplate: string;
|
||||||
private readonly responseTemplate: string;
|
private readonly responseTemplate: string;
|
||||||
|
private readonly errorHandler: ErrorHandler;
|
||||||
|
|
||||||
private finished: boolean;
|
private finished: boolean;
|
||||||
|
|
||||||
public constructor(args: SetupHttpHandlerArgs) {
|
public constructor(args: SetupHttpHandlerArgs) {
|
||||||
super(args);
|
super();
|
||||||
this.finished = false;
|
this.finished = false;
|
||||||
|
|
||||||
this.registrationManager = args.registrationManager;
|
this.registrationManager = args.registrationManager;
|
||||||
@ -109,9 +107,10 @@ export class SetupHttpHandler extends BaseHttpHandler {
|
|||||||
this.storage = args.storage;
|
this.storage = args.storage;
|
||||||
this.viewTemplate = args.viewTemplate;
|
this.viewTemplate = args.viewTemplate;
|
||||||
this.responseTemplate = args.responseTemplate;
|
this.responseTemplate = args.responseTemplate;
|
||||||
|
this.errorHandler = args.errorHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleOperation(operation: Operation): Promise<ResponseDescription> {
|
public async handle({ operation }: OperationHttpHandlerInput): Promise<ResponseDescription> {
|
||||||
let json: Record<string, any>;
|
let json: Record<string, any>;
|
||||||
let template: string;
|
let template: string;
|
||||||
let success = false;
|
let success = false;
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
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 { BaseHttpHandler } from '../server/BaseHttpHandler';
|
|
||||||
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
|
||||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
|
||||||
import type { HttpRequest } from '../server/HttpRequest';
|
|
||||||
import type { ErrorHandler } from './http/ErrorHandler';
|
|
||||||
import type { RequestParser } from './http/RequestParser';
|
|
||||||
import type { ResponseDescription } from './http/response/ResponseDescription';
|
|
||||||
import type { ResponseWriter } from './http/ResponseWriter';
|
|
||||||
import type { OperationMetadataCollector } from './operations/metadata/OperationMetadataCollector';
|
|
||||||
import type { Operation } from './operations/Operation';
|
|
||||||
import type { OperationHandler } from './operations/OperationHandler';
|
|
||||||
import type { ModesExtractor } from './permissions/ModesExtractor';
|
|
||||||
|
|
||||||
export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
|
|
||||||
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
|
|
||||||
requestParser: RequestParser;
|
|
||||||
errorHandler: ErrorHandler;
|
|
||||||
responseWriter: ResponseWriter;
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
/**
|
|
||||||
* Executed the operation.
|
|
||||||
*/
|
|
||||||
operationHandler: OperationHandler;
|
|
||||||
/**
|
|
||||||
* Generates generic operation metadata that is required for a response.
|
|
||||||
*/
|
|
||||||
operationMetadataCollector: OperationMetadataCollector;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The central manager that connects all the necessary handlers to go from an incoming request to an executed operation.
|
|
||||||
*/
|
|
||||||
export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
|
||||||
private readonly credentialsExtractor: CredentialsExtractor;
|
|
||||||
private readonly modesExtractor: ModesExtractor;
|
|
||||||
private readonly permissionReader: PermissionReader;
|
|
||||||
private readonly authorizer: Authorizer;
|
|
||||||
private readonly operationHandler: OperationHandler;
|
|
||||||
private readonly operationMetadataCollector: OperationMetadataCollector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the handler.
|
|
||||||
* @param args - The handlers required. None of them are optional.
|
|
||||||
*/
|
|
||||||
public constructor(args: AuthenticatedLdpHandlerArgs) {
|
|
||||||
super(args);
|
|
||||||
this.credentialsExtractor = args.credentialsExtractor;
|
|
||||||
this.modesExtractor = args.modesExtractor;
|
|
||||||
this.permissionReader = args.permissionReader;
|
|
||||||
this.authorizer = args.authorizer;
|
|
||||||
this.operationHandler = args.operationHandler;
|
|
||||||
this.operationMetadataCollector = args.operationMetadataCollector;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: HttpHandlerInput): Promise<void> {
|
|
||||||
return this.requestParser.canHandle(input.request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the incoming operation and generates a response.
|
|
||||||
* This includes the following steps:
|
|
||||||
* - Extracting credentials from the request.
|
|
||||||
* - Extracting the required permissions.
|
|
||||||
* - Validating if this operation is allowed.
|
|
||||||
* - Executing the operation.
|
|
||||||
*/
|
|
||||||
protected async handleOperation(operation: Operation, request: HttpRequest): Promise<ResponseDescription> {
|
|
||||||
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, performing operation`);
|
|
||||||
const response = await this.operationHandler.handleSafe(operation);
|
|
||||||
|
|
||||||
if (response.metadata) {
|
|
||||||
await this.operationMetadataCollector.handleSafe({ operation, metadata: response.metadata });
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,7 @@ import type { ResourceStore } from '../../storage/ResourceStore';
|
|||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
||||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||||
import type { Operation } from './Operation';
|
import type { OperationHandlerInput } from './OperationHandler';
|
||||||
import { OperationHandler } from './OperationHandler';
|
import { OperationHandler } from './OperationHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,14 +17,14 @@ export class DeleteOperationHandler extends OperationHandler {
|
|||||||
this.store = store;
|
this.store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle(input: Operation): Promise<void> {
|
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||||
if (input.method !== 'DELETE') {
|
if (operation.method !== 'DELETE') {
|
||||||
throw new NotImplementedHttpError('This handler only supports DELETE operations');
|
throw new NotImplementedHttpError('This handler only supports DELETE operations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||||
await this.store.deleteResource(input.target, input.conditions);
|
await this.store.deleteResource(operation.target, operation.conditions);
|
||||||
return new ResetResponseDescription();
|
return new ResetResponseDescription();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import type { ResourceStore } from '../../storage/ResourceStore';
|
|||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { OkResponseDescription } from '../http/response/OkResponseDescription';
|
import { OkResponseDescription } from '../http/response/OkResponseDescription';
|
||||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||||
import type { Operation } from './Operation';
|
import type { OperationHandlerInput } from './OperationHandler';
|
||||||
import { OperationHandler } from './OperationHandler';
|
import { OperationHandler } from './OperationHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,14 +17,14 @@ export class GetOperationHandler extends OperationHandler {
|
|||||||
this.store = store;
|
this.store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle(input: Operation): Promise<void> {
|
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||||
if (input.method !== 'GET') {
|
if (operation.method !== 'GET') {
|
||||||
throw new NotImplementedHttpError('This handler only supports GET operations');
|
throw new NotImplementedHttpError('This handler only supports GET operations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||||
const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions);
|
const body = await this.store.getRepresentation(operation.target, operation.preferences, operation.conditions);
|
||||||
|
|
||||||
return new OkResponseDescription(body.metadata, body.data);
|
return new OkResponseDescription(body.metadata, body.data);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import type { ResourceStore } from '../../storage/ResourceStore';
|
|||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { OkResponseDescription } from '../http/response/OkResponseDescription';
|
import { OkResponseDescription } from '../http/response/OkResponseDescription';
|
||||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||||
import type { Operation } from './Operation';
|
import type { OperationHandlerInput } from './OperationHandler';
|
||||||
import { OperationHandler } from './OperationHandler';
|
import { OperationHandler } from './OperationHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,14 +17,14 @@ export class HeadOperationHandler extends OperationHandler {
|
|||||||
this.store = store;
|
this.store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle(input: Operation): Promise<void> {
|
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||||
if (input.method !== 'HEAD') {
|
if (operation.method !== 'HEAD') {
|
||||||
throw new NotImplementedHttpError('This handler only supports HEAD operations');
|
throw new NotImplementedHttpError('This handler only supports HEAD operations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||||
const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions);
|
const body = await this.store.getRepresentation(operation.target, operation.preferences, operation.conditions);
|
||||||
|
|
||||||
// Close the Readable as we will not return it.
|
// Close the Readable as we will not return it.
|
||||||
body.data.destroy();
|
body.data.destroy();
|
||||||
|
@ -2,7 +2,11 @@ import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
|||||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||||
import type { Operation } from './Operation';
|
import type { Operation } from './Operation';
|
||||||
|
|
||||||
|
export interface OperationHandlerInput {
|
||||||
|
operation: Operation;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for a specific operation type.
|
* Handler for a specific operation type.
|
||||||
*/
|
*/
|
||||||
export abstract class OperationHandler extends AsyncHandler<Operation, ResponseDescription> {}
|
export abstract class OperationHandler extends AsyncHandler<OperationHandlerInput, ResponseDescription> {}
|
||||||
|
@ -5,7 +5,7 @@ import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpErr
|
|||||||
import type { Patch } from '../http/Patch';
|
import type { Patch } from '../http/Patch';
|
||||||
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
||||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||||
import type { Operation } from './Operation';
|
import type { OperationHandlerInput } from './OperationHandler';
|
||||||
import { OperationHandler } from './OperationHandler';
|
import { OperationHandler } from './OperationHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,21 +22,21 @@ export class PatchOperationHandler extends OperationHandler {
|
|||||||
this.store = store;
|
this.store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle(input: Operation): Promise<void> {
|
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||||
if (input.method !== 'PATCH') {
|
if (operation.method !== 'PATCH') {
|
||||||
throw new NotImplementedHttpError('This handler only supports PATCH operations.');
|
throw new NotImplementedHttpError('This handler only supports PATCH operations.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||||
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
||||||
// without the Content-Type header with a status code of 400."
|
// without the Content-Type header with a status code of 400."
|
||||||
// https://solid.github.io/specification/protocol#http-server
|
// https://solid.github.io/specification/protocol#http-server
|
||||||
if (!input.body?.metadata.contentType) {
|
if (!operation.body?.metadata.contentType) {
|
||||||
this.logger.warn('No Content-Type header specified on PATCH request');
|
this.logger.warn('No Content-Type header specified on PATCH request');
|
||||||
throw new BadRequestHttpError('No Content-Type header specified on PATCH request');
|
throw new BadRequestHttpError('No Content-Type header specified on PATCH request');
|
||||||
}
|
}
|
||||||
await this.store.modifyResource(input.target, input.body as Patch, input.conditions);
|
await this.store.modifyResource(operation.target, operation.body as Patch, operation.conditions);
|
||||||
return new ResetResponseDescription();
|
return new ResetResponseDescription();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
|||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { CreatedResponseDescription } from '../http/response/CreatedResponseDescription';
|
import { CreatedResponseDescription } from '../http/response/CreatedResponseDescription';
|
||||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||||
import type { Operation } from './Operation';
|
import type { OperationHandlerInput } from './OperationHandler';
|
||||||
import { OperationHandler } from './OperationHandler';
|
import { OperationHandler } from './OperationHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,21 +21,21 @@ export class PostOperationHandler extends OperationHandler {
|
|||||||
this.store = store;
|
this.store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle(input: Operation): Promise<void> {
|
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||||
if (input.method !== 'POST') {
|
if (operation.method !== 'POST') {
|
||||||
throw new NotImplementedHttpError('This handler only supports POST operations');
|
throw new NotImplementedHttpError('This handler only supports POST operations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||||
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
||||||
// without the Content-Type header with a status code of 400."
|
// without the Content-Type header with a status code of 400."
|
||||||
// https://solid.github.io/specification/protocol#http-server
|
// https://solid.github.io/specification/protocol#http-server
|
||||||
if (!input.body?.metadata.contentType) {
|
if (!operation.body?.metadata.contentType) {
|
||||||
this.logger.warn('No Content-Type header specified on POST request');
|
this.logger.warn('No Content-Type header specified on POST request');
|
||||||
throw new BadRequestHttpError('No Content-Type header specified on POST request');
|
throw new BadRequestHttpError('No Content-Type header specified on POST request');
|
||||||
}
|
}
|
||||||
const identifier = await this.store.addResource(input.target, input.body, input.conditions);
|
const identifier = await this.store.addResource(operation.target, operation.body, operation.conditions);
|
||||||
return new CreatedResponseDescription(identifier);
|
return new CreatedResponseDescription(identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
|||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
|
||||||
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
import type { ResponseDescription } from '../http/response/ResponseDescription';
|
||||||
import type { Operation } from './Operation';
|
import type { OperationHandlerInput } from './OperationHandler';
|
||||||
import { OperationHandler } from './OperationHandler';
|
import { OperationHandler } from './OperationHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,21 +21,21 @@ export class PutOperationHandler extends OperationHandler {
|
|||||||
this.store = store;
|
this.store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle(input: Operation): Promise<void> {
|
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||||
if (input.method !== 'PUT') {
|
if (operation.method !== 'PUT') {
|
||||||
throw new NotImplementedHttpError('This handler only supports PUT operations');
|
throw new NotImplementedHttpError('This handler only supports PUT operations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
||||||
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
// Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
|
||||||
// without the Content-Type header with a status code of 400."
|
// without the Content-Type header with a status code of 400."
|
||||||
// https://solid.github.io/specification/protocol#http-server
|
// https://solid.github.io/specification/protocol#http-server
|
||||||
if (!input.body?.metadata.contentType) {
|
if (!operation.body?.metadata.contentType) {
|
||||||
this.logger.warn('No Content-Type header specified on PUT request');
|
this.logger.warn('No Content-Type header specified on PUT request');
|
||||||
throw new BadRequestHttpError('No Content-Type header specified on PUT request');
|
throw new BadRequestHttpError('No Content-Type header specified on PUT request');
|
||||||
}
|
}
|
||||||
await this.store.setRepresentation(input.target, input.body, input.conditions);
|
await this.store.setRepresentation(operation.target, operation.body, operation.conditions);
|
||||||
return new ResetResponseDescription();
|
return new ResetResponseDescription();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
84
src/server/AuthorizingHttpHandler.ts
Normal file
84
src/server/AuthorizingHttpHandler.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
16
src/server/OperationHttpHandler.ts
Normal file
16
src/server/OperationHttpHandler.ts
Normal 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> {}
|
@ -2,20 +2,23 @@ import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
|||||||
import type { RequestParser } from '../ldp/http/RequestParser';
|
import type { RequestParser } from '../ldp/http/RequestParser';
|
||||||
import type { ResponseDescription } from '../ldp/http/response/ResponseDescription';
|
import type { ResponseDescription } from '../ldp/http/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
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 type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import { assertError } from '../util/errors/ErrorUtil';
|
import { assertError } from '../util/errors/ErrorUtil';
|
||||||
import type { HttpHandlerInput } from './HttpHandler';
|
import type { HttpHandlerInput } from './HttpHandler';
|
||||||
import { HttpHandler } from './HttpHandler';
|
import { HttpHandler } from './HttpHandler';
|
||||||
import type { HttpRequest } from './HttpRequest';
|
import type { OperationHttpHandler } from './OperationHttpHandler';
|
||||||
import type { HttpResponse } from './HttpResponse';
|
|
||||||
|
|
||||||
export interface BaseHttpHandlerArgs {
|
export interface ParsingHttpHandlerArgs {
|
||||||
/**
|
/**
|
||||||
* Parses the incoming requests.
|
* Parses the incoming requests.
|
||||||
*/
|
*/
|
||||||
requestParser: RequestParser;
|
requestParser: RequestParser;
|
||||||
|
/**
|
||||||
|
* Generates generic operation metadata that is required for a response.
|
||||||
|
*/
|
||||||
|
metadataCollector: OperationMetadataCollector;
|
||||||
/**
|
/**
|
||||||
* Converts errors to a serializable format.
|
* Converts errors to a serializable format.
|
||||||
*/
|
*/
|
||||||
@ -24,25 +27,33 @@ export interface BaseHttpHandlerArgs {
|
|||||||
* Writes out the response of the operation.
|
* Writes out the response of the operation.
|
||||||
*/
|
*/
|
||||||
responseWriter: ResponseWriter;
|
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.
|
* 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 {
|
export class ParsingHttpHandler extends HttpHandler {
|
||||||
protected readonly logger = getLoggerFor(this);
|
private readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
protected readonly requestParser: RequestParser;
|
private readonly requestParser: RequestParser;
|
||||||
protected readonly errorHandler: ErrorHandler;
|
private readonly errorHandler: ErrorHandler;
|
||||||
protected readonly responseWriter: ResponseWriter;
|
private readonly responseWriter: ResponseWriter;
|
||||||
|
private readonly metadataCollector: OperationMetadataCollector;
|
||||||
|
private readonly operationHandler: OperationHttpHandler;
|
||||||
|
|
||||||
protected constructor(args: BaseHttpHandlerArgs) {
|
public constructor(args: ParsingHttpHandlerArgs) {
|
||||||
super();
|
super();
|
||||||
this.requestParser = args.requestParser;
|
this.requestParser = args.requestParser;
|
||||||
this.errorHandler = args.errorHandler;
|
this.errorHandler = args.errorHandler;
|
||||||
this.responseWriter = args.responseWriter;
|
this.responseWriter = args.responseWriter;
|
||||||
|
this.metadataCollector = args.metadataCollector;
|
||||||
|
this.operationHandler = args.operationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
||||||
@ -52,7 +63,12 @@ export abstract class BaseHttpHandler extends HttpHandler {
|
|||||||
try {
|
try {
|
||||||
const operation = await this.requestParser.handleSafe(request);
|
const operation = await this.requestParser.handleSafe(request);
|
||||||
({ preferences } = operation);
|
({ 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}`);
|
this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
assertError(error);
|
assertError(error);
|
||||||
@ -63,10 +79,4 @@ export abstract class BaseHttpHandler extends HttpHandler {
|
|||||||
await this.responseWriter.handleSafe({ response, result });
|
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>;
|
|
||||||
}
|
}
|
@ -5,9 +5,7 @@ import { IdentityProviderHttpHandler } from '../../../src/identity/IdentityProvi
|
|||||||
import type { InteractionRoute } from '../../../src/identity/interaction/routing/InteractionRoute';
|
import type { InteractionRoute } from '../../../src/identity/interaction/routing/InteractionRoute';
|
||||||
import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter';
|
import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter';
|
||||||
import type { ErrorHandler, ErrorHandlerArgs } from '../../../src/ldp/http/ErrorHandler';
|
import type { ErrorHandler, ErrorHandlerArgs } from '../../../src/ldp/http/ErrorHandler';
|
||||||
import type { RequestParser } from '../../../src/ldp/http/RequestParser';
|
|
||||||
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
|
||||||
import type { Operation } from '../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../src/ldp/operations/Operation';
|
||||||
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../../src/ldp/representation/Representation';
|
import type { Representation } from '../../../src/ldp/representation/Representation';
|
||||||
@ -27,32 +25,20 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
|||||||
const apiVersion = '0.2';
|
const apiVersion = '0.2';
|
||||||
const baseUrl = 'http://test.com/';
|
const baseUrl = 'http://test.com/';
|
||||||
const idpPath = '/idp';
|
const idpPath = '/idp';
|
||||||
let request: HttpRequest;
|
const request: HttpRequest = {} as any;
|
||||||
const response: HttpResponse = {} as any;
|
const response: HttpResponse = {} as any;
|
||||||
let requestParser: jest.Mocked<RequestParser>;
|
let operation: Operation;
|
||||||
let providerFactory: jest.Mocked<ProviderFactory>;
|
let providerFactory: jest.Mocked<ProviderFactory>;
|
||||||
let routes: Record<'response' | 'complete' | 'error', jest.Mocked<InteractionRoute>>;
|
let routes: Record<'response' | 'complete' | 'error', jest.Mocked<InteractionRoute>>;
|
||||||
let controls: Record<string, string>;
|
let controls: Record<string, string>;
|
||||||
let interactionCompleter: jest.Mocked<InteractionCompleter>;
|
let interactionCompleter: jest.Mocked<InteractionCompleter>;
|
||||||
let converter: jest.Mocked<RepresentationConverter>;
|
let converter: jest.Mocked<RepresentationConverter>;
|
||||||
let errorHandler: jest.Mocked<ErrorHandler>;
|
let errorHandler: jest.Mocked<ErrorHandler>;
|
||||||
let responseWriter: jest.Mocked<ResponseWriter>;
|
|
||||||
let provider: jest.Mocked<Provider>;
|
let provider: jest.Mocked<Provider>;
|
||||||
let handler: IdentityProviderHttpHandler;
|
let handler: IdentityProviderHttpHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
request = { url: '/idp', method: 'GET', headers: {}} as any;
|
operation = { method: 'GET', target: { path: 'http://test.com/idp' }, preferences: { type: { 'text/html': 1 }}};
|
||||||
|
|
||||||
requestParser = {
|
|
||||||
handleSafe: jest.fn(async(req: HttpRequest): Promise<Operation> => ({
|
|
||||||
target: { path: joinUrl(baseUrl, req.url!) },
|
|
||||||
method: req.method!,
|
|
||||||
body: req.method === 'GET' ?
|
|
||||||
undefined :
|
|
||||||
new BasicRepresentation('{}', req.headers['content-type'] ?? 'text/plain'),
|
|
||||||
preferences: { type: { 'text/html': 1 }},
|
|
||||||
})),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
provider = {
|
provider = {
|
||||||
callback: jest.fn(),
|
callback: jest.fn(),
|
||||||
@ -110,41 +96,35 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
|||||||
data: guardedStreamFrom(`{ "name": "${error.name}", "message": "${error.message}" }`),
|
data: guardedStreamFrom(`{ "name": "${error.name}", "message": "${error.message}" }`),
|
||||||
})) } as any;
|
})) } as any;
|
||||||
|
|
||||||
responseWriter = { handleSafe: jest.fn() } as any;
|
|
||||||
|
|
||||||
const args: IdentityProviderHttpHandlerArgs = {
|
const args: IdentityProviderHttpHandlerArgs = {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
idpPath,
|
idpPath,
|
||||||
requestParser,
|
|
||||||
providerFactory,
|
providerFactory,
|
||||||
interactionRoutes: Object.values(routes),
|
interactionRoutes: Object.values(routes),
|
||||||
converter,
|
converter,
|
||||||
interactionCompleter,
|
interactionCompleter,
|
||||||
errorHandler,
|
errorHandler,
|
||||||
responseWriter,
|
|
||||||
};
|
};
|
||||||
handler = new IdentityProviderHttpHandler(args);
|
handler = new IdentityProviderHttpHandler(args);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the provider if there is no matching route.', async(): Promise<void> => {
|
it('calls the provider if there is no matching route.', async(): Promise<void> => {
|
||||||
request.url = 'invalid';
|
operation.target.path = joinUrl(baseUrl, 'invalid');
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response, operation })).resolves.toBeUndefined();
|
||||||
expect(provider.callback).toHaveBeenCalledTimes(1);
|
expect(provider.callback).toHaveBeenCalledTimes(1);
|
||||||
expect(provider.callback).toHaveBeenLastCalledWith(request, response);
|
expect(provider.callback).toHaveBeenLastCalledWith(request, response);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates Representations for InteractionResponseResults.', async(): Promise<void> => {
|
it('creates Representations for InteractionResponseResults.', async(): Promise<void> => {
|
||||||
request.url = '/idp/routeResponse';
|
operation.target.path = joinUrl(baseUrl, '/idp/routeResponse');
|
||||||
request.method = 'POST';
|
operation.method = 'POST';
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
operation.body = new BasicRepresentation('value', 'text/plain');
|
||||||
const operation: Operation = await requestParser.handleSafe.mock.results[0].value;
|
const result = (await handler.handle({ request, response, operation }))!;
|
||||||
|
expect(result).toBeDefined();
|
||||||
expect(routes.response.handleOperation).toHaveBeenCalledTimes(1);
|
expect(routes.response.handleOperation).toHaveBeenCalledTimes(1);
|
||||||
expect(routes.response.handleOperation).toHaveBeenLastCalledWith(operation, undefined);
|
expect(routes.response.handleOperation).toHaveBeenLastCalledWith(operation, undefined);
|
||||||
expect(operation.body?.metadata.contentType).toBe('application/json');
|
expect(operation.body?.metadata.contentType).toBe('application/json');
|
||||||
|
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
const { response: mockResponse, result } = responseWriter.handleSafe.mock.calls[0][0];
|
|
||||||
expect(mockResponse).toBe(response);
|
|
||||||
expect(JSON.parse(await readableToString(result.data!)))
|
expect(JSON.parse(await readableToString(result.data!)))
|
||||||
.toEqual({ apiVersion, key: 'val', authenticating: false, controls });
|
.toEqual({ apiVersion, key: 'val', authenticating: false, controls });
|
||||||
expect(result.statusCode).toBe(200);
|
expect(result.statusCode).toBe(200);
|
||||||
@ -153,20 +133,15 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('creates Representations for InteractionErrorResults.', async(): Promise<void> => {
|
it('creates Representations for InteractionErrorResults.', async(): Promise<void> => {
|
||||||
requestParser.handleSafe.mockResolvedValueOnce({
|
operation.target.path = joinUrl(baseUrl, '/idp/routeError');
|
||||||
target: { path: joinUrl(baseUrl, '/idp/routeError') },
|
operation.method = 'POST';
|
||||||
method: 'POST',
|
operation.preferences = { type: { 'text/html': 1 }};
|
||||||
preferences: { type: { 'text/html': 1 }},
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
const result = (await handler.handle({ request, response, operation }))!;
|
||||||
const operation: Operation = await requestParser.handleSafe.mock.results[0].value;
|
expect(result).toBeDefined();
|
||||||
expect(routes.error.handleOperation).toHaveBeenCalledTimes(1);
|
expect(routes.error.handleOperation).toHaveBeenCalledTimes(1);
|
||||||
expect(routes.error.handleOperation).toHaveBeenLastCalledWith(operation, undefined);
|
expect(routes.error.handleOperation).toHaveBeenLastCalledWith(operation, undefined);
|
||||||
|
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
const { response: mockResponse, result } = responseWriter.handleSafe.mock.calls[0][0];
|
|
||||||
expect(mockResponse).toBe(response);
|
|
||||||
expect(JSON.parse(await readableToString(result.data!)))
|
expect(JSON.parse(await readableToString(result.data!)))
|
||||||
.toEqual({ apiVersion, name: 'Error', message: 'test error', authenticating: false, controls });
|
.toEqual({ apiVersion, name: 'Error', message: 'test error', authenticating: false, controls });
|
||||||
expect(result.statusCode).toBe(400);
|
expect(result.statusCode).toBe(400);
|
||||||
@ -175,22 +150,17 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds a prefilled field in case error requests had a body.', async(): Promise<void> => {
|
it('adds a prefilled field in case error requests had a body.', async(): Promise<void> => {
|
||||||
requestParser.handleSafe.mockResolvedValueOnce({
|
operation.target.path = joinUrl(baseUrl, '/idp/routeError');
|
||||||
target: { path: joinUrl(baseUrl, '/idp/routeError') },
|
operation.method = 'POST';
|
||||||
method: 'POST',
|
operation.preferences = { type: { 'text/html': 1 }};
|
||||||
body: new BasicRepresentation('{ "key": "val" }', 'application/json'),
|
operation.body = new BasicRepresentation('{ "key": "val" }', 'application/json');
|
||||||
preferences: { type: { 'text/html': 1 }},
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
const result = (await handler.handle({ request, response, operation }))!;
|
||||||
const operation: Operation = await requestParser.handleSafe.mock.results[0].value;
|
expect(result).toBeDefined();
|
||||||
expect(routes.error.handleOperation).toHaveBeenCalledTimes(1);
|
expect(routes.error.handleOperation).toHaveBeenCalledTimes(1);
|
||||||
expect(routes.error.handleOperation).toHaveBeenLastCalledWith(operation, undefined);
|
expect(routes.error.handleOperation).toHaveBeenLastCalledWith(operation, undefined);
|
||||||
expect(operation.body?.metadata.contentType).toBe('application/json');
|
expect(operation.body?.metadata.contentType).toBe('application/json');
|
||||||
|
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
const { response: mockResponse, result } = responseWriter.handleSafe.mock.calls[0][0];
|
|
||||||
expect(mockResponse).toBe(response);
|
|
||||||
expect(JSON.parse(await readableToString(result.data!))).toEqual(
|
expect(JSON.parse(await readableToString(result.data!))).toEqual(
|
||||||
{ apiVersion, name: 'Error', message: 'test error', authenticating: false, controls, prefilled: { key: 'val' }},
|
{ apiVersion, name: 'Error', message: 'test error', authenticating: false, controls, prefilled: { key: 'val' }},
|
||||||
);
|
);
|
||||||
@ -200,47 +170,41 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('indicates to the templates if the request is part of an auth flow.', async(): Promise<void> => {
|
it('indicates to the templates if the request is part of an auth flow.', async(): Promise<void> => {
|
||||||
request.url = '/idp/routeResponse';
|
operation.target.path = joinUrl(baseUrl, '/idp/routeResponse');
|
||||||
request.method = 'POST';
|
operation.method = 'POST';
|
||||||
const oidcInteraction = { session: { accountId: 'account' }, prompt: {}} as any;
|
const oidcInteraction = { session: { accountId: 'account' }, prompt: {}} as any;
|
||||||
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction);
|
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction);
|
||||||
routes.response.handleOperation
|
routes.response.handleOperation
|
||||||
.mockResolvedValueOnce({ type: 'response', templateFiles: { 'text/html': '/response' }});
|
.mockResolvedValueOnce({ type: 'response', templateFiles: { 'text/html': '/response' }});
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
|
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
const result = (await handler.handle({ request, response, operation }))!;
|
||||||
const { result } = responseWriter.handleSafe.mock.calls[0][0];
|
expect(result).toBeDefined();
|
||||||
expect(JSON.parse(await readableToString(result.data!))).toEqual({ apiVersion, authenticating: true, controls });
|
expect(JSON.parse(await readableToString(result.data!))).toEqual({ apiVersion, authenticating: true, controls });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors for InteractionCompleteResults if no oidcInteraction is defined.', async(): Promise<void> => {
|
it('errors for InteractionCompleteResults if no oidcInteraction is defined.', async(): Promise<void> => {
|
||||||
request.url = '/idp/routeComplete';
|
operation.target.path = joinUrl(baseUrl, '/idp/routeComplete');
|
||||||
request.method = 'POST';
|
operation.method = 'POST';
|
||||||
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 400 });
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
const operation: Operation = await requestParser.handleSafe.mock.results[0].value;
|
|
||||||
expect(routes.complete.handleOperation).toHaveBeenCalledTimes(1);
|
|
||||||
expect(routes.complete.handleOperation).toHaveBeenLastCalledWith(operation, undefined);
|
|
||||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
expect(operation.body?.metadata.contentType).toBe('application/json');
|
|
||||||
|
|
||||||
const error = expect.objectContaining({
|
const error = expect.objectContaining({
|
||||||
|
statusCode: 400,
|
||||||
message: 'This action can only be performed as part of an OIDC authentication flow.',
|
message: 'This action can only be performed as part of an OIDC authentication flow.',
|
||||||
errorCode: 'E0002',
|
errorCode: 'E0002',
|
||||||
});
|
});
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
await expect(handler.handle({ request, response, operation })).rejects.toThrow(error);
|
||||||
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/html': 1 }}});
|
expect(routes.complete.handleOperation).toHaveBeenCalledTimes(1);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(routes.complete.handleOperation).toHaveBeenLastCalledWith(operation, undefined);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 400 }});
|
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the interactionCompleter for InteractionCompleteResults and redirects.', async(): Promise<void> => {
|
it('calls the interactionCompleter for InteractionCompleteResults and redirects.', async(): Promise<void> => {
|
||||||
request.url = '/idp/routeComplete';
|
operation.target.path = joinUrl(baseUrl, '/idp/routeComplete');
|
||||||
request.method = 'POST';
|
operation.method = 'POST';
|
||||||
|
operation.body = new BasicRepresentation('value', 'text/plain');
|
||||||
const oidcInteraction = { session: { accountId: 'account' }, prompt: {}} as any;
|
const oidcInteraction = { session: { accountId: 'account' }, prompt: {}} as any;
|
||||||
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction);
|
provider.interactionDetails.mockResolvedValueOnce(oidcInteraction);
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
const result = (await handler.handle({ request, response, operation }))!;
|
||||||
const operation: Operation = await requestParser.handleSafe.mock.results[0].value;
|
expect(result).toBeDefined();
|
||||||
expect(routes.complete.handleOperation).toHaveBeenCalledTimes(1);
|
expect(routes.complete.handleOperation).toHaveBeenCalledTimes(1);
|
||||||
expect(routes.complete.handleOperation).toHaveBeenLastCalledWith(operation, oidcInteraction);
|
expect(routes.complete.handleOperation).toHaveBeenLastCalledWith(operation, oidcInteraction);
|
||||||
expect(operation.body?.metadata.contentType).toBe('application/json');
|
expect(operation.body?.metadata.contentType).toBe('application/json');
|
||||||
@ -248,23 +212,7 @@ describe('An IdentityProviderHttpHandler', (): void => {
|
|||||||
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ request, webId: 'webId' });
|
expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ request, webId: 'webId' });
|
||||||
const location = await interactionCompleter.handleSafe.mock.results[0].value;
|
const location = await interactionCompleter.handleSafe.mock.results[0].value;
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(result.statusCode).toBe(302);
|
||||||
const args = responseWriter.handleSafe.mock.calls[0][0];
|
expect(result.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(location);
|
||||||
expect(args.response).toBe(response);
|
|
||||||
expect(args.result.statusCode).toBe(302);
|
|
||||||
expect(args.result.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(location);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls the errorHandler if there is a problem resolving the request.', async(): Promise<void> => {
|
|
||||||
request.url = '/idp/routeResponse';
|
|
||||||
request.method = 'GET';
|
|
||||||
const error = new Error('bad template');
|
|
||||||
converter.handleSafe.mockRejectedValueOnce(error);
|
|
||||||
errorHandler.handleSafe.mockResolvedValueOnce({ statusCode: 500 });
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/html': 1 }}});
|
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: { statusCode: 500 }});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,9 +4,7 @@ import type { Initializer } from '../../../../src/init/Initializer';
|
|||||||
import type { SetupInput } from '../../../../src/init/setup/SetupHttpHandler';
|
import type { SetupInput } from '../../../../src/init/setup/SetupHttpHandler';
|
||||||
import { SetupHttpHandler } from '../../../../src/init/setup/SetupHttpHandler';
|
import { SetupHttpHandler } from '../../../../src/init/setup/SetupHttpHandler';
|
||||||
import type { ErrorHandlerArgs, ErrorHandler } from '../../../../src/ldp/http/ErrorHandler';
|
import type { ErrorHandlerArgs, ErrorHandler } from '../../../../src/ldp/http/ErrorHandler';
|
||||||
import type { RequestParser } from '../../../../src/ldp/http/RequestParser';
|
|
||||||
import type { ResponseDescription } from '../../../../src/ldp/http/response/ResponseDescription';
|
import type { ResponseDescription } from '../../../../src/ldp/http/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from '../../../../src/ldp/http/ResponseWriter';
|
|
||||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
@ -22,22 +20,18 @@ import type { HttpError } from '../../../../src/util/errors/HttpError';
|
|||||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||||
import { MethodNotAllowedHttpError } from '../../../../src/util/errors/MethodNotAllowedHttpError';
|
import { MethodNotAllowedHttpError } from '../../../../src/util/errors/MethodNotAllowedHttpError';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
import { joinUrl } from '../../../../src/util/PathUtil';
|
|
||||||
import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil';
|
import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil';
|
||||||
import { CONTENT_TYPE, SOLID_META } from '../../../../src/util/Vocabularies';
|
import { CONTENT_TYPE, SOLID_META } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
describe('A SetupHttpHandler', (): void => {
|
describe('A SetupHttpHandler', (): void => {
|
||||||
const baseUrl = 'http://test.com/';
|
|
||||||
let request: HttpRequest;
|
let request: HttpRequest;
|
||||||
let requestBody: SetupInput;
|
|
||||||
const response: HttpResponse = {} as any;
|
const response: HttpResponse = {} as any;
|
||||||
|
let operation: Operation;
|
||||||
const viewTemplate = '/templates/view';
|
const viewTemplate = '/templates/view';
|
||||||
const responseTemplate = '/templates/response';
|
const responseTemplate = '/templates/response';
|
||||||
const storageKey = 'completed';
|
const storageKey = 'completed';
|
||||||
let details: RegistrationResponse;
|
let details: RegistrationResponse;
|
||||||
let requestParser: jest.Mocked<RequestParser>;
|
|
||||||
let errorHandler: jest.Mocked<ErrorHandler>;
|
let errorHandler: jest.Mocked<ErrorHandler>;
|
||||||
let responseWriter: jest.Mocked<ResponseWriter>;
|
|
||||||
let registrationManager: jest.Mocked<RegistrationManager>;
|
let registrationManager: jest.Mocked<RegistrationManager>;
|
||||||
let initializer: jest.Mocked<Initializer>;
|
let initializer: jest.Mocked<Initializer>;
|
||||||
let converter: jest.Mocked<RepresentationConverter>;
|
let converter: jest.Mocked<RepresentationConverter>;
|
||||||
@ -45,27 +39,17 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
let handler: SetupHttpHandler;
|
let handler: SetupHttpHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
request = { url: '/setup', method: 'GET', headers: {}} as any;
|
operation = {
|
||||||
requestBody = {};
|
method: 'GET',
|
||||||
|
target: { path: 'http://test.com/setup' },
|
||||||
requestParser = {
|
|
||||||
handleSafe: jest.fn(async(req: HttpRequest): Promise<Operation> => ({
|
|
||||||
target: { path: joinUrl(baseUrl, req.url!) },
|
|
||||||
method: req.method!,
|
|
||||||
body: req.method === 'GET' ?
|
|
||||||
undefined :
|
|
||||||
new BasicRepresentation(JSON.stringify(requestBody), req.headers['content-type'] ?? 'text/plain'),
|
|
||||||
preferences: { type: { 'text/html': 1 }},
|
preferences: { type: { 'text/html': 1 }},
|
||||||
})),
|
};
|
||||||
} as any;
|
|
||||||
|
|
||||||
errorHandler = { handleSafe: jest.fn(({ error }: ErrorHandlerArgs): ResponseDescription => ({
|
errorHandler = { handleSafe: jest.fn(({ error }: ErrorHandlerArgs): ResponseDescription => ({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
data: guardedStreamFrom(`{ "name": "${error.name}", "message": "${error.message}" }`),
|
data: guardedStreamFrom(`{ "name": "${error.name}", "message": "${error.message}" }`),
|
||||||
})) } as any;
|
})) } as any;
|
||||||
|
|
||||||
responseWriter = { handleSafe: jest.fn() } as any;
|
|
||||||
|
|
||||||
initializer = {
|
initializer = {
|
||||||
handleSafe: jest.fn(),
|
handleSafe: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
@ -94,9 +78,6 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
storage = new Map<string, any>() as any;
|
storage = new Map<string, any>() as any;
|
||||||
|
|
||||||
handler = new SetupHttpHandler({
|
handler = new SetupHttpHandler({
|
||||||
requestParser,
|
|
||||||
errorHandler,
|
|
||||||
responseWriter,
|
|
||||||
initializer,
|
initializer,
|
||||||
registrationManager,
|
registrationManager,
|
||||||
converter,
|
converter,
|
||||||
@ -104,23 +85,25 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
storage,
|
storage,
|
||||||
viewTemplate,
|
viewTemplate,
|
||||||
responseTemplate,
|
responseTemplate,
|
||||||
|
errorHandler,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Since all tests check similar things, the test functionality is generalized in here
|
// Since all tests check similar things, the test functionality is generalized in here
|
||||||
async function testPost(input: SetupInput, error?: HttpError): Promise<void> {
|
async function testPost(input: SetupInput, error?: HttpError): Promise<void> {
|
||||||
request.method = 'POST';
|
operation.method = 'POST';
|
||||||
const initialize = Boolean(input.initialize);
|
const initialize = Boolean(input.initialize);
|
||||||
const registration = Boolean(input.registration);
|
const registration = Boolean(input.registration);
|
||||||
requestBody = { initialize, registration };
|
const requestBody = { initialize, registration };
|
||||||
|
if (Object.keys(input).length > 0) {
|
||||||
|
operation.body = new BasicRepresentation(JSON.stringify(requestBody), 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
const result = await handler.handle({ operation, request, response });
|
||||||
|
expect(result).toBeDefined();
|
||||||
expect(initializer.handleSafe).toHaveBeenCalledTimes(!error && initialize ? 1 : 0);
|
expect(initializer.handleSafe).toHaveBeenCalledTimes(!error && initialize ? 1 : 0);
|
||||||
expect(registrationManager.validateInput).toHaveBeenCalledTimes(!error && registration ? 1 : 0);
|
expect(registrationManager.validateInput).toHaveBeenCalledTimes(!error && registration ? 1 : 0);
|
||||||
expect(registrationManager.register).toHaveBeenCalledTimes(!error && registration ? 1 : 0);
|
expect(registrationManager.register).toHaveBeenCalledTimes(!error && registration ? 1 : 0);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
const { response: mockResponse, result } = responseWriter.handleSafe.mock.calls[0][0];
|
|
||||||
expect(mockResponse).toBe(response);
|
|
||||||
let expectedResult: any = { initialize, registration };
|
let expectedResult: any = { initialize, registration };
|
||||||
if (error) {
|
if (error) {
|
||||||
expectedResult = { name: error.name, message: error.message };
|
expectedResult = { name: error.name, message: error.message };
|
||||||
@ -139,10 +122,8 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('returns the view template on GET requests.', async(): Promise<void> => {
|
it('returns the view template on GET requests.', async(): Promise<void> => {
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
const result = await handler.handle({ operation, request, response });
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(result).toBeDefined();
|
||||||
const { response: mockResponse, result } = responseWriter.handleSafe.mock.calls[0][0];
|
|
||||||
expect(mockResponse).toBe(response);
|
|
||||||
expect(JSON.parse(await readableToString(result.data!))).toEqual({});
|
expect(JSON.parse(await readableToString(result.data!))).toEqual({});
|
||||||
expect(result.statusCode).toBe(200);
|
expect(result.statusCode).toBe(200);
|
||||||
expect(result.metadata?.contentType).toBe('text/html');
|
expect(result.metadata?.contentType).toBe('text/html');
|
||||||
@ -160,11 +141,6 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('defaults to an empty body if there is none.', async(): Promise<void> => {
|
it('defaults to an empty body if there is none.', async(): Promise<void> => {
|
||||||
requestParser.handleSafe.mockResolvedValueOnce({
|
|
||||||
target: { path: joinUrl(baseUrl, '/randomPath') },
|
|
||||||
method: 'POST',
|
|
||||||
preferences: { type: { 'text/html': 1 }},
|
|
||||||
});
|
|
||||||
await expect(testPost({})).resolves.toBeUndefined();
|
await expect(testPost({})).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -183,18 +159,18 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('errors on non-GET/POST requests.', async(): Promise<void> => {
|
it('errors on non-GET/POST requests.', async(): Promise<void> => {
|
||||||
request.method = 'PUT';
|
operation.method = 'PUT';
|
||||||
requestBody = { initialize: true, registration: true };
|
const requestBody = { initialize: true, registration: true };
|
||||||
|
operation.body = new BasicRepresentation(JSON.stringify(requestBody), 'application/json');
|
||||||
const error = new MethodNotAllowedHttpError();
|
const error = new MethodNotAllowedHttpError();
|
||||||
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
const result = await handler.handle({ operation, request, response });
|
||||||
|
expect(result).toBeDefined();
|
||||||
expect(initializer.handleSafe).toHaveBeenCalledTimes(0);
|
expect(initializer.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
expect(registrationManager.register).toHaveBeenCalledTimes(0);
|
expect(registrationManager.register).toHaveBeenCalledTimes(0);
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { [APPLICATION_JSON]: 1 }}});
|
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { [APPLICATION_JSON]: 1 }}});
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
const { response: mockResponse, result } = responseWriter.handleSafe.mock.calls[0][0];
|
|
||||||
expect(mockResponse).toBe(response);
|
|
||||||
expect(JSON.parse(await readableToString(result.data!))).toEqual({ name: error.name, message: error.message });
|
expect(JSON.parse(await readableToString(result.data!))).toEqual({ name: error.name, message: error.message });
|
||||||
expect(result.statusCode).toBe(405);
|
expect(result.statusCode).toBe(405);
|
||||||
expect(result.metadata?.contentType).toBe('text/html');
|
expect(result.metadata?.contentType).toBe('text/html');
|
||||||
@ -206,9 +182,7 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
|
|
||||||
it('errors when attempting registration when no RegistrationManager is defined.', async(): Promise<void> => {
|
it('errors when attempting registration when no RegistrationManager is defined.', async(): Promise<void> => {
|
||||||
handler = new SetupHttpHandler({
|
handler = new SetupHttpHandler({
|
||||||
requestParser,
|
|
||||||
errorHandler,
|
errorHandler,
|
||||||
responseWriter,
|
|
||||||
initializer,
|
initializer,
|
||||||
converter,
|
converter,
|
||||||
storageKey,
|
storageKey,
|
||||||
@ -216,8 +190,9 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
viewTemplate,
|
viewTemplate,
|
||||||
responseTemplate,
|
responseTemplate,
|
||||||
});
|
});
|
||||||
request.method = 'POST';
|
operation.method = 'POST';
|
||||||
requestBody = { initialize: false, registration: true };
|
const requestBody = { initialize: false, registration: true };
|
||||||
|
operation.body = new BasicRepresentation(JSON.stringify(requestBody), 'application/json');
|
||||||
const error = new NotImplementedHttpError('This server is not configured to support registration during setup.');
|
const error = new NotImplementedHttpError('This server is not configured to support registration during setup.');
|
||||||
await expect(testPost({ initialize: false, registration: true }, error)).resolves.toBeUndefined();
|
await expect(testPost({ initialize: false, registration: true }, error)).resolves.toBeUndefined();
|
||||||
|
|
||||||
@ -227,9 +202,7 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
|
|
||||||
it('errors when attempting initialization when no Initializer is defined.', async(): Promise<void> => {
|
it('errors when attempting initialization when no Initializer is defined.', async(): Promise<void> => {
|
||||||
handler = new SetupHttpHandler({
|
handler = new SetupHttpHandler({
|
||||||
requestParser,
|
|
||||||
errorHandler,
|
errorHandler,
|
||||||
responseWriter,
|
|
||||||
registrationManager,
|
registrationManager,
|
||||||
converter,
|
converter,
|
||||||
storageKey,
|
storageKey,
|
||||||
@ -237,8 +210,9 @@ describe('A SetupHttpHandler', (): void => {
|
|||||||
viewTemplate,
|
viewTemplate,
|
||||||
responseTemplate,
|
responseTemplate,
|
||||||
});
|
});
|
||||||
request.method = 'POST';
|
operation.method = 'POST';
|
||||||
requestBody = { initialize: true, registration: false };
|
const requestBody = { initialize: true, registration: false };
|
||||||
|
operation.body = new BasicRepresentation(JSON.stringify(requestBody), 'application/json');
|
||||||
const error = new NotImplementedHttpError('This server is not configured with a setup initializer.');
|
const error = new NotImplementedHttpError('This server is not configured with a setup initializer.');
|
||||||
await expect(testPost({ initialize: true, registration: false }, error)).resolves.toBeUndefined();
|
await expect(testPost({ initialize: true, registration: false }, error)).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
@ -1,133 +0,0 @@
|
|||||||
import { CredentialGroup } from '../../../src/authentication/Credentials';
|
|
||||||
import type { CredentialSet } from '../../../src/authentication/Credentials';
|
|
||||||
import type { AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler';
|
|
||||||
import { AuthenticatedLdpHandler } from '../../../src/ldp/AuthenticatedLdpHandler';
|
|
||||||
import { OkResponseDescription } from '../../../src/ldp/http/response/OkResponseDescription';
|
|
||||||
import { ResetResponseDescription } from '../../../src/ldp/http/response/ResetResponseDescription';
|
|
||||||
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
|
||||||
import type { Operation } from '../../../src/ldp/operations/Operation';
|
|
||||||
import type { PermissionSet } from '../../../src/ldp/permissions/Permissions';
|
|
||||||
import { AccessMode } from '../../../src/ldp/permissions/Permissions';
|
|
||||||
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
|
||||||
import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences';
|
|
||||||
import * as LogUtil from '../../../src/logging/LogUtil';
|
|
||||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
|
||||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
|
||||||
|
|
||||||
describe('An AuthenticatedLdpHandler', (): void => {
|
|
||||||
const request: HttpRequest = {} as any;
|
|
||||||
const response: HttpResponse = {} as any;
|
|
||||||
const preferences: RepresentationPreferences = { type: { 'text/turtle': 0.9 }};
|
|
||||||
let operation: Operation;
|
|
||||||
const credentials: CredentialSet = {};
|
|
||||||
const modes: Set<AccessMode> = new Set([ AccessMode.read ]);
|
|
||||||
const permissionSet: PermissionSet = { [CredentialGroup.agent]: { read: true }};
|
|
||||||
const result: ResponseDescription = new ResetResponseDescription();
|
|
||||||
const errorResult: ResponseDescription = { statusCode: 500 };
|
|
||||||
let args: AuthenticatedLdpHandlerArgs;
|
|
||||||
let handler: AuthenticatedLdpHandler;
|
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
|
||||||
operation = { target: { path: 'identifier' }, method: 'GET', preferences };
|
|
||||||
args = {
|
|
||||||
requestParser: {
|
|
||||||
canHandle: jest.fn(),
|
|
||||||
handleSafe: jest.fn().mockResolvedValue(operation),
|
|
||||||
} as any,
|
|
||||||
credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
|
|
||||||
modesExtractor: { handleSafe: jest.fn().mockResolvedValue(modes) } as any,
|
|
||||||
permissionReader: { handleSafe: jest.fn().mockResolvedValue(permissionSet) } as any,
|
|
||||||
authorizer: { handleSafe: jest.fn() } as any,
|
|
||||||
operationHandler: { handleSafe: jest.fn().mockResolvedValue(result) } as any,
|
|
||||||
operationMetadataCollector: { handleSafe: jest.fn() } as any,
|
|
||||||
errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any,
|
|
||||||
responseWriter: { handleSafe: jest.fn() } as any,
|
|
||||||
};
|
|
||||||
handler = new AuthenticatedLdpHandler(args);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can be created.', async(): Promise<void> => {
|
|
||||||
expect(new AuthenticatedLdpHandler(args)).toBeInstanceOf(AuthenticatedLdpHandler);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can not handle the input if the RequestParser rejects it.', async(): Promise<void> => {
|
|
||||||
(args.requestParser.canHandle as jest.Mock).mockRejectedValueOnce(new Error('bad data!'));
|
|
||||||
await expect(handler.canHandle({ request, response })).rejects.toThrow('bad data!');
|
|
||||||
expect(args.requestParser.canHandle).toHaveBeenLastCalledWith(request);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can handle the input if the RequestParser can handle it.', async(): Promise<void> => {
|
|
||||||
await expect(handler.canHandle({ request, response })).resolves.toBeUndefined();
|
|
||||||
expect(args.requestParser.canHandle).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.requestParser.canHandle).toHaveBeenLastCalledWith(request);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can handle input.', async(): Promise<void> => {
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.requestParser.handleSafe).toHaveBeenLastCalledWith(request);
|
|
||||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
|
|
||||||
expect(args.modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
|
|
||||||
expect(args.permissionReader.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.permissionReader.handleSafe).toHaveBeenLastCalledWith({ credentials, identifier: operation.target });
|
|
||||||
expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.authorizer.handleSafe)
|
|
||||||
.toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, modes, permissionSet });
|
|
||||||
expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation);
|
|
||||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls the operation metadata collector if there is response metadata.', async(): Promise<void> => {
|
|
||||||
const metadata = new RepresentationMetadata();
|
|
||||||
const okResult = new OkResponseDescription(metadata);
|
|
||||||
(args.operationHandler.handleSafe as jest.Mock).mockResolvedValueOnce(okResult);
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenLastCalledWith({ operation, metadata });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets preferences to text/plain in case of an error during request parsing.', async(): Promise<void> => {
|
|
||||||
const error = new Error('bad request!');
|
|
||||||
(args.requestParser.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
|
|
||||||
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences: { type: { 'text/plain': 1 }}});
|
|
||||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResult });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets preferences to the request preferences if they were parsed before the error.', async(): Promise<void> => {
|
|
||||||
const error = new Error('bad request!');
|
|
||||||
(args.credentialsExtractor.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
|
|
||||||
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
expect(args.requestParser.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(0);
|
|
||||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });
|
|
||||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
|
||||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResult });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('logs an error if authorization failed.', async(): Promise<void> => {
|
|
||||||
const logger = { verbose: jest.fn() };
|
|
||||||
const mock = jest.spyOn(LogUtil, 'getLoggerFor');
|
|
||||||
mock.mockReturnValueOnce(logger as any);
|
|
||||||
handler = new AuthenticatedLdpHandler(args);
|
|
||||||
(args.authorizer.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad auth!'));
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
|
||||||
expect(logger.verbose).toHaveBeenLastCalledWith('Authorization failed: bad auth!');
|
|
||||||
|
|
||||||
mock.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
@ -5,22 +5,25 @@ import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
|||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
|
|
||||||
describe('A DeleteOperationHandler', (): void => {
|
describe('A DeleteOperationHandler', (): void => {
|
||||||
|
let operation: Operation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions = new BasicConditions({});
|
||||||
const store = {} as unknown as ResourceStore;
|
const store = {} as unknown as ResourceStore;
|
||||||
const handler = new DeleteOperationHandler(store);
|
const handler = new DeleteOperationHandler(store);
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
operation = { method: 'DELETE', target: { path: 'http://test.com/foo' }, preferences: {}, conditions };
|
||||||
store.deleteResource = jest.fn(async(): Promise<any> => undefined);
|
store.deleteResource = jest.fn(async(): Promise<any> => undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only supports DELETE operations.', async(): Promise<void> => {
|
it('only supports DELETE operations.', async(): Promise<void> => {
|
||||||
await expect(handler.canHandle({ method: 'DELETE' } as Operation)).resolves.toBeUndefined();
|
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
operation.method = 'GET';
|
||||||
|
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
|
it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
|
||||||
const result = await handler.handle({ target: { path: 'url' }, conditions } as Operation);
|
const result = await handler.handle({ operation });
|
||||||
expect(store.deleteResource).toHaveBeenCalledTimes(1);
|
expect(store.deleteResource).toHaveBeenCalledTimes(1);
|
||||||
expect(store.deleteResource).toHaveBeenLastCalledWith({ path: 'url' }, conditions);
|
expect(store.deleteResource).toHaveBeenLastCalledWith(operation.target, conditions);
|
||||||
expect(result.statusCode).toBe(205);
|
expect(result.statusCode).toBe(205);
|
||||||
expect(result.metadata).toBeUndefined();
|
expect(result.metadata).toBeUndefined();
|
||||||
expect(result.data).toBeUndefined();
|
expect(result.data).toBeUndefined();
|
||||||
|
@ -6,12 +6,14 @@ import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
|||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
|
|
||||||
describe('A GetOperationHandler', (): void => {
|
describe('A GetOperationHandler', (): void => {
|
||||||
|
let operation: Operation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions = new BasicConditions({});
|
||||||
const preferences = {};
|
const preferences = {};
|
||||||
let store: ResourceStore;
|
let store: ResourceStore;
|
||||||
let handler: GetOperationHandler;
|
let handler: GetOperationHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences, conditions };
|
||||||
store = {
|
store = {
|
||||||
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
||||||
({ binary: false, data: 'data', metadata: 'metadata' } as any)),
|
({ binary: false, data: 'data', metadata: 'metadata' } as any)),
|
||||||
@ -21,16 +23,17 @@ describe('A GetOperationHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('only supports GET operations.', async(): Promise<void> => {
|
it('only supports GET operations.', async(): Promise<void> => {
|
||||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).resolves.toBeUndefined();
|
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||||
await expect(handler.canHandle({ method: 'POST' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
operation.method = 'POST';
|
||||||
|
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the representation from the store with the correct response.', async(): Promise<void> => {
|
it('returns the representation from the store with the correct response.', async(): Promise<void> => {
|
||||||
const result = await handler.handle({ target: { path: 'url' }, preferences, conditions } as Operation);
|
const result = await handler.handle({ operation });
|
||||||
expect(result.statusCode).toBe(200);
|
expect(result.statusCode).toBe(200);
|
||||||
expect(result.metadata).toBe('metadata');
|
expect(result.metadata).toBe('metadata');
|
||||||
expect(result.data).toBe('data');
|
expect(result.data).toBe('data');
|
||||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@ import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
|||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
|
|
||||||
describe('A HeadOperationHandler', (): void => {
|
describe('A HeadOperationHandler', (): void => {
|
||||||
|
let operation: Operation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions = new BasicConditions({});
|
||||||
const preferences = {};
|
const preferences = {};
|
||||||
let store: ResourceStore;
|
let store: ResourceStore;
|
||||||
@ -14,6 +15,7 @@ describe('A HeadOperationHandler', (): void => {
|
|||||||
let data: Readable;
|
let data: Readable;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
operation = { method: 'HEAD', target: { path: 'http://test.com/foo' }, preferences, conditions };
|
||||||
data = { destroy: jest.fn() } as any;
|
data = { destroy: jest.fn() } as any;
|
||||||
store = {
|
store = {
|
||||||
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
||||||
@ -24,18 +26,20 @@ describe('A HeadOperationHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('only supports HEAD operations.', async(): Promise<void> => {
|
it('only supports HEAD operations.', async(): Promise<void> => {
|
||||||
await expect(handler.canHandle({ method: 'HEAD' } as Operation)).resolves.toBeUndefined();
|
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
operation.method = 'GET';
|
||||||
await expect(handler.canHandle({ method: 'POST' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||||
|
operation.method = 'POST';
|
||||||
|
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the representation from the store with the correct response.', async(): Promise<void> => {
|
it('returns the representation from the store with the correct response.', async(): Promise<void> => {
|
||||||
const result = await handler.handle({ target: { path: 'url' }, preferences, conditions } as Operation);
|
const result = await handler.handle({ operation });
|
||||||
expect(result.statusCode).toBe(200);
|
expect(result.statusCode).toBe(200);
|
||||||
expect(result.metadata).toBe('metadata');
|
expect(result.metadata).toBe('metadata');
|
||||||
expect(result.data).toBeUndefined();
|
expect(result.data).toBeUndefined();
|
||||||
expect(data.destroy).toHaveBeenCalledTimes(1);
|
expect(data.destroy).toHaveBeenCalledTimes(1);
|
||||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,35 +1,41 @@
|
|||||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||||
import { PatchOperationHandler } from '../../../../src/ldp/operations/PatchOperationHandler';
|
import { PatchOperationHandler } from '../../../../src/ldp/operations/PatchOperationHandler';
|
||||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||||
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
|
|
||||||
describe('A PatchOperationHandler', (): void => {
|
describe('A PatchOperationHandler', (): void => {
|
||||||
|
let operation: Operation;
|
||||||
|
let body: Representation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions = new BasicConditions({});
|
||||||
const store = {} as unknown as ResourceStore;
|
const store = {} as unknown as ResourceStore;
|
||||||
const handler = new PatchOperationHandler(store);
|
const handler = new PatchOperationHandler(store);
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
body = new BasicRepresentation('', 'text/turtle');
|
||||||
|
operation = { method: 'PATCH', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
||||||
store.modifyResource = jest.fn(async(): Promise<any> => undefined);
|
store.modifyResource = jest.fn(async(): Promise<any> => undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only supports PATCH operations.', async(): Promise<void> => {
|
it('only supports PATCH operations.', async(): Promise<void> => {
|
||||||
await expect(handler.canHandle({ method: 'PATCH' } as Operation)).resolves.toBeUndefined();
|
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
operation.method = 'GET';
|
||||||
|
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
||||||
await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
|
operation.body!.metadata.contentType = undefined;
|
||||||
await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
|
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||||
.rejects.toThrow(BadRequestHttpError);
|
delete operation.body;
|
||||||
|
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
|
it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
|
||||||
const metadata = new RepresentationMetadata('text/turtle');
|
const result = await handler.handle({ operation });
|
||||||
const result = await handler.handle({ target: { path: 'url' }, body: { metadata }, conditions } as Operation);
|
|
||||||
expect(store.modifyResource).toHaveBeenCalledTimes(1);
|
expect(store.modifyResource).toHaveBeenCalledTimes(1);
|
||||||
expect(store.modifyResource).toHaveBeenLastCalledWith({ path: 'url' }, { metadata }, conditions);
|
expect(store.modifyResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
||||||
expect(result.statusCode).toBe(205);
|
expect(result.statusCode).toBe(205);
|
||||||
expect(result.metadata).toBeUndefined();
|
expect(result.metadata).toBeUndefined();
|
||||||
expect(result.data).toBeUndefined();
|
expect(result.data).toBeUndefined();
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||||
import { PostOperationHandler } from '../../../../src/ldp/operations/PostOperationHandler';
|
import { PostOperationHandler } from '../../../../src/ldp/operations/PostOperationHandler';
|
||||||
|
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||||
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||||
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
||||||
@ -9,11 +11,15 @@ import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplemen
|
|||||||
import { SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
import { SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
describe('A PostOperationHandler', (): void => {
|
describe('A PostOperationHandler', (): void => {
|
||||||
|
let operation: Operation;
|
||||||
|
let body: Representation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions = new BasicConditions({});
|
||||||
let store: ResourceStore;
|
let store: ResourceStore;
|
||||||
let handler: PostOperationHandler;
|
let handler: PostOperationHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
body = new BasicRepresentation('', 'text/turtle');
|
||||||
|
operation = { method: 'POST', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
||||||
store = {
|
store = {
|
||||||
addResource: jest.fn(async(): Promise<ResourceIdentifier> => ({ path: 'newPath' } as ResourceIdentifier)),
|
addResource: jest.fn(async(): Promise<ResourceIdentifier> => ({ path: 'newPath' } as ResourceIdentifier)),
|
||||||
} as unknown as ResourceStore;
|
} as unknown as ResourceStore;
|
||||||
@ -21,28 +27,25 @@ describe('A PostOperationHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('only supports POST operations.', async(): Promise<void> => {
|
it('only supports POST operations.', async(): Promise<void> => {
|
||||||
await expect(handler.canHandle({ method: 'POST', body: { }} as Operation))
|
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||||
.resolves.toBeUndefined();
|
operation.method = 'GET';
|
||||||
await expect(handler.canHandle({ method: 'GET', body: { }} as Operation))
|
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||||
.rejects.toThrow(NotImplementedHttpError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
||||||
await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
|
operation.body!.metadata.contentType = undefined;
|
||||||
await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
|
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||||
.rejects.toThrow(BadRequestHttpError);
|
delete operation.body;
|
||||||
|
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds the given representation to the store and returns the correct response.', async(): Promise<void> => {
|
it('adds the given representation to the store and returns the correct response.', async(): Promise<void> => {
|
||||||
const metadata = new RepresentationMetadata('text/turtle');
|
const result = await handler.handle({ operation });
|
||||||
const result = await handler.handle(
|
|
||||||
{ method: 'POST', target: { path: 'url' }, body: { metadata }, conditions } as Operation,
|
|
||||||
);
|
|
||||||
expect(result.statusCode).toBe(201);
|
expect(result.statusCode).toBe(201);
|
||||||
expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
|
expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
|
||||||
expect(result.metadata?.get(SOLID_HTTP.location)?.value).toBe('newPath');
|
expect(result.metadata?.get(SOLID_HTTP.location)?.value).toBe('newPath');
|
||||||
expect(result.data).toBeUndefined();
|
expect(result.data).toBeUndefined();
|
||||||
expect(store.addResource).toHaveBeenCalledTimes(1);
|
expect(store.addResource).toHaveBeenCalledTimes(1);
|
||||||
expect(store.addResource).toHaveBeenLastCalledWith({ path: 'url' }, { metadata }, conditions);
|
expect(store.addResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,36 +1,42 @@
|
|||||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||||
import { PutOperationHandler } from '../../../../src/ldp/operations/PutOperationHandler';
|
import { PutOperationHandler } from '../../../../src/ldp/operations/PutOperationHandler';
|
||||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||||
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
import { BasicConditions } from '../../../../src/storage/BasicConditions';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
|
|
||||||
describe('A PutOperationHandler', (): void => {
|
describe('A PutOperationHandler', (): void => {
|
||||||
|
let operation: Operation;
|
||||||
|
let body: Representation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions = new BasicConditions({});
|
||||||
const store = {} as unknown as ResourceStore;
|
const store = {} as unknown as ResourceStore;
|
||||||
const handler = new PutOperationHandler(store);
|
const handler = new PutOperationHandler(store);
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
body = new BasicRepresentation('', 'text/turtle');
|
||||||
|
operation = { method: 'PUT', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
store.setRepresentation = jest.fn(async(): Promise<any> => {});
|
store.setRepresentation = jest.fn(async(): Promise<any> => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only supports PUT operations.', async(): Promise<void> => {
|
it('only supports PUT operations.', async(): Promise<void> => {
|
||||||
await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
|
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
|
||||||
await expect(handler.canHandle({ method: 'PUT' } as Operation)).resolves.toBeUndefined();
|
operation.method = 'GET';
|
||||||
|
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
it('errors if there is no body or content-type.', async(): Promise<void> => {
|
||||||
await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
|
operation.body!.metadata.contentType = undefined;
|
||||||
await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
|
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||||
.rejects.toThrow(BadRequestHttpError);
|
delete operation.body;
|
||||||
|
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the representation in the store and returns the correct response.', async(): Promise<void> => {
|
it('sets the representation in the store and returns the correct response.', async(): Promise<void> => {
|
||||||
const metadata = new RepresentationMetadata('text/turtle');
|
const result = await handler.handle({ operation });
|
||||||
const result = await handler.handle({ target: { path: 'url' }, body: { metadata }, conditions } as Operation);
|
|
||||||
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
|
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
|
||||||
expect(store.setRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, { metadata }, conditions);
|
expect(store.setRepresentation).toHaveBeenLastCalledWith(operation.target, body, conditions);
|
||||||
expect(result.statusCode).toBe(205);
|
expect(result.statusCode).toBe(205);
|
||||||
expect(result.metadata).toBeUndefined();
|
expect(result.metadata).toBeUndefined();
|
||||||
expect(result.data).toBeUndefined();
|
expect(result.data).toBeUndefined();
|
||||||
|
78
test/unit/server/AuthorizingHttpHandler.test.ts
Normal file
78
test/unit/server/AuthorizingHttpHandler.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { CredentialGroup } from '../../../src/authentication/Credentials';
|
||||||
|
import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
|
||||||
|
import type { Authorizer } from '../../../src/authorization/Authorizer';
|
||||||
|
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
|
||||||
|
import type { Operation } from '../../../src/ldp/operations/Operation';
|
||||||
|
import type { ModesExtractor } from '../../../src/ldp/permissions/ModesExtractor';
|
||||||
|
import { AccessMode } from '../../../src/ldp/permissions/Permissions';
|
||||||
|
import { AuthorizingHttpHandler } from '../../../src/server/AuthorizingHttpHandler';
|
||||||
|
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||||
|
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
|
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
|
||||||
|
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
|
||||||
|
|
||||||
|
describe('An AuthorizingHttpHandler', (): void => {
|
||||||
|
const credentials = { [CredentialGroup.public]: {}};
|
||||||
|
const modes = new Set([ AccessMode.read ]);
|
||||||
|
const permissionSet = { [CredentialGroup.public]: { read: true }};
|
||||||
|
const request: HttpRequest = {} as any;
|
||||||
|
const response: HttpResponse = {} as any;
|
||||||
|
let operation: Operation;
|
||||||
|
let credentialsExtractor: jest.Mocked<CredentialsExtractor>;
|
||||||
|
let modesExtractor: jest.Mocked<ModesExtractor>;
|
||||||
|
let permissionReader: jest.Mocked<PermissionReader>;
|
||||||
|
let authorizer: jest.Mocked<Authorizer>;
|
||||||
|
let source: jest.Mocked<OperationHttpHandler>;
|
||||||
|
let handler: AuthorizingHttpHandler;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
operation = {
|
||||||
|
target: { path: 'http://test.com/foo' },
|
||||||
|
method: 'GET',
|
||||||
|
preferences: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialsExtractor = {
|
||||||
|
handleSafe: jest.fn().mockResolvedValue(credentials),
|
||||||
|
} as any;
|
||||||
|
modesExtractor = {
|
||||||
|
handleSafe: jest.fn().mockResolvedValue(modes),
|
||||||
|
} as any;
|
||||||
|
permissionReader = {
|
||||||
|
handleSafe: jest.fn().mockResolvedValue(permissionSet),
|
||||||
|
} as any;
|
||||||
|
authorizer = {
|
||||||
|
handleSafe: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
source = {
|
||||||
|
handleSafe: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
handler = new AuthorizingHttpHandler(
|
||||||
|
{ credentialsExtractor, modesExtractor, permissionReader, authorizer, operationHandler: source },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes through all the steps and calls the source.', async(): Promise<void> => {
|
||||||
|
await expect(handler.handle({ request, response, operation })).resolves.toBeUndefined();
|
||||||
|
expect(credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
|
||||||
|
expect(modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||||
|
expect(permissionReader.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(permissionReader.handleSafe).toHaveBeenLastCalledWith({ credentials, identifier: operation.target });
|
||||||
|
expect(authorizer.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(authorizer.handleSafe)
|
||||||
|
.toHaveBeenLastCalledWith({ credentials, identifier: operation.target, modes, permissionSet });
|
||||||
|
expect(source.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(source.handleSafe).toHaveBeenLastCalledWith({ request, response, operation });
|
||||||
|
expect(operation.permissionSet).toBe(permissionSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if authorization fails.', async(): Promise<void> => {
|
||||||
|
const error = new ForbiddenHttpError();
|
||||||
|
authorizer.handleSafe.mockRejectedValueOnce(error);
|
||||||
|
await expect(handler.handle({ request, response, operation })).rejects.toThrow(error);
|
||||||
|
expect(source.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
@ -1,65 +1,75 @@
|
|||||||
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
|
import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler';
|
||||||
import type { RequestParser } from '../../../src/ldp/http/RequestParser';
|
import type { RequestParser } from '../../../src/ldp/http/RequestParser';
|
||||||
|
import { OkResponseDescription } from '../../../src/ldp/http/response/OkResponseDescription';
|
||||||
import { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
import { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
|
||||||
|
import type { OperationMetadataCollector } from '../../../src/ldp/operations/metadata/OperationMetadataCollector';
|
||||||
import type { Operation } from '../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../src/ldp/operations/Operation';
|
||||||
import type { BaseHttpHandlerArgs } from '../../../src/server/BaseHttpHandler';
|
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import { BaseHttpHandler } from '../../../src/server/BaseHttpHandler';
|
|
||||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
|
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
|
||||||
|
import { ParsingHttpHandler } from '../../../src/server/ParsingHttpHandler';
|
||||||
|
|
||||||
class DummyHttpHandler extends BaseHttpHandler {
|
describe('A ParsingHttpHandler', (): void => {
|
||||||
public constructor(args: BaseHttpHandlerArgs) {
|
|
||||||
super(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleOperation(): Promise<ResponseDescription | undefined> {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('A BaseHttpHandler', (): void => {
|
|
||||||
const request: HttpRequest = {} as any;
|
const request: HttpRequest = {} as any;
|
||||||
const response: HttpResponse = {} as any;
|
const response: HttpResponse = {} as any;
|
||||||
const preferences = { type: { 'text/html': 1 }};
|
const preferences = { type: { 'text/html': 1 }};
|
||||||
const operation: Operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences };
|
const operation: Operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences };
|
||||||
const errorResponse = new ResponseDescription(400);
|
const errorResponse = new ResponseDescription(400);
|
||||||
let requestParser: jest.Mocked<RequestParser>;
|
let requestParser: jest.Mocked<RequestParser>;
|
||||||
|
let metadataCollector: jest.Mocked<OperationMetadataCollector>;
|
||||||
let errorHandler: jest.Mocked<ErrorHandler>;
|
let errorHandler: jest.Mocked<ErrorHandler>;
|
||||||
let responseWriter: jest.Mocked<ResponseWriter>;
|
let responseWriter: jest.Mocked<ResponseWriter>;
|
||||||
let handler: jest.Mocked<DummyHttpHandler>;
|
let source: jest.Mocked<OperationHttpHandler>;
|
||||||
|
let handler: ParsingHttpHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
requestParser = { handleSafe: jest.fn().mockResolvedValue(operation) } as any;
|
requestParser = { handleSafe: jest.fn().mockResolvedValue(operation) } as any;
|
||||||
|
metadataCollector = { handleSafe: jest.fn() } as any;
|
||||||
errorHandler = { handleSafe: jest.fn().mockResolvedValue(errorResponse) } as any;
|
errorHandler = { handleSafe: jest.fn().mockResolvedValue(errorResponse) } as any;
|
||||||
responseWriter = { handleSafe: jest.fn() } as any;
|
responseWriter = { handleSafe: jest.fn() } as any;
|
||||||
|
|
||||||
handler = new DummyHttpHandler({ requestParser, errorHandler, responseWriter }) as any;
|
source = {
|
||||||
handler.handleOperation = jest.fn();
|
handleSafe: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
handler = new ParsingHttpHandler(
|
||||||
|
{ requestParser, metadataCollector, errorHandler, responseWriter, operationHandler: source },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the handleOperation function with the generated operation.', async(): Promise<void> => {
|
it('calls the source with the generated operation.', async(): Promise<void> => {
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
expect(handler.handleOperation).toHaveBeenCalledTimes(1);
|
expect(source.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(handler.handleOperation).toHaveBeenLastCalledWith(operation, request, response);
|
expect(source.handleSafe).toHaveBeenLastCalledWith({ operation, request, response });
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the responseWriter if there is a response.', async(): Promise<void> => {
|
it('calls the responseWriter if there is a response.', async(): Promise<void> => {
|
||||||
const result = new ResponseDescription(200);
|
const result = new ResponseDescription(200);
|
||||||
handler.handleOperation.mockResolvedValueOnce(result);
|
source.handleSafe.mockResolvedValueOnce(result);
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
expect(handler.handleOperation).toHaveBeenCalledTimes(1);
|
expect(source.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(handler.handleOperation).toHaveBeenLastCalledWith(operation, request, response);
|
expect(source.handleSafe).toHaveBeenLastCalledWith({ operation, request, response });
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls the operation metadata collector if there is response metadata.', async(): Promise<void> => {
|
||||||
|
const metadata = new RepresentationMetadata();
|
||||||
|
const okResult = new OkResponseDescription(metadata);
|
||||||
|
source.handleSafe.mockResolvedValueOnce(okResult);
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(metadataCollector.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(metadataCollector.handleSafe).toHaveBeenLastCalledWith({ operation, metadata });
|
||||||
|
});
|
||||||
|
|
||||||
it('calls the error handler if something goes wrong.', async(): Promise<void> => {
|
it('calls the error handler if something goes wrong.', async(): Promise<void> => {
|
||||||
const error = new Error('bad data');
|
const error = new Error('bad data');
|
||||||
handler.handleOperation.mockRejectedValueOnce(error);
|
source.handleSafe.mockRejectedValueOnce(error);
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });
|
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });
|
Loading…
x
Reference in New Issue
Block a user