feat: Move WAC-Allow metadata collecting to HTTP handler

This depends on all auth related handlers to cache their results.
This allows us to remove the permission field from Operation.
This commit is contained in:
Joachim Van Herwegen 2022-11-18 10:54:19 +01:00
parent 59e64a22ea
commit 6ad5c0c797
16 changed files with 218 additions and 176 deletions

View File

@ -6,7 +6,6 @@
"@id": "urn:solid-server:default:SetupParsingHandler", "@id": "urn:solid-server:default:SetupParsingHandler",
"@type": "ParsingHttpHandler", "@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": { "args_operationHandler": {

View File

@ -13,7 +13,6 @@
"@id": "urn:solid-server:default:NotificationParsingHandler", "@id": "urn:solid-server:default:NotificationParsingHandler",
"@type": "ParsingHttpHandler", "@type": "ParsingHttpHandler",
"requestParser": { "@id": "urn:solid-server:default:RequestParser" }, "requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"operationHandler": { "operationHandler": {

View File

@ -16,7 +16,6 @@
"handler": { "handler": {
"@type": "ParsingHttpHandler", "@type": "ParsingHttpHandler",
"requestParser": { "@id": "urn:solid-server:default:RequestParser" }, "requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" },
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"operationHandler": { "operationHandler": {

View File

@ -22,7 +22,6 @@
"@id": "urn:solid-server:default:IdentityProviderParsingHandler", "@id": "urn:solid-server:default:IdentityProviderParsingHandler",
"@type": "ParsingHttpHandler", "@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": { "args_operationHandler": {

View File

@ -15,7 +15,6 @@
"@id": "urn:solid-server:default:LdpHandler", "@id": "urn:solid-server:default:LdpHandler",
"@type": "ParsingHttpHandler", "@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": { "args_operationHandler": {
@ -24,7 +23,13 @@
"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": {
"@type": "WacAllowHttpHandler",
"args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
"args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
"args_permissionReader": { "@id": "urn:solid-server:default:PermissionReader" },
"args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" }
}
} }
} }
] ]

View File

@ -1,4 +1,3 @@
import type { PermissionMap } from '../authorization/permissions/Permissions';
import type { Conditions } from '../storage/Conditions'; import type { Conditions } from '../storage/Conditions';
import type { Representation } from './representation/Representation'; import type { Representation } from './representation/Representation';
import type { RepresentationPreferences } from './representation/RepresentationPreferences'; import type { RepresentationPreferences } from './representation/RepresentationPreferences';
@ -24,10 +23,6 @@ export interface Operation {
* Conditions the resource must fulfill for a valid operation. * Conditions the resource must fulfill for a valid operation.
*/ */
conditions?: Conditions; conditions?: Conditions;
/**
* The permissions available for the current operation.
*/
availablePermissions?: PermissionMap;
/** /**
* Representation of the body and metadata headers. * Representation of the body and metadata headers.
*/ */

View File

@ -1,19 +0,0 @@
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
import type { Operation } from '../../Operation';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
export interface OperationMetadataCollectorInput {
/**
* Metadata to update with permission knowledge.
*/
metadata: RepresentationMetadata;
/**
* Operation corresponding to the request.
*/
operation: Operation;
}
/**
* Adds metadata about the operation to the provided metadata object.
*/
export abstract class OperationMetadataCollector extends AsyncHandler<OperationMetadataCollectorInput> {}

View File

@ -1,39 +0,0 @@
import { AclMode } from '../../../authorization/permissions/AclPermission';
import type { AclPermission } from '../../../authorization/permissions/AclPermission';
import { AccessMode } from '../../../authorization/permissions/Permissions';
import { ACL, AUTH } from '../../../util/Vocabularies';
import type { OperationMetadataCollectorInput } from './OperationMetadataCollector';
import { OperationMetadataCollector } from './OperationMetadataCollector';
const VALID_METHODS = new Set([ 'HEAD', 'GET' ]);
const VALID_ACL_MODES = new Set([ AccessMode.read, AccessMode.write, AccessMode.append, AclMode.control ]);
/**
* Indicates which acl permissions are available on the requested resource.
* Only adds public and agent permissions for HEAD/GET requests.
*/
export class WebAclMetadataCollector extends OperationMetadataCollector {
public async handle({ metadata, operation }: OperationMetadataCollectorInput): Promise<void> {
const permissionSet = operation.availablePermissions?.get(operation.target);
if (!permissionSet || !VALID_METHODS.has(operation.method)) {
return;
}
const user: AclPermission = permissionSet.agent ?? {};
const everyone: AclPermission = permissionSet.public ?? {};
const modes = new Set<AccessMode>([ ...Object.keys(user), ...Object.keys(everyone) ] as AccessMode[]);
for (const mode of modes) {
if (VALID_ACL_MODES.has(mode)) {
const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control';
if (everyone[mode]) {
metadata.add(AUTH.terms.publicMode, ACL.terms[capitalizedMode]);
}
if (user[mode]) {
metadata.add(AUTH.terms.userMode, ACL.terms[capitalizedMode]);
}
}
}
}
}

View File

@ -82,10 +82,6 @@ export * from './http/input/preferences/PreferenceParser';
export * from './http/input/BasicRequestParser'; export * from './http/input/BasicRequestParser';
export * from './http/input/RequestParser'; export * from './http/input/RequestParser';
// HTTP/LDP/Metadata
export * from './http/ldp/metadata/OperationMetadataCollector';
export * from './http/ldp/metadata/WebAclMetadataCollector';
// HTTP/LDP // HTTP/LDP
export * from './http/ldp/DeleteOperationHandler'; export * from './http/ldp/DeleteOperationHandler';
export * from './http/ldp/GetOperationHandler'; export * from './http/ldp/GetOperationHandler';
@ -295,6 +291,7 @@ export * from './server/HttpServerFactory';
export * from './server/OperationHttpHandler'; export * from './server/OperationHttpHandler';
export * from './server/ParsingHttpHandler'; export * from './server/ParsingHttpHandler';
export * from './server/ServerConfigurator'; export * from './server/ServerConfigurator';
export * from './server/WacAllowHttpHandler';
export * from './server/WebSocketServerConfigurator'; export * from './server/WebSocketServerConfigurator';
// Server/Description // Server/Description

View File

@ -71,7 +71,6 @@ export class AuthorizingHttpHandler extends OperationHttpHandler {
try { try {
await this.authorizer.handleSafe({ credentials, requestedModes, availablePermissions }); await this.authorizer.handleSafe({ credentials, requestedModes, availablePermissions });
operation.availablePermissions = availablePermissions;
} catch (error: unknown) { } catch (error: unknown) {
this.logger.verbose(`Authorization failed: ${(error as any).message}`); this.logger.verbose(`Authorization failed: ${(error as any).message}`);
throw error; throw error;

View File

@ -1,5 +1,4 @@
import type { RequestParser } from '../http/input/RequestParser'; import type { RequestParser } from '../http/input/RequestParser';
import type { OperationMetadataCollector } from '../http/ldp/metadata/OperationMetadataCollector';
import type { ErrorHandler } from '../http/output/error/ErrorHandler'; import type { ErrorHandler } from '../http/output/error/ErrorHandler';
import type { ResponseDescription } from '../http/output/response/ResponseDescription'; import type { ResponseDescription } from '../http/output/response/ResponseDescription';
import type { ResponseWriter } from '../http/output/ResponseWriter'; import type { ResponseWriter } from '../http/output/ResponseWriter';
@ -17,10 +16,6 @@ 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.
*/ */
@ -46,7 +41,6 @@ export class ParsingHttpHandler extends HttpHandler {
private readonly requestParser: RequestParser; private readonly requestParser: RequestParser;
private readonly errorHandler: ErrorHandler; private readonly errorHandler: ErrorHandler;
private readonly responseWriter: ResponseWriter; private readonly responseWriter: ResponseWriter;
private readonly metadataCollector: OperationMetadataCollector;
private readonly operationHandler: OperationHttpHandler; private readonly operationHandler: OperationHttpHandler;
public constructor(args: ParsingHttpHandlerArgs) { public constructor(args: ParsingHttpHandlerArgs) {
@ -54,7 +48,6 @@ export class ParsingHttpHandler extends HttpHandler {
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; this.operationHandler = args.operationHandler;
} }
@ -80,10 +73,6 @@ export class ParsingHttpHandler extends HttpHandler {
const operation = await this.requestParser.handleSafe(request); const operation = await this.requestParser.handleSafe(request);
const result = await this.operationHandler.handleSafe({ operation, request, response }); const 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}`);
return result; return result;
} }

View File

@ -0,0 +1,91 @@
import type { Credentials } from '../authentication/Credentials';
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
import type { PermissionReader } from '../authorization/PermissionReader';
import { AclMode } from '../authorization/permissions/AclPermission';
import type { AclPermission } from '../authorization/permissions/AclPermission';
import type { ModesExtractor } from '../authorization/permissions/ModesExtractor';
import type { PermissionSet } from '../authorization/permissions/Permissions';
import { AccessMode } from '../authorization/permissions/Permissions';
import type { ResponseDescription } from '../http/output/response/ResponseDescription';
import type { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
import { getLoggerFor } from '../logging/LogUtil';
import { ACL, AUTH } from '../util/Vocabularies';
import type { OperationHttpHandlerInput } from './OperationHttpHandler';
import { OperationHttpHandler } from './OperationHttpHandler';
const VALID_METHODS = new Set([ 'HEAD', 'GET' ]);
const VALID_ACL_MODES = new Set([ AccessMode.read, AccessMode.write, AccessMode.append, AclMode.control ]);
export interface WacAllowHttpHandlerArgs {
credentialsExtractor: CredentialsExtractor;
modesExtractor: ModesExtractor;
permissionReader: PermissionReader;
operationHandler: OperationHttpHandler;
}
/**
* Adds all the available permissions to the response metadata,
* which can be used to generate the correct WAC-Allow header.
*
* This class does many things similar to the {@link AuthorizingHttpHandler},
* so in general it is a good idea to make sure all these classes cache their results.
*/
export class WacAllowHttpHandler extends OperationHttpHandler {
private readonly logger = getLoggerFor(this);
private readonly credentialsExtractor: CredentialsExtractor;
private readonly modesExtractor: ModesExtractor;
private readonly permissionReader: PermissionReader;
private readonly operationHandler: OperationHttpHandler;
public constructor(args: WacAllowHttpHandlerArgs) {
super();
this.credentialsExtractor = args.credentialsExtractor;
this.modesExtractor = args.modesExtractor;
this.permissionReader = args.permissionReader;
this.operationHandler = args.operationHandler;
}
public async handle(input: OperationHttpHandlerInput): Promise<ResponseDescription> {
const { request, operation } = input;
const response = await this.operationHandler.handleSafe(input);
const { metadata } = response;
// WAC-Allow is only needed for HEAD/GET requests
if (!VALID_METHODS.has(operation.method) || !metadata) {
return response;
}
this.logger.debug('Determining available permissions.');
const credentials: Credentials = await this.credentialsExtractor.handleSafe(request);
const requestedModes = await this.modesExtractor.handleSafe(operation);
const availablePermissions = await this.permissionReader.handleSafe({ credentials, requestedModes });
const permissionSet = availablePermissions.get(operation.target);
if (permissionSet) {
this.logger.debug('Adding WAC-Allow metadata.');
this.addWacAllowMetadata(metadata, permissionSet);
}
return response;
}
private addWacAllowMetadata(metadata: RepresentationMetadata, permissionSet: PermissionSet): void {
const user: AclPermission = permissionSet.agent ?? {};
const everyone: AclPermission = permissionSet.public ?? {};
const modes = new Set<AccessMode>([ ...Object.keys(user), ...Object.keys(everyone) ] as AccessMode[]);
for (const mode of modes) {
if (VALID_ACL_MODES.has(mode)) {
const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control';
if (everyone[mode]) {
metadata.add(AUTH.terms.publicMode, ACL.terms[capitalizedMode]);
}
if (user[mode]) {
metadata.add(AUTH.terms.userMode, ACL.terms[capitalizedMode]);
}
}
}
}
}

View File

@ -1,75 +0,0 @@
import 'jest-rdf';
import type { AclPermission } from '../../../../../src/authorization/permissions/AclPermission';
import { WebAclMetadataCollector } from '../../../../../src/http/ldp/metadata/WebAclMetadataCollector';
import type { Operation } from '../../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import { IdentifierMap } from '../../../../../src/util/map/IdentifierMap';
import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
describe('A WebAclMetadataCollector', (): void => {
const target = { path: 'http://example.com/foo' };
let operation: Operation;
let metadata: RepresentationMetadata;
const writer = new WebAclMetadataCollector();
beforeEach(async(): Promise<void> => {
operation = {
method: 'GET',
target,
preferences: {},
body: new BasicRepresentation(),
};
metadata = new RepresentationMetadata();
});
it('adds no metadata if there is no target entry.', async(): Promise<void> => {
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
operation.availablePermissions = new IdentifierMap();
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('adds no metadata if there are no permissions.', async(): Promise<void> => {
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
operation.availablePermissions = new IdentifierMap([[ target, {}]]);
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('adds no metadata if the method is wrong.', async(): Promise<void> => {
operation.availablePermissions = new IdentifierMap(
[[ target, { public: { read: true, write: false }}]],
);
operation.method = 'DELETE';
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('adds corresponding metadata for all permissions present.', async(): Promise<void> => {
operation.availablePermissions = new IdentifierMap([[ target, {
agent: { read: true, write: true, control: false } as AclPermission,
public: { read: true, write: false },
}]]);
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(3);
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
});
it('ignores unknown modes.', async(): Promise<void> => {
operation.availablePermissions = new IdentifierMap([[ target, {
agent: { read: true, create: true },
public: { read: true },
}]]);
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(2);
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read ]);
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
});
});

View File

@ -15,7 +15,7 @@ import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/Iden
describe('An AuthorizingHttpHandler', (): void => { describe('An AuthorizingHttpHandler', (): void => {
const credentials = { }; const credentials = { };
const target = { path: 'http://test.com/foo' }; const target = { path: 'http://example.com/foo' };
const requestedModes: AccessMap = new IdentifierSetMultiMap<AccessMode>([[ target, AccessMode.read ]]); const requestedModes: AccessMap = new IdentifierSetMultiMap<AccessMode>([[ target, AccessMode.read ]]);
const availablePermissions: PermissionMap = new IdentifierMap( const availablePermissions: PermissionMap = new IdentifierMap(
[[ target, { public: { read: true }}]], [[ target, { public: { read: true }}]],
@ -71,7 +71,6 @@ describe('An AuthorizingHttpHandler', (): void => {
expect(authorizer.handleSafe).toHaveBeenLastCalledWith({ credentials, requestedModes, availablePermissions }); expect(authorizer.handleSafe).toHaveBeenLastCalledWith({ credentials, requestedModes, availablePermissions });
expect(source.handleSafe).toHaveBeenCalledTimes(1); expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(source.handleSafe).toHaveBeenLastCalledWith({ request, response, operation }); expect(source.handleSafe).toHaveBeenLastCalledWith({ request, response, operation });
expect(operation.availablePermissions).toBe(availablePermissions);
}); });
it('errors if authorization fails.', async(): Promise<void> => { it('errors if authorization fails.', async(): Promise<void> => {

View File

@ -1,8 +1,6 @@
import type { RequestParser } from '../../../src/http/input/RequestParser'; import type { RequestParser } from '../../../src/http/input/RequestParser';
import type { OperationMetadataCollector } from '../../../src/http/ldp/metadata/OperationMetadataCollector';
import type { Operation } from '../../../src/http/Operation'; import type { Operation } from '../../../src/http/Operation';
import type { ErrorHandler } from '../../../src/http/output/error/ErrorHandler'; import type { ErrorHandler } from '../../../src/http/output/error/ErrorHandler';
import { OkResponseDescription } from '../../../src/http/output/response/OkResponseDescription';
import { ResponseDescription } from '../../../src/http/output/response/ResponseDescription'; import { ResponseDescription } from '../../../src/http/output/response/ResponseDescription';
import type { ResponseWriter } from '../../../src/http/output/ResponseWriter'; import type { ResponseWriter } from '../../../src/http/output/ResponseWriter';
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation'; import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
@ -20,7 +18,6 @@ describe('A ParsingHttpHandler', (): void => {
const operation: Operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences: {}, body }; const operation: Operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences: {}, body };
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 source: jest.Mocked<OperationHttpHandler>; let source: jest.Mocked<OperationHttpHandler>;
@ -28,7 +25,6 @@ describe('A ParsingHttpHandler', (): void => {
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;
@ -37,7 +33,7 @@ describe('A ParsingHttpHandler', (): void => {
} as any; } as any;
handler = new ParsingHttpHandler( handler = new ParsingHttpHandler(
{ requestParser, metadataCollector, errorHandler, responseWriter, operationHandler: source }, { requestParser, errorHandler, responseWriter, operationHandler: source },
); );
}); });
@ -60,15 +56,6 @@ describe('A ParsingHttpHandler', (): void => {
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');
source.handleSafe.mockRejectedValueOnce(error); source.handleSafe.mockRejectedValueOnce(error);

View File

@ -0,0 +1,117 @@
import 'jest-rdf';
import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
import type { AclPermission } from '../../../src/authorization/permissions/AclPermission';
import type { ModesExtractor } from '../../../src/authorization/permissions/ModesExtractor';
import type { Operation } from '../../../src/http/Operation';
import { OkResponseDescription } from '../../../src/http/output/response/OkResponseDescription';
import { ResponseDescription } from '../../../src/http/output/response/ResponseDescription';
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../src/server/HttpResponse';
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
import { WacAllowHttpHandler } from '../../../src/server/WacAllowHttpHandler';
import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/IdentifierMap';
import { ACL, AUTH } from '../../../src/util/Vocabularies';
describe('A WacAllowHttpHandler', (): void => {
const target = { path: 'http://example.com/foo' };
const request: HttpRequest = {} as any;
const response: HttpResponse = {} as any;
let output: ResponseDescription;
let operation: Operation;
let credentialsExtractor: jest.Mocked<CredentialsExtractor>;
let modesExtractor: jest.Mocked<ModesExtractor>;
let permissionReader: jest.Mocked<PermissionReader>;
let source: jest.Mocked<OperationHttpHandler>;
let handler: WacAllowHttpHandler;
beforeEach(async(): Promise<void> => {
output = new OkResponseDescription(new RepresentationMetadata());
operation = {
target,
method: 'GET',
preferences: {},
body: new BasicRepresentation(),
};
credentialsExtractor = {
handleSafe: jest.fn().mockResolvedValue({}),
} as any;
modesExtractor = {
handleSafe: jest.fn().mockResolvedValue(new IdentifierSetMultiMap()),
} as any;
permissionReader = {
handleSafe: jest.fn().mockResolvedValue(new IdentifierMap()),
} as any;
source = {
handleSafe: jest.fn().mockResolvedValue(output),
} as any;
handler = new WacAllowHttpHandler(
{ credentialsExtractor, modesExtractor, permissionReader, operationHandler: source },
);
});
it('adds permission metadata.', async(): Promise<void> => {
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap([[ target, {
agent: { read: true, write: true, control: false } as AclPermission,
public: { read: true, write: false },
}]]));
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(output.metadata!.quads()).toHaveLength(3);
expect(output.metadata!.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
expect(output.metadata!.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(source.handleSafe).toHaveBeenLastCalledWith({ operation, request, response });
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: await credentialsExtractor.handleSafe.mock.results[0].value,
requestedModes: await modesExtractor.handleSafe.mock.results[0].value,
});
});
it('adds no permissions for credential groups that are not defined.', async(): Promise<void> => {
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap([[ target, {}]]));
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(output.metadata!.quads()).toHaveLength(0);
});
it('adds no permissions if none of them are on the target.', async(): Promise<void> => {
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap([[{ path: 'http://example/other' }, {
agent: { read: true, write: true, control: false } as AclPermission,
public: { read: true, write: false },
}]]));
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(output.metadata!.quads()).toHaveLength(0);
});
it('immediately returns the source output if the operation method is not GET or HEAD.', async(): Promise<void> => {
operation.method = 'DELETE';
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(credentialsExtractor.handleSafe).toHaveBeenCalledTimes(0);
expect(modesExtractor.handleSafe).toHaveBeenCalledTimes(0);
expect(permissionReader.handleSafe).toHaveBeenCalledTimes(0);
});
it('immediately returns the source output if the output response has no metadata.', async(): Promise<void> => {
output = new ResponseDescription(200);
source.handleSafe.mockResolvedValue(output);
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(credentialsExtractor.handleSafe).toHaveBeenCalledTimes(0);
expect(modesExtractor.handleSafe).toHaveBeenCalledTimes(0);
expect(permissionReader.handleSafe).toHaveBeenCalledTimes(0);
});
});