feat: Create OperationMetadataCollector to handle operation metadata

This commit is contained in:
Joachim Van Herwegen 2021-09-20 14:22:01 +02:00
parent bf28c83ffa
commit 5104cd56e8
23 changed files with 228 additions and 222 deletions

View File

@ -6,12 +6,9 @@
"DO NOT USE IN PRODUCTION. ONLY FOR DEVELOPMENT, TESTING, OR DEBUGGING.",
"Always allows all operations."
],
"@id": "urn:solid-server:default:Authorizer",
"@type": "PermissionBasedAuthorizer",
"reader": {
"@id": "urn:solid-server:default:PermissionReader",
"@type": "AllStaticReader",
"allow": true
}
}
]
}

View File

@ -6,9 +6,7 @@
"@graph": [
{
"comment": "Uses Web Access Control for authorization.",
"@id": "urn:solid-server:default:Authorizer",
"@type": "PermissionBasedAuthorizer",
"reader": {
"@id": "urn:solid-server:default:PermissionReader",
"@type": "UnionPermissionReader",
"readers": [
{
@ -26,6 +24,5 @@
{ "@id": "urn:solid-server:default:WebAclReader" }
]
}
}
]
}

View File

@ -0,0 +1,10 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Matches requested permissions with those available.",
"@id": "urn:solid-server:default:Authorizer",
"@type": "PermissionBasedAuthorizer"
}
]
}

View File

@ -7,7 +7,7 @@
"handlers": [
{
"@type": "GetOperationHandler",
"store": { "@id": "urn:solid-server:default:ResourceStore" }
"store": { "@id": "urn:solid-server:default:ResourceStore" },
},
{
"@type": "PostOperationHandler",
@ -23,7 +23,7 @@
},
{
"@type": "HeadOperationHandler",
"store": { "@id": "urn:solid-server:default:ResourceStore" }
"store": { "@id": "urn:solid-server:default:ResourceStore" },
},
{
"@type": "PatchOperationHandler",

View File

@ -0,0 +1,9 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"@id": "urn:solid-server:default:OperationMetadataCollector",
"@type": "WebAclMetadataCollector"
}
]
}

View File

@ -1,8 +1,10 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/ldp/handler/components/authorizer.json",
"files-scs:config/ldp/handler/components/error-handler.json",
"files-scs:config/ldp/handler/components/operation-handler.json",
"files-scs:config/ldp/handler/components/operation-metadata.json",
"files-scs:config/ldp/handler/components/request-parser.json",
"files-scs:config/ldp/handler/components/response-writer.json"
],
@ -14,10 +16,12 @@
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"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_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
"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_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"args_operationMetadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" }
}
]
}

View File

@ -1,12 +0,0 @@
import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
/**
* The output of an Authorizer
*/
export interface Authorization {
/**
* Add metadata relevant for this Authorization.
* @param metadata - Metadata to update.
*/
addMetadata: (metadata: RepresentationMetadata) => void;
}

View File

@ -1,8 +1,7 @@
import type { CredentialSet } from '../authentication/Credentials';
import type { AccessMode } from '../ldp/permissions/Permissions';
import type { AccessMode, PermissionSet } from '../ldp/permissions/Permissions';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { AsyncHandler } from '../util/handlers/AsyncHandler';
import type { Authorization } from './Authorization';
export interface AuthorizerInput {
/**
@ -17,10 +16,14 @@ export interface AuthorizerInput {
* Modes that are requested on the resource.
*/
modes: Set<AccessMode>;
/**
* Permissions that are available for the request.
*/
permissionSet: PermissionSet;
}
/**
* Verifies if the credentials provide access with the given permissions on the resource.
* An {@link Error} with the necessary explanation will be thrown when permissions are not granted.
*/
export abstract class Authorizer extends AsyncHandler<AuthorizerInput, Authorization> {}
export abstract class Authorizer extends AsyncHandler<AuthorizerInput> {}

View File

