feat: Extract set of required modes instead of PermissionSet

This commit is contained in:
Joachim Van Herwegen
2021-09-27 14:00:29 +02:00
parent ba1886ab85
commit e8dedf5c23
40 changed files with 183 additions and 254 deletions

View File

@@ -1,5 +1,5 @@
import type { CredentialSet } from '../authentication/Credentials';
import type { PermissionSet } from '../ldp/permissions/PermissionSet';
import type { AccessMode } from '../ldp/permissions/PermissionSet';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { AsyncHandler } from '../util/handlers/AsyncHandler';
import type { Authorization } from './Authorization';
@@ -14,9 +14,9 @@ export interface AuthorizerInput {
*/
identifier: ResourceIdentifier;
/**
* Permissions that are requested on the resource.
* Modes that are requested on the resource.
*/
permissions: PermissionSet;
modes: Set<AccessMode>;
}
/**

View File

@@ -3,6 +3,7 @@ import { Store } from 'n3';
import type { Credential, CredentialSet } from '../authentication/Credentials';
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { PermissionSet } from '../ldp/permissions/PermissionSet';
import { AccessMode } from '../ldp/permissions/PermissionSet';
import type { Representation } from '../ldp/representation/Representation';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
@@ -22,11 +23,11 @@ import type { AuthorizerInput } from './Authorizer';
import { Authorizer } from './Authorizer';
import { WebAclAuthorization } from './WebAclAuthorization';
const modesMap: Record<string, keyof PermissionSet> = {
[ACL.Read]: 'read',
[ACL.Write]: 'write',
[ACL.Append]: 'append',
[ACL.Control]: 'control',
const modesMap: Record<string, AccessMode> = {
[ACL.Read]: AccessMode.read,
[ACL.Write]: AccessMode.write,
[ACL.Append]: AccessMode.append,
[ACL.Control]: AccessMode.control,
} as const;
/**
@@ -61,11 +62,10 @@ export class WebAclAuthorizer extends Authorizer {
* Will throw an error if this is not the case.
* @param input - Relevant data needed to check if access can be granted.
*/
public async handle({ identifier, permissions, credentials }: AuthorizerInput):
public async handle({ identifier, modes, credentials }: AuthorizerInput):
Promise<WebAclAuthorization> {
// Determine the required access modes
const modes = (Object.keys(permissions) as (keyof PermissionSet)[]).filter((key): boolean => permissions[key]);
this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modes.join()} permissions for ${identifier.path}`);
const modeString = [ ...modes ].join(',');
this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modeString} permissions for ${identifier.path}`);
// Determine the full authorization for the agent granted by the applicable ACL
const acl = await this.getAclRecursive(identifier);
@@ -76,7 +76,7 @@ export class WebAclAuthorizer extends Authorizer {
for (const mode of modes) {
this.requirePermission(agent, authorization, mode);
}
this.logger.debug(`${agent.webId} has ${modes.join()} permissions for ${identifier.path}`);
this.logger.debug(`${agent.webId} has ${modeString} permissions for ${identifier.path}`);
return authorization;
}

View File

@@ -150,11 +150,11 @@ export * from './ldp/operations/PostOperationHandler';
export * from './ldp/operations/PutOperationHandler';
// LDP/Permissions
export * from './ldp/permissions/AclPermissionsExtractor';
export * from './ldp/permissions/AclModesExtractor';
export * from './ldp/permissions/PermissionSet';
export * from './ldp/permissions/PermissionsExtractor';
export * from './ldp/permissions/MethodPermissionsExtractor';
export * from './ldp/permissions/SparqlPatchPermissionsExtractor';
export * from './ldp/permissions/ModesExtractor';
export * from './ldp/permissions/MethodModesExtractor';
export * from './ldp/permissions/SparqlPatchModesExtractor';
// LDP/Representation
export * from './ldp/representation/BasicRepresentation';

View File

