mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Extract set of required modes instead of PermissionSet
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
24
src/ldp/permissions/AclModesExtractor.ts
Normal file
24
src/ldp/permissions/AclModesExtractor.ts
Normal 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 ]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
5
src/ldp/permissions/ModesExtractor.ts
Normal file
5
src/ldp/permissions/ModesExtractor.ts
Normal 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>> {}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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> {}
|
||||
@@ -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 {
|
||||
Reference in New Issue
Block a user