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

@ -5,6 +5,7 @@
"Error", "Error",
"EventEmitter", "EventEmitter",
"HttpErrorOptions", "HttpErrorOptions",
"PermissionSet",
"Template", "Template",
"TemplateEngine", "TemplateEngine",
"ValuePreferencesArg", "ValuePreferencesArg",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/dynamic.json", "files-scs:config/storage/backend/dynamic.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/file.json", "files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/file.json", "files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/file.json", "files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -27,8 +27,8 @@ Contains a list of parsers that will be run on incoming requests to generate met
Contains a list of metadata writers that will be run on outgoing responses. Contains a list of metadata writers that will be run on outgoing responses.
* *default*: Contains the default writers. Can be added to when specific parsers are required. * *default*: Contains the default writers. Can be added to when specific parsers are required.
## Permissions ## Modes
Determines which permissions are needed for requests, Determines which modes are needed for requests,
by default this is based on the used HTTP method. by default this is based on the used HTTP method.
* *acl*: The default setup with specific support for accessing .acl documents. * *acl*: The default setup with specific support for accessing .acl documents.
* *no-acl*: Same as above but interprets .acl documents as any other document. * *no-acl*: Same as above but interprets .acl documents as any other document.

View File

@ -13,7 +13,7 @@
"@type": "AuthenticatedLdpHandler", "@type": "AuthenticatedLdpHandler",
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" }, "args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" }, "args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
"args_permissionsExtractor": { "@id": "urn:solid-server:default:PermissionsExtractor" }, "args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
"args_authorizer": { "@id": "urn:solid-server:default:Authorizer" }, "args_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
"args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" }, "args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" },
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },

View File

@ -3,15 +3,15 @@
"@graph": [ "@graph": [
{ {
"comment": "Makes sure acl files require control permissions.", "comment": "Makes sure acl files require control permissions.",
"@id": "urn:solid-server:default:PermissionsExtractor", "@id": "urn:solid-server:default:ModesExtractor",
"@type": "WaterfallHandler", "@type": "WaterfallHandler",
"handlers": [ "handlers": [
{ {
"@type": "AclPermissionsExtractor", "@type": "AclModesExtractor",
"aclStrategy": { "@id": "urn:solid-server:default:AclStrategy" } "aclStrategy": { "@id": "urn:solid-server:default:AclStrategy" }
}, },
{ "@type": "MethodPermissionsExtractor" }, { "@type": "MethodModesExtractor" },
{ "@type": "SparqlPatchPermissionsExtractor" } { "@type": "SparqlPatchModesExtractor" }
] ]
} }
] ]

View File

@ -3,11 +3,11 @@
"@graph": [ "@graph": [
{ {
"comment": "Extracts the required permissions based on the HTTP method.", "comment": "Extracts the required permissions based on the HTTP method.",
"@id": "urn:solid-server:default:PermissionsExtractor", "@id": "urn:solid-server:default:ModesExtractor",
"@type": "WaterfallHandler", "@type": "WaterfallHandler",
"handlers": [ "handlers": [
{ "@type": "MethodPermissionsExtractor" }, { "@type": "MethodModesExtractor" },
{ "@type": "SparqlPatchPermissionsExtractor" } { "@type": "SparqlPatchModesExtractor" }
] ]
} }
] ]

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/regex.json", "files-scs:config/storage/backend/regex.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/sparql.json", "files-scs:config/storage/backend/sparql.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/sparql.json", "files-scs:config/storage/backend/sparql.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

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

View File