@@ -11,8 +11,7 @@ import type { ResponseDescription } from './http/response/ResponseDescription';
import type { ResponseWriter } from './http/ResponseWriter';
import type { Operation } from './operations/Operation';
import type { OperationHandler } from './operations/OperationHandler';
import type { PermissionSet } from './permissions/PermissionSet';
import type { PermissionsExtractor } from './permissions/PermissionsExtractor';
import type { ModesExtractor } from './permissions/ModesExtractor';
export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
@@ -24,9 +23,9 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
*/
credentialsExtractor: CredentialsExtractor;
/**
* Extracts the required permissions from the generated Operation.
* Extracts the required modes from the generated Operation.
*/
permissionsExtractor: PermissionsExtractor;
modesExtractor: ModesExtractor;
/**
* Verifies if the requested operation is allowed.
*/
@@ -42,7 +41,7 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
*/
export class AuthenticatedLdpHandler extends BaseHttpHandler {
private readonly credentialsExtractor: CredentialsExtractor;
private readonly permissionsExtractor: PermissionsExtractor;
private readonly modesExtractor: ModesExtractor;
private readonly authorizer: Authorizer;
private readonly operationHandler: OperationHandler;
@@ -53,7 +52,7 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
public constructor(args: AuthenticatedLdpHandlerArgs) {
super(args);
this.credentialsExtractor = args.credentialsExtractor;
this.permissionsExtractor = args.permissionsExtractor;
this.modesExtractor = args.modesExtractor;
this.authorizer = args.authorizer;
this.operationHandler = args.operationHandler;
}
@@ -81,13 +80,12 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
const permissions: PermissionSet = await this.permissionsExtractor.handleSafe(operation);
const { read, write, append } = permissions;
this.logger.verbose(`Required permissions are read: ${read}, write: ${write}, append: ${append}`);
const modes = await this.modesExtractor.handleSafe(operation);
this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
try {
const authorization = await this.authorizer
.handleSafe({ credentials, identifier: operation.target, permissions });
.handleSafe({ credentials, identifier: operation.target, modes });
operation.authorization = authorization;
} catch (error: unknown) {
this.logger.verbose(`Authorization failed: ${(error as any).message}`);

View File

@@ -0,0 +1,24 @@
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import type { AuxiliaryIdentifierStrategy } from '../auxiliary/AuxiliaryIdentifierStrategy';
import type { Operation } from '../operations/Operation';
import { ModesExtractor } from './ModesExtractor';
import { AccessMode } from './PermissionSet';
export class AclModesExtractor extends ModesExtractor {
private readonly aclStrategy: AuxiliaryIdentifierStrategy;
public constructor(aclStrategy: AuxiliaryIdentifierStrategy) {
super();
this.aclStrategy = aclStrategy;
}
public async canHandle({ target }: Operation): Promise<void> {
if (!this.aclStrategy.isAuxiliaryIdentifier(target)) {
throw new NotImplementedHttpError('Can only determine permissions of acl resources');
}
}
public async handle(): Promise<Set<AccessMode>> {
return new Set([ AccessMode.control ]);
}
}

View File

@@ -1,36 +0,0 @@
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import type { AuxiliaryIdentifierStrategy } from '../auxiliary/AuxiliaryIdentifierStrategy';
import type { Operation } from '../operations/Operation';
import type { PermissionSet } from './PermissionSet';
import { PermissionsExtractor } from './PermissionsExtractor';
/**
* PermissionsExtractor specifically for acl resources.
*
* Solid, §4.3.3: "To discover, read, create, or modify an ACL auxiliary resource, an acl:agent MUST have
* acl:Control privileges per the ACL inheritance algorithm on the resource directly associated with it."
* https://solid.github.io/specification/protocol#auxiliary-resources-reserved
*/
export class AclPermissionsExtractor extends PermissionsExtractor {
private readonly aclStrategy: AuxiliaryIdentifierStrategy;
public constructor(aclStrategy: AuxiliaryIdentifierStrategy) {
super();
this.aclStrategy = aclStrategy;
}
public async canHandle({ target }: Operation): Promise<void> {
if (!this.aclStrategy.isAuxiliaryIdentifier(target)) {
throw new NotImplementedHttpError('Can only determine permissions of acl resources');
}
}
public async handle(): Promise<PermissionSet> {
return {
read: false,
write: false,
append: false,
control: true,
};
}
}

View File

@@ -1,7 +1,7 @@
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import type { Operation } from '../operations/Operation';
import type { PermissionSet } from './PermissionSet';
import { PermissionsExtractor } from './PermissionsExtractor';
import { ModesExtractor } from './ModesExtractor';
import { AccessMode } from './PermissionSet';
const READ_METHODS = new Set([ 'GET', 'HEAD' ]);
const WRITE_METHODS = new Set([ 'PUT', 'DELETE' ]);
@@ -12,18 +12,24 @@ const SUPPORTED_METHODS = new Set([ ...READ_METHODS, ...WRITE_METHODS, ...APPEND
* Generates permissions for the base set of methods that always require the same permissions.
* Specifically: GET, HEAD, POST, PUT and DELETE.
*/
export class MethodPermissionsExtractor extends PermissionsExtractor {
export class MethodModesExtractor extends ModesExtractor {
public async canHandle({ method }: Operation): Promise<void> {
if (!SUPPORTED_METHODS.has(method)) {
throw new NotImplementedHttpError(`Cannot determine permissions of ${method}`);
}
}
public async handle({ method }: Operation): Promise<PermissionSet> {
const read = READ_METHODS.has(method);
const write = WRITE_METHODS.has(method);
const append = write || APPEND_METHODS.has(method);
const control = false;
return { read, write, append, control };
public async handle({ method }: Operation): Promise<Set<AccessMode>> {
const result = new Set<AccessMode>();
if (READ_METHODS.has(method)) {
result.add(AccessMode.read);
}
if (WRITE_METHODS.has(method)) {
result.add(AccessMode.write);
result.add(AccessMode.append);
} else if (APPEND_METHODS.has(method)) {
result.add(AccessMode.append);
}
return result;
}
}

View File

@@ -0,0 +1,5 @@
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import type { Operation } from '../operations/Operation';
import type { AccessMode } from './PermissionSet';
export abstract class ModesExtractor extends AsyncHandler<Operation, Set<AccessMode>> {}

View File

@@ -1,9 +1,14 @@
/**
* Different modes that require permission.
*/
export enum AccessMode {
read = 'read',
append = 'append',
write = 'write',
control = 'control',
}
/**
* A data interface indicating which permissions are required (based on the context).
*/
export interface PermissionSet {
read: boolean;
append: boolean;
write: boolean;
control: boolean;
}
export type PermissionSet = Record<AccessMode, boolean>;

View File

@@ -1,8 +0,0 @@
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import type { Operation } from '../operations/Operation';
import type { PermissionSet } from './PermissionSet';
/**
* Verifies which permissions are requested on a given {@link Operation}.
*/
export abstract class PermissionsExtractor extends AsyncHandler<Operation, PermissionSet> {}

View File

@@ -3,15 +3,10 @@ import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpErr
import type { SparqlUpdatePatch } from '../http/SparqlUpdatePatch';
import type { Operation } from '../operations/Operation';
import type { Representation } from '../representation/Representation';
import type { PermissionSet } from './PermissionSet';
import { PermissionsExtractor } from './PermissionsExtractor';
import { ModesExtractor } from './ModesExtractor';
import { AccessMode } from './PermissionSet';
/**
* Generates permissions for a SPARQL DELETE/INSERT patch.
* Updates with only an INSERT can be done with just append permissions,
* while DELETEs require write permissions as well.
*/
export class SparqlPatchPermissionsExtractor extends PermissionsExtractor {
export class SparqlPatchModesExtractor extends ModesExtractor {
public async canHandle({ method, body }: Operation): Promise<void> {
if (method !== 'PATCH') {
throw new NotImplementedHttpError(`Cannot determine permissions of ${method}, only PATCH.`);
@@ -27,16 +22,19 @@ export class SparqlPatchPermissionsExtractor extends PermissionsExtractor {
}
}
public async handle({ body }: Operation): Promise<PermissionSet> {
public async handle({ body }: Operation): Promise<Set<AccessMode>> {
// Verified in `canHandle` call
const update = (body as SparqlUpdatePatch).algebra as Algebra.DeleteInsert;
const result = new Set<AccessMode>();
// Since `append` is a specific type of write, it is true if `write` is true.
const read = false;
const write = this.needsWrite(update);
const append = write || this.needsAppend(update);
const control = false;
return { read, write, append, control };
if (this.needsWrite(update)) {
result.add(AccessMode.write);
result.add(AccessMode.append);
} else if (this.needsAppend(update)) {
result.add(AccessMode.append);
}
return result;
}
private isSparql(data: Representation): data is SparqlUpdatePatch {