mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
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:
parent
59e64a22ea
commit
6ad5c0c797
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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> {}
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
91
src/server/WacAllowHttpHandler.ts
Normal file
91
src/server/WacAllowHttpHandler.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
@ -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<AccessMode>([[ 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<void> => {
|
||||
|
@ -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<RequestParser>;
|
||||
let metadataCollector: jest.Mocked<OperationMetadataCollector>;
|
||||
let errorHandler: jest.Mocked<ErrorHandler>;
|
||||
let responseWriter: jest.Mocked<ResponseWriter>;
|
||||
let source: jest.Mocked<OperationHttpHandler>;
|
||||
@ -28,7 +25,6 @@ describe('A ParsingHttpHandler', (): void => {
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
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<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> => {
|
||||
const error = new Error('bad data');
|
||||
source.handleSafe.mockRejectedValueOnce(error);
|
||||
|
117
test/unit/server/WacAllowHttpHandler.test.ts
Normal file
117
test/unit/server/WacAllowHttpHandler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user