@ -150,11 +150,11 @@ export * from './ldp/operations/PostOperationHandler';
export * from './ldp/operations/PutOperationHandler'; export * from './ldp/operations/PutOperationHandler';
// LDP/Permissions // LDP/Permissions
export * from './ldp/permissions/AclPermissionsExtractor'; export * from './ldp/permissions/AclModesExtractor';
export * from './ldp/permissions/PermissionSet'; export * from './ldp/permissions/PermissionSet';
export * from './ldp/permissions/PermissionsExtractor'; export * from './ldp/permissions/ModesExtractor';
export * from './ldp/permissions/MethodPermissionsExtractor'; export * from './ldp/permissions/MethodModesExtractor';
export * from './ldp/permissions/SparqlPatchPermissionsExtractor'; export * from './ldp/permissions/SparqlPatchModesExtractor';
// LDP/Representation // LDP/Representation
export * from './ldp/representation/BasicRepresentation'; 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 { ResponseWriter } from './http/ResponseWriter';
import type { Operation } from './operations/Operation'; import type { Operation } from './operations/Operation';
import type { OperationHandler } from './operations/OperationHandler'; import type { OperationHandler } from './operations/OperationHandler';
import type { PermissionSet } from './permissions/PermissionSet'; import type { ModesExtractor } from './permissions/ModesExtractor';
import type { PermissionsExtractor } from './permissions/PermissionsExtractor';
export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs { export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73 // Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
@ -24,9 +23,9 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
*/ */
credentialsExtractor: CredentialsExtractor; 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. * Verifies if the requested operation is allowed.
*/ */
@ -42,7 +41,7 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
*/ */
export class AuthenticatedLdpHandler extends BaseHttpHandler { export class AuthenticatedLdpHandler extends BaseHttpHandler {
private readonly credentialsExtractor: CredentialsExtractor; private readonly credentialsExtractor: CredentialsExtractor;
private readonly permissionsExtractor: PermissionsExtractor; private readonly modesExtractor: ModesExtractor;
private readonly authorizer: Authorizer; private readonly authorizer: Authorizer;
private readonly operationHandler: OperationHandler; private readonly operationHandler: OperationHandler;
@ -53,7 +52,7 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
public constructor(args: AuthenticatedLdpHandlerArgs) { public constructor(args: AuthenticatedLdpHandlerArgs) {
super(args); super(args);
this.credentialsExtractor = args.credentialsExtractor; this.credentialsExtractor = args.credentialsExtractor;
this.permissionsExtractor = args.permissionsExtractor; this.modesExtractor = args.modesExtractor;
this.authorizer = args.authorizer; this.authorizer = args.authorizer;
this.operationHandler = args.operationHandler; this.operationHandler = args.operationHandler;
} }
@ -81,13 +80,12 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request); const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`); this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
const permissions: PermissionSet = await this.permissionsExtractor.handleSafe(operation); const modes = await this.modesExtractor.handleSafe(operation);
const { read, write, append } = permissions; this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
this.logger.verbose(`Required permissions are read: ${read}, write: ${write}, append: ${append}`);
try { try {
const authorization = await this.authorizer const authorization = await this.authorizer
.handleSafe({ credentials, identifier: operation.target, permissions }); .handleSafe({ credentials, identifier: operation.target, modes });
operation.authorization = authorization; operation.authorization = authorization;
} catch (error: unknown) { } catch (error: unknown) {
this.logger.verbose(`Authorization failed: ${(error as any).message}`); 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 { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import type { Operation } from '../operations/Operation'; import type { Operation } from '../operations/Operation';
import type { PermissionSet } from './PermissionSet'; import { ModesExtractor } from './ModesExtractor';
import { PermissionsExtractor } from './PermissionsExtractor'; import { AccessMode } from './PermissionSet';
const READ_METHODS = new Set([ 'GET', 'HEAD' ]); const READ_METHODS = new Set([ 'GET', 'HEAD' ]);
const WRITE_METHODS = new Set([ 'PUT', 'DELETE' ]); 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. * Generates permissions for the base set of methods that always require the same permissions.
* Specifically: GET, HEAD, POST, PUT and DELETE. * Specifically: GET, HEAD, POST, PUT and DELETE.
*/ */
export class MethodPermissionsExtractor extends PermissionsExtractor { export class MethodModesExtractor extends ModesExtractor {
public async canHandle({ method }: Operation): Promise<void> { public async canHandle({ method }: Operation): Promise<void> {
if (!SUPPORTED_METHODS.has(method)) { if (!SUPPORTED_METHODS.has(method)) {
throw new NotImplementedHttpError(`Cannot determine permissions of ${method}`); throw new NotImplementedHttpError(`Cannot determine permissions of ${method}`);
} }
} }
public async handle({ method }: Operation): Promise<PermissionSet> { public async handle({ method }: Operation): Promise<Set<AccessMode>> {
const read = READ_METHODS.has(method); const result = new Set<AccessMode>();
const write = WRITE_METHODS.has(method); if (READ_METHODS.has(method)) {
const append = write || APPEND_METHODS.has(method); result.add(AccessMode.read);
const control = false; }
return { read, write, append, control }; 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). * A data interface indicating which permissions are required (based on the context).
*/ */
export interface PermissionSet { export type PermissionSet = Record<AccessMode, boolean>;
read: boolean;
append: boolean;
write: boolean;
control: 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 { SparqlUpdatePatch } from '../http/SparqlUpdatePatch';
import type { Operation } from '../operations/Operation'; import type { Operation } from '../operations/Operation';
import type { Representation } from '../representation/Representation'; import type { Representation } from '../representation/Representation';
import type { PermissionSet } from './PermissionSet'; import { ModesExtractor } from './ModesExtractor';
import { PermissionsExtractor } from './PermissionsExtractor'; import { AccessMode } from './PermissionSet';
/** export class SparqlPatchModesExtractor extends ModesExtractor {
* 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 {
public async canHandle({ method, body }: Operation): Promise<void> { public async canHandle({ method, body }: Operation): Promise<void> {
if (method !== 'PATCH') { if (method !== 'PATCH') {
throw new NotImplementedHttpError(`Cannot determine permissions of ${method}, only 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 // Verified in `canHandle` call
const update = (body as SparqlUpdatePatch).algebra as Algebra.DeleteInsert; 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. // Since `append` is a specific type of write, it is true if `write` is true.
const read = false; if (this.needsWrite(update)) {
const write = this.needsWrite(update); result.add(AccessMode.write);
const append = write || this.needsAppend(update); result.add(AccessMode.append);
const control = false; } else if (this.needsAppend(update)) {
return { read, write, append, control }; result.add(AccessMode.append);
}
return result;
} }
private isSparql(data: Representation): data is SparqlUpdatePatch { private isSparql(data: Representation): data is SparqlUpdatePatch {

View File

@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json", "files-scs:config/util/auxiliary/acl.json",

View File

@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/dynamic.json", "files-scs:config/storage/backend/dynamic.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -17,7 +17,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json", "files-scs:config/util/auxiliary/acl.json",

View File

@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json", "files-scs:config/ldp/modes/acl.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -1,12 +1,12 @@
import { CredentialGroup } from '../../../src/authentication/Credentials'; import { CredentialGroup } from '../../../src/authentication/Credentials';
import type { Credentials } from '../../../src/authentication/Credentials'; import type { CredentialSet } from '../../../src/authentication/Credentials';
import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor'; import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
import { UnionCredentialsExtractor } from '../../../src/authentication/UnionCredentialsExtractor'; import { UnionCredentialsExtractor } from '../../../src/authentication/UnionCredentialsExtractor';
import type { HttpRequest } from '../../../src/server/HttpRequest'; import type { HttpRequest } from '../../../src/server/HttpRequest';
describe('A UnionCredentialsExtractor', (): void => { describe('A UnionCredentialsExtractor', (): void => {
const agent: Credentials = { [CredentialGroup.agent]: { webId: 'http://test.com/#me' }}; const agent: CredentialSet = { [CredentialGroup.agent]: { webId: 'http://test.com/#me' }};
const everyone: Credentials = { [CredentialGroup.public]: {}}; const everyone: CredentialSet = { [CredentialGroup.public]: {}};
const request: HttpRequest = {} as any; const request: HttpRequest = {} as any;
let extractors: jest.Mocked<CredentialsExtractor>[]; let extractors: jest.Mocked<CredentialsExtractor>[];
let extractor: UnionCredentialsExtractor; let extractor: UnionCredentialsExtractor;

View File

@ -1,7 +1,7 @@
import type { Authorizer } from '../../../src/authorization/Authorizer'; import type { Authorizer } from '../../../src/authorization/Authorizer';
import { AuxiliaryAuthorizer } from '../../../src/authorization/AuxiliaryAuthorizer'; import { AuxiliaryAuthorizer } from '../../../src/authorization/AuxiliaryAuthorizer';
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; import { AccessMode } from '../../../src/ldp/permissions/PermissionSet';
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
@ -10,18 +10,13 @@ describe('An AuxiliaryAuthorizer', (): void => {
const credentials = {}; const credentials = {};
const associatedIdentifier = { path: 'http://test.com/foo' }; const associatedIdentifier = { path: 'http://test.com/foo' };
const auxiliaryIdentifier = { path: 'http://test.com/foo.dummy' }; const auxiliaryIdentifier = { path: 'http://test.com/foo.dummy' };
let permissions: PermissionSet; let modes: Set<AccessMode>;
let source: Authorizer; let source: Authorizer;
let strategy: AuxiliaryIdentifierStrategy; let strategy: AuxiliaryIdentifierStrategy;
let authorizer: AuxiliaryAuthorizer; let authorizer: AuxiliaryAuthorizer;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
permissions = { modes = new Set([ AccessMode.read, AccessMode.write, AccessMode.append ]);
read: true,
write: true,
append: true,
control: false,
};
source = { source = {
canHandle: jest.fn(), canHandle: jest.fn(),
@ -38,39 +33,39 @@ describe('An AuxiliaryAuthorizer', (): void => {
}); });
it('can handle auxiliary resources if the source supports the associated resource.', async(): Promise<void> => { it('can handle auxiliary resources if the source supports the associated resource.', async(): Promise<void> => {
await expect(authorizer.canHandle({ identifier: auxiliaryIdentifier, credentials, permissions })) await expect(authorizer.canHandle({ identifier: auxiliaryIdentifier, credentials, modes }))
.resolves.toBeUndefined(); .resolves.toBeUndefined();
expect(source.canHandle).toHaveBeenLastCalledWith( expect(source.canHandle).toHaveBeenLastCalledWith(
{ identifier: associatedIdentifier, credentials, permissions }, { identifier: associatedIdentifier, credentials, modes },
); );
await expect(authorizer.canHandle({ identifier: associatedIdentifier, credentials, permissions })) await expect(authorizer.canHandle({ identifier: associatedIdentifier, credentials, modes }))
.rejects.toThrow(NotImplementedHttpError); .rejects.toThrow(NotImplementedHttpError);
source.canHandle = jest.fn().mockRejectedValue(new Error('no source support')); source.canHandle = jest.fn().mockRejectedValue(new Error('no source support'));
await expect(authorizer.canHandle({ identifier: auxiliaryIdentifier, credentials, permissions })) await expect(authorizer.canHandle({ identifier: auxiliaryIdentifier, credentials, modes }))
.rejects.toThrow('no source support'); .rejects.toThrow('no source support');
}); });
it('handles resources by sending the updated parameters to the source.', async(): Promise<void> => { it('handles resources by sending the updated parameters to the source.', async(): Promise<void> => {
await expect(authorizer.handle({ identifier: auxiliaryIdentifier, credentials, permissions })) await expect(authorizer.handle({ identifier: auxiliaryIdentifier, credentials, modes }))
.resolves.toBeUndefined(); .resolves.toBeUndefined();
expect(source.handle).toHaveBeenLastCalledWith( expect(source.handle).toHaveBeenLastCalledWith(
{ identifier: associatedIdentifier, credentials, permissions }, { identifier: associatedIdentifier, credentials, modes },
); );
// Safety checks are not present when calling `handle` // Safety checks are not present when calling `handle`
await expect(authorizer.handle({ identifier: associatedIdentifier, credentials, permissions })) await expect(authorizer.handle({ identifier: associatedIdentifier, credentials, modes }))
.rejects.toThrow(NotImplementedHttpError); .rejects.toThrow(NotImplementedHttpError);
}); });
it('combines both checking and handling when calling handleSafe.', async(): Promise<void> => { it('combines both checking and handling when calling handleSafe.', async(): Promise<void> => {
await expect(authorizer.handleSafe({ identifier: auxiliaryIdentifier, credentials, permissions })) await expect(authorizer.handleSafe({ identifier: auxiliaryIdentifier, credentials, modes }))
.resolves.toBeUndefined(); .resolves.toBeUndefined();
expect(source.handleSafe).toHaveBeenLastCalledWith( expect(source.handleSafe).toHaveBeenLastCalledWith(
{ identifier: associatedIdentifier, credentials, permissions }, { identifier: associatedIdentifier, credentials, modes },
); );
await expect(authorizer.handleSafe({ identifier: associatedIdentifier, credentials, permissions })) await expect(authorizer.handleSafe({ identifier: associatedIdentifier, credentials, modes }))
.rejects.toThrow(NotImplementedHttpError); .rejects.toThrow(NotImplementedHttpError);
source.handleSafe = jest.fn().mockRejectedValue(new Error('no source support')); source.handleSafe = jest.fn().mockRejectedValue(new Error('no source support'));
await expect(authorizer.handleSafe({ identifier: auxiliaryIdentifier, credentials, permissions })) await expect(authorizer.handleSafe({ identifier: auxiliaryIdentifier, credentials, modes }))
.rejects.toThrow('no source support'); .rejects.toThrow('no source support');
}); });
}); });

View File

@ -1,5 +1,6 @@
import type { Authorizer, AuthorizerInput } from '../../../src/authorization/Authorizer'; import type { Authorizer, AuthorizerInput } from '../../../src/authorization/Authorizer';
import { PathBasedAuthorizer } from '../../../src/authorization/PathBasedAuthorizer'; import { PathBasedAuthorizer } from '../../../src/authorization/PathBasedAuthorizer';
import { AccessMode } from '../../../src/ldp/permissions/PermissionSet';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
describe('A PathBasedAuthorizer', (): void => { describe('A PathBasedAuthorizer', (): void => {
@ -11,7 +12,7 @@ describe('A PathBasedAuthorizer', (): void => {
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
input = { input = {
identifier: { path: `${baseUrl}first` }, identifier: { path: `${baseUrl}first` },
permissions: { read: true, append: false, write: false, control: false }, modes: new Set([ AccessMode.read ]),
credentials: {}, credentials: {},
}; };

View File

@ -5,7 +5,7 @@ import type { AccessChecker } from '../../../src/authorization/access-checkers/A
import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization'; import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization';
import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer'; import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer';
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; import { AccessMode } from '../../../src/ldp/permissions/PermissionSet';
import type { Representation } from '../../../src/ldp/representation/Representation'; import type { Representation } from '../../../src/ldp/representation/Representation';
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
import type { ResourceStore } from '../../../src/storage/ResourceStore'; import type { ResourceStore } from '../../../src/storage/ResourceStore';
@ -31,19 +31,14 @@ describe('A WebAclAuthorizer', (): void => {
} as any; } as any;
let store: jest.Mocked<ResourceStore>; let store: jest.Mocked<ResourceStore>;
const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/'); const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/');
let permissions: PermissionSet; let modes: Set<AccessMode>;
let credentials: CredentialSet; let credentials: CredentialSet;
let identifier: ResourceIdentifier; let identifier: ResourceIdentifier;
let authorization: WebAclAuthorization; let authorization: WebAclAuthorization;
let accessChecker: jest.Mocked<AccessChecker>; let accessChecker: jest.Mocked<AccessChecker>;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
permissions = { modes = new Set([ AccessMode.read, AccessMode.write ]);
read: true,
append: false,
write: true,
control: false,
};
credentials = { [CredentialGroup.public]: {}, [CredentialGroup.agent]: {}}; credentials = { [CredentialGroup.public]: {}, [CredentialGroup.agent]: {}};
identifier = { path: 'http://test.com/foo' }; identifier = { path: 'http://test.com/foo' };
authorization = new WebAclAuthorization( authorization = new WebAclAuthorization(
@ -73,7 +68,7 @@ describe('A WebAclAuthorizer', (): void => {
}); });
it('handles all non-acl inputs.', async(): Promise<void> => { it('handles all non-acl inputs.', async(): Promise<void> => {
await expect(authorizer.canHandle({ identifier, credentials, permissions })).resolves.toBeUndefined(); await expect(authorizer.canHandle({ identifier, credentials, modes })).resolves.toBeUndefined();
await expect(authorizer.canHandle({ identifier: aclStrategy.getAuxiliaryIdentifier(identifier) } as any)) await expect(authorizer.canHandle({ identifier: aclStrategy.getAuxiliaryIdentifier(identifier) } as any))
.rejects.toThrow(NotImplementedHttpError); .rejects.toThrow(NotImplementedHttpError);
}); });
@ -89,7 +84,7 @@ describe('A WebAclAuthorizer', (): void => {
]) } as Representation); ]) } as Representation);
Object.assign(authorization.everyone, { read: true, write: true, append: true, control: false }); Object.assign(authorization.everyone, { read: true, write: true, append: true, control: false });
Object.assign(authorization.user, { read: true, write: true, append: true, control: false }); Object.assign(authorization.user, { read: true, write: true, append: true, control: false });
await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); await expect(authorizer.handle({ identifier, modes, credentials })).resolves.toEqual(authorization);
}); });
it('allows access if the acl file allows all agents.', async(): Promise<void> => { it('allows access if the acl file allows all agents.', async(): Promise<void> => {
@ -102,7 +97,7 @@ describe('A WebAclAuthorizer', (): void => {
]) } as Representation); ]) } as Representation);
Object.assign(authorization.everyone, { read: true, write: true, append: true }); Object.assign(authorization.everyone, { read: true, write: true, append: true });
Object.assign(authorization.user, { read: true, write: true, append: true }); Object.assign(authorization.user, { read: true, write: true, append: true });
await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); await expect(authorizer.handle({ identifier, modes, credentials })).resolves.toEqual(authorization);
}); });
it('allows access if there is a parent acl file allowing all agents.', async(): Promise<void> => { it('allows access if there is a parent acl file allowing all agents.', async(): Promise<void> => {
@ -122,7 +117,7 @@ describe('A WebAclAuthorizer', (): void => {
}); });
Object.assign(authorization.everyone, { read: true, write: true, append: true }); Object.assign(authorization.everyone, { read: true, write: true, append: true });
Object.assign(authorization.user, { read: true, write: true, append: true }); Object.assign(authorization.user, { read: true, write: true, append: true });
await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); await expect(authorizer.handle({ identifier, modes, credentials })).resolves.toEqual(authorization);
}); });
it('throws a ForbiddenHttpError if access is not granted and credentials have a WebID.', async(): Promise<void> => { it('throws a ForbiddenHttpError if access is not granted and credentials have a WebID.', async(): Promise<void> => {
@ -132,7 +127,7 @@ describe('A WebAclAuthorizer', (): void => {
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
]) } as Representation); ]) } as Representation);
credentials.agent = { webId: 'http://test.com/alice/profile/card#me' }; credentials.agent = { webId: 'http://test.com/alice/profile/card#me' };
await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError); await expect(authorizer.handle({ identifier, modes, credentials })).rejects.toThrow(ForbiddenHttpError);
}); });
it('throws an UnauthorizedHttpError if access is not granted there are no credentials.', async(): Promise<void> => { it('throws an UnauthorizedHttpError if access is not granted there are no credentials.', async(): Promise<void> => {
@ -142,30 +137,25 @@ describe('A WebAclAuthorizer', (): void => {
quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)), quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
]) } as Representation); ]) } as Representation);
await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(UnauthorizedHttpError); await expect(authorizer.handle({ identifier, modes, credentials })).rejects.toThrow(UnauthorizedHttpError);
}); });
it('re-throws ResourceStore errors as internal errors.', async(): Promise<void> => { it('re-throws ResourceStore errors as internal errors.', async(): Promise<void> => {
store.getRepresentation.mockRejectedValue(new Error('TEST!')); store.getRepresentation.mockRejectedValue(new Error('TEST!'));
const promise = authorizer.handle({ identifier, permissions, credentials }); const promise = authorizer.handle({ identifier, modes, credentials });
await expect(promise).rejects.toThrow(`Error reading ACL for ${identifier.path}: TEST!`); await expect(promise).rejects.toThrow(`Error reading ACL for ${identifier.path}: TEST!`);
await expect(promise).rejects.toThrow(InternalServerError); await expect(promise).rejects.toThrow(InternalServerError);
}); });
it('errors if the root container has no corresponding acl document.', async(): Promise<void> => { it('errors if the root container has no corresponding acl document.', async(): Promise<void> => {
store.getRepresentation.mockRejectedValue(new NotFoundHttpError()); store.getRepresentation.mockRejectedValue(new NotFoundHttpError());
const promise = authorizer.handle({ identifier, permissions, credentials }); const promise = authorizer.handle({ identifier, modes, credentials });
await expect(promise).rejects.toThrow('No ACL document found for root container'); await expect(promise).rejects.toThrow('No ACL document found for root container');
await expect(promise).rejects.toThrow(ForbiddenHttpError); await expect(promise).rejects.toThrow(ForbiddenHttpError);
}); });
it('allows an agent to append if they have write access.', async(): Promise<void> => { it('allows an agent to append if they have write access.', async(): Promise<void> => {
permissions = { modes = new Set([ AccessMode.append ]);
read: false,
write: false,
append: true,
control: false,
};
store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([ store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([
quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)), quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
@ -173,6 +163,6 @@ describe('A WebAclAuthorizer', (): void => {
]) } as Representation); ]) } as Representation);
Object.assign(authorization.everyone, { write: true, append: true }); Object.assign(authorization.everyone, { write: true, append: true });
Object.assign(authorization.user, { write: true, append: true }); Object.assign(authorization.user, { write: true, append: true });
await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); await expect(authorizer.handle({ identifier, modes, credentials })).resolves.toEqual(authorization);
}); });
}); });