@ -1,14 +1,10 @@
import type { CredentialSet } from '../authentication/Credentials';
import type { AccessMode, PermissionSet } from '../ldp/permissions/Permissions';
import { getLoggerFor } from '../logging/LogUtil';
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
import type { Authorization } from './Authorization';
import type { AuthorizerInput } from './Authorizer';
import { Authorizer } from './Authorizer';
import type { PermissionReader } from './PermissionReader';
import { WebAclAuthorization } from './WebAclAuthorization';
/**
* Authorizer that bases its decision on the output it gets from its PermissionReader.
@ -19,32 +15,16 @@ import { WebAclAuthorization } from './WebAclAuthorization';
export class PermissionBasedAuthorizer extends Authorizer {
protected readonly logger = getLoggerFor(this);
private readonly reader: PermissionReader;
public constructor(reader: PermissionReader) {
super();
this.reader = reader;
}
public async canHandle(input: AuthorizerInput): Promise<void> {
return this.reader.canHandle(input);
}
public async handle(input: AuthorizerInput): Promise<Authorization> {
const { credentials, modes, identifier } = input;
// Read out the permissions
const permissions = await this.reader.handle(input);
const authorization = new WebAclAuthorization(permissions.agent ?? {}, permissions.public ?? {});
public async handle(input: AuthorizerInput): Promise<void> {
const { credentials, modes, identifier, permissionSet } = input;
const modeString = [ ...modes ].join(',');
this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modeString} permissions for ${identifier.path}`);
for (const mode of modes) {
this.requireModePermission(credentials, permissions, mode);
this.requireModePermission(credentials, permissionSet, mode);
}
this.logger.debug(`${JSON.stringify(credentials)} has ${modeString} permissions for ${identifier.path}`);
return authorization;
}
/**

View File

@ -1,36 +0,0 @@
import type { Permission } from '../ldp/permissions/Permissions';
import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import { ACL, AUTH } from '../util/Vocabularies';
import type { Authorization } from './Authorization';
/**
* Indicates which permissions are available on the requested resource.
*/
export class WebAclAuthorization implements Authorization {
/**
* Permissions granted to the agent requesting the resource.
*/
public user: Permission;
/**
* Permissions granted to the public.
*/
public everyone: Permission;
public constructor(user: Permission, everyone: Permission) {
this.user = user;
this.everyone = everyone;
}
public addMetadata(metadata: RepresentationMetadata): void {
const modes = new Set([ ...Object.keys(this.user), ...Object.keys(this.everyone) ] as (keyof Permission)[]);
for (const mode of modes) {
const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control';
if (this.user[mode]) {
metadata.add(AUTH.terms.userMode, ACL.terms[capitalizedMode]);
}
if (this.everyone[mode]) {
metadata.add(AUTH.terms.publicMode, ACL.terms[capitalizedMode]);
}
}
}
}

View File

@ -8,24 +8,22 @@ export * from './authentication/UnionCredentialsExtractor';
export * from './authentication/UnsecureConstantCredentialsExtractor';
export * from './authentication/UnsecureWebIdExtractor';
// Authorization/Access-Checkers
export * from './authorization/access-checkers/AccessChecker';
export * from './authorization/access-checkers/AgentAccessChecker';
export * from './authorization/access-checkers/AgentClassAccessChecker';
export * from './authorization/access-checkers/AgentGroupAccessChecker';
// Authorization
export * from './authorization/AllStaticReader';
export * from './authorization/Authorization';
export * from './authorization/Authorizer';
export * from './authorization/AuxiliaryReader';
export * from './authorization/PathBasedReader';
export * from './authorization/PermissionBasedAuthorizer';
export * from './authorization/PermissionReader';
export * from './authorization/UnionPermissionReader';
export * from './authorization/WebAclAuthorization';
export * from './authorization/WebAclReader';
// Authorization/access-checkers
export * from './authorization/access-checkers/AccessChecker';
export * from './authorization/access-checkers/AgentAccessChecker';
export * from './authorization/access-checkers/AgentClassAccessChecker';
export * from './authorization/access-checkers/AgentGroupAccessChecker';
// Identity/Configuration
export * from './identity/configuration/IdentityProviderFactory';
export * from './identity/configuration/ProviderFactory';
@ -141,6 +139,10 @@ export * from './ldp/http/SparqlUpdateBodyParser';
export * from './ldp/http/SparqlUpdatePatch';
export * from './ldp/http/TargetExtractor';
// LDP/Operations/Metadata
export * from './ldp/operations/metadata/OperationMetadataCollector';
export * from './ldp/operations/metadata/WebAclMetadataCollector';
// LDP/Operations
export * from './ldp/operations/DeleteOperationHandler';
export * from './ldp/operations/GetOperationHandler';

