mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create OperationMetadataCollector to handle operation metadata
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/index.ts
18
src/index.ts
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
19
src/ldp/operations/metadata/OperationMetadataCollector.ts
Normal file
19
src/ldp/operations/metadata/OperationMetadataCollector.ts
Normal 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> {}
|
||||
33
src/ldp/operations/metadata/WebAclMetadataCollector.ts
Normal file
33
src/ldp/operations/metadata/WebAclMetadataCollector.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user