From 6ad5c0c7977f4b53fe9d2249161b6157d056f9bb Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 18 Nov 2022 10:54:19 +0100 Subject: [PATCH] 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. --- config/app/setup/handlers/setup.json | 1 - .../http/handler/handlers/notifications.json | 1 - .../handler/handlers/storage-description.json | 1 - config/identity/handler/default.json | 1 - config/ldp/handler/default.json | 9 +- src/http/Operation.ts | 5 - .../metadata/OperationMetadataCollector.ts | 19 --- .../ldp/metadata/WebAclMetadataCollector.ts | 39 ------ src/index.ts | 5 +- src/server/AuthorizingHttpHandler.ts | 1 - src/server/ParsingHttpHandler.ts | 11 -- src/server/WacAllowHttpHandler.ts | 91 ++++++++++++++ .../metadata/WebAclMetadataCollector.test.ts | 75 ----------- .../server/AuthorizingHttpHandler.test.ts | 3 +- test/unit/server/ParsingHttpHandler.test.ts | 15 +-- test/unit/server/WacAllowHttpHandler.test.ts | 117 ++++++++++++++++++ 16 files changed, 218 insertions(+), 176 deletions(-) delete mode 100644 src/http/ldp/metadata/OperationMetadataCollector.ts delete mode 100644 src/http/ldp/metadata/WebAclMetadataCollector.ts create mode 100644 src/server/WacAllowHttpHandler.ts delete mode 100644 test/unit/http/ldp/metadata/WebAclMetadataCollector.test.ts create mode 100644 test/unit/server/WacAllowHttpHandler.test.ts diff --git a/config/app/setup/handlers/setup.json b/config/app/setup/handlers/setup.json index 7764354ea..226e680d3 100644 --- a/config/app/setup/handlers/setup.json +++ b/config/app/setup/handlers/setup.json @@ -6,7 +6,6 @@ "@id": "urn:solid-server:default:SetupParsingHandler", "@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": { diff --git a/config/http/handler/handlers/notifications.json b/config/http/handler/handlers/notifications.json index 73d9ec046..e3dd8c764 100644 --- a/config/http/handler/handlers/notifications.json +++ b/config/http/handler/handlers/notifications.json @@ -13,7 +13,6 @@ "@id": "urn:solid-server:default:NotificationParsingHandler", "@type": "ParsingHttpHandler", "requestParser": { "@id": "urn:solid-server:default:RequestParser" }, - "metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" }, "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, "operationHandler": { diff --git a/config/http/handler/handlers/storage-description.json b/config/http/handler/handlers/storage-description.json index 47a49b0b3..163fdbeca 100644 --- a/config/http/handler/handlers/storage-description.json +++ b/config/http/handler/handlers/storage-description.json @@ -16,7 +16,6 @@ "handler": { "@type": "ParsingHttpHandler", "requestParser": { "@id": "urn:solid-server:default:RequestParser" }, - "metadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" }, "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, "operationHandler": { diff --git a/config/identity/handler/default.json b/config/identity/handler/default.json index 8a2b6c6f2..a13f3609a 100644 --- a/config/identity/handler/default.json +++ b/config/identity/handler/default.json @@ -22,7 +22,6 @@ "@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": { diff --git a/config/ldp/handler/default.json b/config/ldp/handler/default.json index 7c346f00e..d368c06d1 100644 --- a/config/ldp/handler/default.json +++ b/config/ldp/handler/default.json @@ -15,7 +15,6 @@ "@id": "urn:solid-server:default:LdpHandler", "@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": { @@ -24,7 +23,13 @@ "args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" }, "args_permissionReader": { "@id": "urn:solid-server:default:PermissionReader" }, "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" } + } } } ] diff --git a/src/http/Operation.ts b/src/http/Operation.ts index 06fb625f4..266145296 100644 --- a/src/http/Operation.ts +++ b/src/http/Operation.ts @@ -1,4 +1,3 @@ -import type { PermissionMap } from '../authorization/permissions/Permissions'; import type { Conditions } from '../storage/Conditions'; import type { Representation } from './representation/Representation'; import type { RepresentationPreferences } from './representation/RepresentationPreferences'; @@ -24,10 +23,6 @@ export interface Operation { * Conditions the resource must fulfill for a valid operation. */ conditions?: Conditions; - /** - * The permissions available for the current operation. - */ - availablePermissions?: PermissionMap; /** * Representation of the body and metadata headers. */ diff --git a/src/http/ldp/metadata/OperationMetadataCollector.ts b/src/http/ldp/metadata/OperationMetadataCollector.ts deleted file mode 100644 index ba8645780..000000000 --- a/src/http/ldp/metadata/OperationMetadataCollector.ts +++ /dev/null @@ -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 {} diff --git a/src/http/ldp/metadata/WebAclMetadataCollector.ts b/src/http/ldp/metadata/WebAclMetadataCollector.ts deleted file mode 100644 index 22b57e8f8..000000000 --- a/src/http/ldp/metadata/WebAclMetadataCollector.ts +++ /dev/null @@ -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 { - 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([ ...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]); - } - } - } - } -} diff --git a/src/index.ts b/src/index.ts index bd76bb528..0c0fcd6a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -82,10 +82,6 @@ export * from './http/input/preferences/PreferenceParser'; export * from './http/input/BasicRequestParser'; export * from './http/input/RequestParser'; -// HTTP/LDP/Metadata -export * from './http/ldp/metadata/OperationMetadataCollector'; -export * from './http/ldp/metadata/WebAclMetadataCollector'; - // HTTP/LDP export * from './http/ldp/DeleteOperationHandler'; export * from './http/ldp/GetOperationHandler'; @@ -295,6 +291,7 @@ export * from './server/HttpServerFactory'; export * from './server/OperationHttpHandler'; export * from './server/ParsingHttpHandler'; export * from './server/ServerConfigurator'; +export * from './server/WacAllowHttpHandler'; export * from './server/WebSocketServerConfigurator'; // Server/Description diff --git a/src/server/AuthorizingHttpHandler.ts b/src/server/AuthorizingHttpHandler.ts index f910f4977..de36bf400 100644 --- a/src/server/AuthorizingHttpHandler.ts +++ b/src/server/AuthorizingHttpHandler.ts @@ -71,7 +71,6 @@ export class AuthorizingHttpHandler extends OperationHttpHandler { try { await this.authorizer.handleSafe({ credentials, requestedModes, availablePermissions }); - operation.availablePermissions = availablePermissions; } catch (error: unknown) { this.logger.verbose(`Authorization failed: ${(error as any).message}`); throw error; diff --git a/src/server/ParsingHttpHandler.ts b/src/server/ParsingHttpHandler.ts index 0e9d2e83f..0c9c9b9ea 100644 --- a/src/server/ParsingHttpHandler.ts +++ b/src/server/ParsingHttpHandler.ts @@ -1,5 +1,4 @@ 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 { ResponseDescription } from '../http/output/response/ResponseDescription'; import type { ResponseWriter } from '../http/output/ResponseWriter'; @@ -17,10 +16,6 @@ export interface ParsingHttpHandlerArgs { * Parses the incoming requests. */ requestParser: RequestParser; - /** - * Generates generic operation metadata that is required for a response. - */ - metadataCollector: OperationMetadataCollector; /** * Converts errors to a serializable format. */ @@ -46,7 +41,6 @@ export class ParsingHttpHandler extends HttpHandler { private readonly requestParser: RequestParser; private readonly errorHandler: ErrorHandler; private readonly responseWriter: ResponseWriter; - private readonly metadataCollector: OperationMetadataCollector; private readonly operationHandler: OperationHttpHandler; public constructor(args: ParsingHttpHandlerArgs) { @@ -54,7 +48,6 @@ export class ParsingHttpHandler extends HttpHandler { this.requestParser = args.requestParser; this.errorHandler = args.errorHandler; this.responseWriter = args.responseWriter; - this.metadataCollector = args.metadataCollector; this.operationHandler = args.operationHandler; } @@ -80,10 +73,6 @@ export class ParsingHttpHandler extends HttpHandler { const operation = await this.requestParser.handleSafe(request); 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}`); return result; } diff --git a/src/server/WacAllowHttpHandler.ts b/src/server/WacAllowHttpHandler.ts new file mode 100644 index 000000000..c226c0b09 --- /dev/null +++ b/src/server/WacAllowHttpHandler.ts @@ -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 { + 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([ ...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]); + } + } + } + } +} diff --git a/test/unit/http/ldp/metadata/WebAclMetadataCollector.test.ts b/test/unit/http/ldp/metadata/WebAclMetadataCollector.test.ts deleted file mode 100644 index c57ccdd2f..000000000 --- a/test/unit/http/ldp/metadata/WebAclMetadataCollector.test.ts +++ /dev/null @@ -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 => { - operation = { - method: 'GET', - target, - preferences: {}, - body: new BasicRepresentation(), - }; - - metadata = new RepresentationMetadata(); - }); - - it('adds no metadata if there is no target entry.', async(): Promise => { - 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 => { - 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 => { - 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 => { - 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 => { - 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); - }); -}); diff --git a/test/unit/server/AuthorizingHttpHandler.test.ts b/test/unit/server/AuthorizingHttpHandler.test.ts index d36ca7d93..70daab2d8 100644 --- a/test/unit/server/AuthorizingHttpHandler.test.ts +++ b/test/unit/server/AuthorizingHttpHandler.test.ts @@ -15,7 +15,7 @@ import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/Iden describe('An AuthorizingHttpHandler', (): void => { const credentials = { }; - const target = { path: 'http://test.com/foo' }; + const target = { path: 'http://example.com/foo' }; const requestedModes: AccessMap = new IdentifierSetMultiMap([[ target, AccessMode.read ]]); const availablePermissions: PermissionMap = new IdentifierMap( [[ target, { public: { read: true }}]], @@ -71,7 +71,6 @@ describe('An AuthorizingHttpHandler', (): void => { expect(authorizer.handleSafe).toHaveBeenLastCalledWith({ credentials, requestedModes, availablePermissions }); expect(source.handleSafe).toHaveBeenCalledTimes(1); expect(source.handleSafe).toHaveBeenLastCalledWith({ request, response, operation }); - expect(operation.availablePermissions).toBe(availablePermissions); }); it('errors if authorization fails.', async(): Promise => { diff --git a/test/unit/server/ParsingHttpHandler.test.ts b/test/unit/server/ParsingHttpHandler.test.ts index 013eba5ca..5557e6328 100644 --- a/test/unit/server/ParsingHttpHandler.test.ts +++ b/test/unit/server/ParsingHttpHandler.test.ts @@ -1,8 +1,6 @@ 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 { ErrorHandler } from '../../../src/http/output/error/ErrorHandler'; -import { OkResponseDescription } from '../../../src/http/output/response/OkResponseDescription'; import { ResponseDescription } from '../../../src/http/output/response/ResponseDescription'; import type { ResponseWriter } from '../../../src/http/output/ResponseWriter'; 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 errorResponse = new ResponseDescription(400); let requestParser: jest.Mocked; - let metadataCollector: jest.Mocked; let errorHandler: jest.Mocked; let responseWriter: jest.Mocked; let source: jest.Mocked; @@ -28,7 +25,6 @@ describe('A ParsingHttpHandler', (): void => { beforeEach(async(): Promise => { requestParser = { handleSafe: jest.fn().mockResolvedValue(operation) } as any; - metadataCollector = { handleSafe: jest.fn() } as any; errorHandler = { handleSafe: jest.fn().mockResolvedValue(errorResponse) } as any; responseWriter = { handleSafe: jest.fn() } as any; @@ -37,7 +33,7 @@ describe('A ParsingHttpHandler', (): void => { } as any; 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 }); }); - it('calls the operation metadata collector if there is response metadata.', async(): Promise => { - 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 => { const error = new Error('bad data'); source.handleSafe.mockRejectedValueOnce(error); diff --git a/test/unit/server/WacAllowHttpHandler.test.ts b/test/unit/server/WacAllowHttpHandler.test.ts new file mode 100644 index 000000000..b6f07f7b6 --- /dev/null +++ b/test/unit/server/WacAllowHttpHandler.test.ts @@ -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; + let modesExtractor: jest.Mocked; + let permissionReader: jest.Mocked; + let source: jest.Mocked; + let handler: WacAllowHttpHandler; + + beforeEach(async(): Promise => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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); + }); +});