View File

@ -1,6 +1,7 @@
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';
@ -9,6 +10,7 @@ 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';
@ -26,6 +28,10 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
* 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.
*/
@ -34,6 +40,10 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
* Executed the operation.
*/
operationHandler: OperationHandler;
/**
* Generates generic operation metadata that is required for a response.
*/
operationMetadataCollector: OperationMetadataCollector;
}
/**
@ -42,8 +52,10 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
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.
@ -53,8 +65,10 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
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;
}
/**
@ -83,16 +97,24 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
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 {
const authorization = await this.authorizer
.handleSafe({ credentials, identifier: operation.target, modes });
operation.authorization = authorization;
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`);
return this.operationHandler.handleSafe(operation);
const response = await this.operationHandler.handleSafe(operation);
if (response.metadata) {
await this.operationMetadataCollector.handleSafe({ operation, metadata: response.metadata });
}
return response;
}
}

View File

@ -26,8 +26,6 @@ export class GetOperationHandler extends OperationHandler {
public async handle(input: Operation): Promise<ResponseDescription> {
const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions);
input.authorization?.addMetadata(body.metadata);
return new OkResponseDescription(body.metadata, body.data);
}
}

View File

@ -29,8 +29,6 @@ export class HeadOperationHandler extends OperationHandler {
// Close the Readable as we will not return it.
body.data.destroy();
input.authorization?.addMetadata(body.metadata);
return new OkResponseDescription(body.metadata);
}
}

View File

@ -1,5 +1,5 @@
import type { Authorization } from '../../authorization/Authorization';
import type { Conditions } from '../../storage/Conditions';
import type { PermissionSet } from '../permissions/Permissions';
import type { Representation } from '../representation/Representation';
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
@ -25,9 +25,9 @@ export interface Operation {
*/
conditions?: Conditions;
/**
* This value will be set if the Operation was authorized by an Authorizer.
* The permissions available for the current operation.
*/
authorization?: Authorization;
permissionSet?: PermissionSet;
/**
* Optional representation of the body.
*/

View File

@ -0,0 +1,19 @@
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import type { Operation } from '../Operation';
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

