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

@@ -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]);
}
}
}
}