View File

@ -5,7 +5,7 @@ import { AuthenticatedLdpHandler } from '../../../src/ldp/AuthenticatedLdpHandle
import { ResetResponseDescription } from '../../../src/ldp/http/response/ResetResponseDescription'; import { ResetResponseDescription } from '../../../src/ldp/http/response/ResetResponseDescription';
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription'; import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
import type { Operation } from '../../../src/ldp/operations/Operation'; import type { Operation } from '../../../src/ldp/operations/Operation';
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; import { AccessMode } from '../../../src/ldp/permissions/PermissionSet';
import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences'; import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences';
import * as LogUtil from '../../../src/logging/LogUtil'; import * as LogUtil from '../../../src/logging/LogUtil';
import type { HttpRequest } from '../../../src/server/HttpRequest'; import type { HttpRequest } from '../../../src/server/HttpRequest';
@ -17,7 +17,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
const preferences: RepresentationPreferences = { type: { 'text/turtle': 0.9 }}; const preferences: RepresentationPreferences = { type: { 'text/turtle': 0.9 }};
let operation: Operation; let operation: Operation;
const credentials: CredentialSet = {}; const credentials: CredentialSet = {};
const permissions: PermissionSet = { read: true, write: false, append: false, control: false }; const modes: Set<AccessMode> = new Set([ AccessMode.read ]);
const authorization: Authorization = { addMetadata: jest.fn() }; const authorization: Authorization = { addMetadata: jest.fn() };
const result: ResponseDescription = new ResetResponseDescription(); const result: ResponseDescription = new ResetResponseDescription();
const errorResult: ResponseDescription = { statusCode: 500 }; const errorResult: ResponseDescription = { statusCode: 500 };
@ -32,7 +32,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
handleSafe: jest.fn().mockResolvedValue(operation), handleSafe: jest.fn().mockResolvedValue(operation),
} as any, } as any,
credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any, credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
permissionsExtractor: { handleSafe: jest.fn().mockResolvedValue(permissions) } as any, modesExtractor: { handleSafe: jest.fn().mockResolvedValue(modes) } as any,
authorizer: { handleSafe: jest.fn().mockResolvedValue(authorization) } as any, authorizer: { handleSafe: jest.fn().mockResolvedValue(authorization) } as any,
operationHandler: { handleSafe: jest.fn().mockResolvedValue(result) } as any, operationHandler: { handleSafe: jest.fn().mockResolvedValue(result) } as any,
errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any, errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any,
@ -63,11 +63,11 @@ describe('An AuthenticatedLdpHandler', (): void => {
expect(args.requestParser.handleSafe).toHaveBeenLastCalledWith(request); expect(args.requestParser.handleSafe).toHaveBeenLastCalledWith(request);
expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1); expect(args.credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request); expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
expect(args.permissionsExtractor.handleSafe).toHaveBeenCalledTimes(1); expect(args.modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(args.permissionsExtractor.handleSafe).toHaveBeenLastCalledWith(operation); expect(args.modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(1); expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(1);
expect(args.authorizer.handleSafe) expect(args.authorizer.handleSafe)
.toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, permissions }); .toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, modes });
expect(operation.authorization).toBe(authorization); expect(operation.authorization).toBe(authorization);
expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1); expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation); expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation);