@ -0,0 +1,33 @@
import { ACL, AUTH } from '../../../util/Vocabularies';
import type { AccessMode } from '../../permissions/Permissions';
import type { OperationMetadataCollectorInput } from './OperationMetadataCollector';
import { OperationMetadataCollector } from './OperationMetadataCollector';
const VALID_METHODS = new Set([ 'HEAD', 'GET' ]);
/**
* 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> {
if (!operation.permissionSet || !VALID_METHODS.has(operation.method)) {
return;
}
const user = operation.permissionSet.agent ?? {};
const everyone = operation.permissionSet.public ?? {};
const modes = new Set<AccessMode>([ ...Object.keys(user), ...Object.keys(everyone) ] as AccessMode[]);
for (const mode of modes) {
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,71 +1,56 @@
import { CredentialGroup } from '../../../src/authentication/Credentials';
import type { AuthorizerInput } from '../../../src/authorization/Authorizer';
import { PermissionBasedAuthorizer } from '../../../src/authorization/PermissionBasedAuthorizer';
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization';
import { AccessMode } from '../../../src/ldp/permissions/Permissions';
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError';
describe('A PermissionBasedAuthorizer', (): void => {
let input: AuthorizerInput;
let authorization: WebAclAuthorization;
let reader: jest.Mocked<PermissionReader>;
let authorizer: PermissionBasedAuthorizer;
beforeEach(async(): Promise<void> => {
input = {
identifier: { path: 'http://test.com/foo' },
modes: new Set<AccessMode>(),
permissionSet: {},
credentials: {},
};
authorization = new WebAclAuthorization({}, {});
reader = {
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue({}),
} as any;
authorizer = new PermissionBasedAuthorizer(reader);
authorizer = new PermissionBasedAuthorizer();
});
it('can handle any input supported by its reader.', async(): Promise<void> => {
it('can handle any input.', async(): Promise<void> => {
await expect(authorizer.canHandle(input)).resolves.toBeUndefined();
reader.canHandle.mockRejectedValue(new Error('bad request'));
await expect(authorizer.canHandle(input)).rejects.toThrow('bad request');
});
it('allows access if the permissions are matched by the reader output.', async(): Promise<void> => {
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
reader.handle.mockResolvedValueOnce({
input.permissionSet = {
[CredentialGroup.public]: { read: true, write: false },
[CredentialGroup.agent]: { write: true },
});
Object.assign(authorization.everyone, { read: true, write: false });
Object.assign(authorization.user, { write: true });
await expect(authorizer.handle(input)).resolves.toEqual(authorization);
};
await expect(authorizer.handle(input)).resolves.toBeUndefined();
});
it('throws an UnauthorizedHttpError when an unauthenticated request has no access.', async(): Promise<void> => {
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
reader.handle.mockResolvedValueOnce({
input.permissionSet = {
[CredentialGroup.public]: { read: true, write: false },
});
};
await expect(authorizer.handle(input)).rejects.toThrow(UnauthorizedHttpError);
});
it('throws a ForbiddenHttpError when an authenticated request has no access.', async(): Promise<void> => {
input.credentials = { agent: { webId: 'http://test.com/#me' }};
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
reader.handle.mockResolvedValueOnce({
input.permissionSet = {
[CredentialGroup.public]: { read: true, write: false },
});
};
await expect(authorizer.handle(input)).rejects.toThrow(ForbiddenHttpError);
});
it('defaults to empty permissions for the Authorization.', async(): Promise<void> => {
await expect(authorizer.handle(input)).resolves.toEqual(authorization);
await expect(authorizer.handle(input)).resolves.toBeUndefined();
});
});

View File

@ -1,43 +0,0 @@
import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization';
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
import { ACL, AUTH } from '../../../src/util/Vocabularies';
import 'jest-rdf';
describe('A WebAclAuthorization', (): void => {
let authorization: WebAclAuthorization;
let metadata: RepresentationMetadata;
beforeEach(async(): Promise<void> => {
authorization = new WebAclAuthorization(
{
read: false,
append: false,
write: false,
control: false,
},
{
read: false,
append: false,
write: false,
control: false,
},
);
metadata = new RepresentationMetadata();
});
it('adds no metadata if there are no permissions.', async(): Promise<void> => {
expect(authorization.addMetadata(metadata)).toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('adds corresponding acl metadata for all permissions present.', async(): Promise<void> => {
authorization.user.read = true;
authorization.user.write = true;
authorization.everyone.read = true;
expect(authorization.addMetadata(metadata)).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);
});
});

View File

@ -1,11 +1,14 @@
import { CredentialGroup } from '../../../src/authentication/Credentials';
import type { CredentialSet } from '../../../src/authentication/Credentials';
import type { Authorization } from '../../../src/authorization/Authorization';
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';
@ -18,7 +21,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
let operation: Operation;
const credentials: CredentialSet = {};
const modes: Set<AccessMode> = new Set([ AccessMode.read ]);
const authorization: Authorization = { addMetadata: jest.fn() };
const permissionSet: PermissionSet = { [CredentialGroup.agent]: { read: true }};
const result: ResponseDescription = new ResetResponseDescription();
const errorResult: ResponseDescription = { statusCode: 500 };
let args: AuthenticatedLdpHandlerArgs;
@ -33,8 +36,10 @@ describe('An AuthenticatedLdpHandler', (): void => {
} as any,
credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
modesExtractor: { handleSafe: jest.fn().mockResolvedValue(modes) } as any,
authorizer: { handleSafe: jest.fn().mockResolvedValue(authorization) } 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,
};
@ -65,17 +70,28 @@ describe('An AuthenticatedLdpHandler', (): void => {
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 });
expect(operation.authorization).toBe(authorization);
.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!'));

View File

@ -1,4 +1,3 @@
import type { Authorization } from '../../../../src/authorization/Authorization';
import { GetOperationHandler } from '../../../../src/ldp/operations/GetOperationHandler';
import type { Operation } from '../../../../src/ldp/operations/Operation';
import type { Representation } from '../../../../src/ldp/representation/Representation';
@ -34,16 +33,4 @@ describe('A GetOperationHandler', (): void => {
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
});
it('adds authorization metadata in case the operation is an AuthorizedOperation.', async(): Promise<void> => {
const authorization: Authorization = { addMetadata: jest.fn() };
const result = await handler.handle(
{ target: { path: 'url' }, preferences, conditions, authorization } as Operation,
);
expect(result.statusCode).toBe(200);
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
expect(authorization.addMetadata).toHaveBeenCalledTimes(1);
expect(authorization.addMetadata).toHaveBeenLastCalledWith('metadata');
});
});

View File

@ -1,5 +1,4 @@
import type { Readable } from 'stream';
import type { Authorization } from '../../../../src/authorization/Authorization';
import { HeadOperationHandler } from '../../../../src/ldp/operations/HeadOperationHandler';
import type { Operation } from '../../../../src/ldp/operations/Operation';
import type { Representation } from '../../../../src/ldp/representation/Representation';
@ -20,6 +19,7 @@ describe('A HeadOperationHandler', (): void => {
getRepresentation: jest.fn(async(): Promise<Representation> =>
({ binary: false, data, metadata: 'metadata' } as any)),
} as any;
handler = new HeadOperationHandler(store);
});
@ -38,16 +38,4 @@ describe('A HeadOperationHandler', (): void => {
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
});
it('adds authorization metadata in case the operation is an AuthorizedOperation.', async(): Promise<void> => {
const authorization: Authorization = { addMetadata: jest.fn() };
const result = await handler.handle(
{ target: { path: 'url' }, preferences, conditions, authorization } as Operation,
);
expect(result.statusCode).toBe(200);
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
expect(authorization.addMetadata).toHaveBeenCalledTimes(1);
expect(authorization.addMetadata).toHaveBeenLastCalledWith('metadata');
});
});

View File

@ -0,0 +1,49 @@
import 'jest-rdf';
import { CredentialGroup } from '../../../../../src/authentication/Credentials';
import { WebAclMetadataCollector } from '../../../../../src/ldp/operations/metadata/WebAclMetadataCollector';
import type { Operation } from '../../../../../src/ldp/operations/Operation';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
describe('A WebAclMetadataCollector', (): void => {
let operation: Operation;
let metadata: RepresentationMetadata;
const writer = new WebAclMetadataCollector();
beforeEach(async(): Promise<void> => {
operation = {
method: 'GET',
target: { path: 'http://test.com/foo' },
preferences: {},
};
metadata = new RepresentationMetadata();
});
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.permissionSet = {};
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.permissionSet = { [CredentialGroup.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.permissionSet = {
[CredentialGroup.agent]: { read: true, write: true, control: false },
[CredentialGroup.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);
});
});