View File

@ -1,15 +1,16 @@
import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import { AclPermissionsExtractor } from '../../../../src/ldp/permissions/AclPermissionsExtractor'; import { AclModesExtractor } from '../../../../src/ldp/permissions/AclModesExtractor';
import { AccessMode } from '../../../../src/ldp/permissions/PermissionSet';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('An AclPermissionsExtractor', (): void => { describe('An AclModesExtractor', (): void => {
let extractor: AclPermissionsExtractor; let extractor: AclModesExtractor;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
const aclStrategy = { const aclStrategy = {
isAuxiliaryIdentifier: (id): boolean => id.path.endsWith('.acl'), isAuxiliaryIdentifier: (id): boolean => id.path.endsWith('.acl'),
} as AuxiliaryIdentifierStrategy; } as AuxiliaryIdentifierStrategy;
extractor = new AclPermissionsExtractor(aclStrategy); extractor = new AclModesExtractor(aclStrategy);
}); });
it('can only handle acl files.', async(): Promise<void> => { it('can only handle acl files.', async(): Promise<void> => {
@ -20,11 +21,6 @@ describe('An AclPermissionsExtractor', (): void => {
}); });
it('returns control permissions.', async(): Promise<void> => { it('returns control permissions.', async(): Promise<void> => {
await expect(extractor.handle()).resolves.toEqual({ await expect(extractor.handle()).resolves.toEqual(new Set([ AccessMode.control ]));
read: false,
write: false,
append: false,
control: true,
});
}); });
}); });

View File

@ -1,9 +1,10 @@
import type { Operation } from '../../../../src/ldp/operations/Operation'; import type { Operation } from '../../../../src/ldp/operations/Operation';
import { MethodPermissionsExtractor } from '../../../../src/ldp/permissions/MethodPermissionsExtractor'; import { MethodModesExtractor } from '../../../../src/ldp/permissions/MethodModesExtractor';
import { AccessMode } from '../../../../src/ldp/permissions/PermissionSet';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('A MethodPermissionsExtractor', (): void => { describe('A MethodModesExtractor', (): void => {
const extractor = new MethodPermissionsExtractor(); const extractor = new MethodModesExtractor();
it('can handle HEAD/GET/POST/PUT/DELETE.', async(): Promise<void> => { it('can handle HEAD/GET/POST/PUT/DELETE.', async(): Promise<void> => {
await expect(extractor.canHandle({ method: 'HEAD' } as Operation)).resolves.toBeUndefined(); await expect(extractor.canHandle({ method: 'HEAD' } as Operation)).resolves.toBeUndefined();
@ -15,47 +16,24 @@ describe('A MethodPermissionsExtractor', (): void => {
}); });
it('requires read for HEAD operations.', async(): Promise<void> => { it('requires read for HEAD operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'HEAD' } as Operation)).resolves.toEqual({ await expect(extractor.handle({ method: 'HEAD' } as Operation)).resolves.toEqual(new Set([ AccessMode.read ]));
read: true,
append: false,
write: false,
control: false,
});
}); });
it('requires read for GET operations.', async(): Promise<void> => { it('requires read for GET operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'GET' } as Operation)).resolves.toEqual({ await expect(extractor.handle({ method: 'GET' } as Operation)).resolves.toEqual(new Set([ AccessMode.read ]));
read: true,
append: false,
write: false,
control: false,
});
}); });
it('requires append for POST operations.', async(): Promise<void> => { it('requires append for POST operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'POST' } as Operation)).resolves.toEqual({ await expect(extractor.handle({ method: 'POST' } as Operation)).resolves.toEqual(new Set([ AccessMode.append ]));
read: false,
append: true,
write: false,
control: false,
});
}); });
it('requires write for PUT operations.', async(): Promise<void> => { it('requires write for PUT operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'PUT' } as Operation)).resolves.toEqual({ await expect(extractor.handle({ method: 'PUT' } as Operation))
read: false, .resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ]));
append: true,
write: true,
control: false,
});
}); });
it('requires write for DELETE operations.', async(): Promise<void> => { it('requires write for DELETE operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'DELETE' } as Operation)).resolves.toEqual({ await expect(extractor.handle({ method: 'DELETE' } as Operation))
read: false, .resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ]));
append: true,
write: true,
control: false,
});
}); });
}); });

View File

@ -1,11 +1,12 @@
import { Factory } from 'sparqlalgebrajs'; import { Factory } from 'sparqlalgebrajs';
import type { SparqlUpdatePatch } from '../../../../src/ldp/http/SparqlUpdatePatch'; import type { SparqlUpdatePatch } from '../../../../src/ldp/http/SparqlUpdatePatch';
import type { Operation } from '../../../../src/ldp/operations/Operation'; import type { Operation } from '../../../../src/ldp/operations/Operation';
import { SparqlPatchPermissionsExtractor } from '../../../../src/ldp/permissions/SparqlPatchPermissionsExtractor'; import { AccessMode } from '../../../../src/ldp/permissions/PermissionSet';
import { SparqlPatchModesExtractor } from '../../../../src/ldp/permissions/SparqlPatchModesExtractor';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('A SparqlPatchPermissionsExtractor', (): void => { describe('A SparqlPatchModesExtractor', (): void => {
const extractor = new SparqlPatchPermissionsExtractor(); const extractor = new SparqlPatchModesExtractor();
const factory = new Factory(); const factory = new Factory();
it('can only handle (composite) SPARQL DELETE/INSERT PATCH operations.', async(): Promise<void> => { it('can only handle (composite) SPARQL DELETE/INSERT PATCH operations.', async(): Promise<void> => {
@ -37,12 +38,7 @@ describe('A SparqlPatchPermissionsExtractor', (): void => {
method: 'PATCH', method: 'PATCH',
body: { algebra: factory.createNop() }, body: { algebra: factory.createNop() },
} as unknown as Operation; } as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual({ await expect(extractor.handle(operation)).resolves.toEqual(new Set());
read: false,
append: false,
write: false,
control: false,
});
}); });
it('requires append for INSERT operations.', async(): Promise<void> => { it('requires append for INSERT operations.', async(): Promise<void> => {
@ -52,12 +48,7 @@ describe('A SparqlPatchPermissionsExtractor', (): void => {
factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')), factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')),
]) }, ]) },
} as unknown as Operation; } as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual({ await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append ]));
read: false,
append: true,
write: false,
control: false,
});
}); });
it('requires write for DELETE operations.', async(): Promise<void> => { it('requires write for DELETE operations.', async(): Promise<void> => {
@ -67,12 +58,7 @@ describe('A SparqlPatchPermissionsExtractor', (): void => {
factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')), factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')),
]) }, ]) },
} as unknown as Operation; } as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual({ await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ]));
read: false,
append: true,
write: true,
control: false,
});
}); });
it('requires append for composite operations with an insert.', async(): Promise<void> => { it('requires append for composite operations with an insert.', async(): Promise<void> => {
@ -82,12 +68,7 @@ describe('A SparqlPatchPermissionsExtractor', (): void => {
factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')), factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')),
]) ]) }, ]) ]) },
} as unknown as Operation; } as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual({ await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append ]));
read: false,
append: true,
write: false,
control: false,
});
}); });
it('requires write for composite operations with a delete.', async(): Promise<void> => { it('requires write for composite operations with a delete.', async(): Promise<void> => {
@ -100,11 +81,6 @@ describe('A SparqlPatchPermissionsExtractor', (): void => {
factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')), factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')),
]) ]) }, ]) ]) },
} as unknown as Operation; } as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual({ await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ]));
read: false,
append: true,
write: true,
control: false,
});
}); });
}); });