From 0545ca121eedec5541900aa1411dbeea8af015e2 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 7 Aug 2020 11:54:27 +0200 Subject: [PATCH] feat: Add acl support --- src/authorization/AclConstants.ts | 20 +++ src/authorization/AclManager.ts | 26 +++ src/authorization/SimpleAclAuthorizer.ts | 152 ++++++++++++++++ .../SimpleExtensionAclManager.ts | 26 +++ src/ldp/permissions/PermissionSet.ts | 1 - .../permissions/SimplePermissionsExtractor.ts | 7 +- src/storage/ContainerManager.ts | 16 ++ src/storage/UrlContainerManager.ts | 34 ++++ src/util/errors/ForbiddenHttpError.ts | 10 ++ src/util/errors/UnauthorizedHttpError.ts | 10 ++ .../authorization/SimpleAclAuthorizer.test.ts | 170 ++++++++++++++++++ .../SimpleExtensionAclManager.test.ts | 20 +++ .../SimplePermissionsExtractor.test.ts | 14 +- test/unit/storage/UrlContainerManager.test.ts | 31 ++++ 14 files changed, 524 insertions(+), 13 deletions(-) create mode 100644 src/authorization/AclConstants.ts create mode 100644 src/authorization/AclManager.ts create mode 100644 src/authorization/SimpleAclAuthorizer.ts create mode 100644 src/authorization/SimpleExtensionAclManager.ts create mode 100644 src/storage/ContainerManager.ts create mode 100644 src/storage/UrlContainerManager.ts create mode 100644 src/util/errors/ForbiddenHttpError.ts create mode 100644 src/util/errors/UnauthorizedHttpError.ts create mode 100644 test/unit/authorization/SimpleAclAuthorizer.test.ts create mode 100644 test/unit/authorization/SimpleExtensionAclManager.test.ts create mode 100644 test/unit/storage/UrlContainerManager.test.ts diff --git a/src/authorization/AclConstants.ts b/src/authorization/AclConstants.ts new file mode 100644 index 000000000..43751e55b --- /dev/null +++ b/src/authorization/AclConstants.ts @@ -0,0 +1,20 @@ +const ACL_PREFIX = 'http://www.w3.org/ns/auth/acl#'; +const FOAF_PREFIX = 'http://xmlns.com/foaf/0.1/'; + +export const ACL = { + accessTo: `${ACL_PREFIX}accessTo`, + agent: `${ACL_PREFIX}agent`, + agentClass: `${ACL_PREFIX}agentClass`, + default: `${ACL_PREFIX}default`, + mode: `${ACL_PREFIX}mode`, + + Write: `${ACL_PREFIX}Write`, + Read: `${ACL_PREFIX}Read`, + Append: `${ACL_PREFIX}Append`, + Control: `${ACL_PREFIX}Control`, +}; + +export const FOAF = { + Agent: `${FOAF_PREFIX}Agent`, + AuthenticatedAgent: `${FOAF_PREFIX}AuthenticatedAgent`, +}; diff --git a/src/authorization/AclManager.ts b/src/authorization/AclManager.ts new file mode 100644 index 000000000..b26a71506 --- /dev/null +++ b/src/authorization/AclManager.ts @@ -0,0 +1,26 @@ +import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; + +/** + * Handles where acl files are stored. + */ +export interface AclManager { + /** + * Returns the identifier of the acl file corresponding to the given resource. + * This does not guarantee that this acl file exists. + * In the case the input is already an acl file that will also be the response. + * @param id - The ResourceIdentifier of which we need the corresponding acl file. + * + * @returns The ResourceIdentifier of the corresponding acl file. + */ + getAcl: (id: ResourceIdentifier) => Promise; + + /** + * Checks if the input identifier corresponds to an acl file. + * This does not check if that acl file exists, + * only if the identifier indicates that there could be an acl file there. + * @param id - Identifier to check. + * + * @returns true if the input identifier points to an acl file. + */ + isAcl: (id: ResourceIdentifier) => Promise; +} diff --git a/src/authorization/SimpleAclAuthorizer.ts b/src/authorization/SimpleAclAuthorizer.ts new file mode 100644 index 000000000..796530b76 --- /dev/null +++ b/src/authorization/SimpleAclAuthorizer.ts @@ -0,0 +1,152 @@ +import { AclManager } from './AclManager'; +import { ContainerManager } from '../storage/ContainerManager'; +import { CONTENT_TYPE_QUADS } from '../util/ContentTypes'; +import { Credentials } from '../authentication/Credentials'; +import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError'; +import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; +import { PermissionSet } from '../ldp/permissions/PermissionSet'; +import { Representation } from '../ldp/representation/Representation'; +import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; +import { ResourceStore } from '../storage/ResourceStore'; +import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError'; +import { ACL, FOAF } from './AclConstants'; +import { Authorizer, AuthorizerArgs } from './Authorizer'; +import { Quad, Store, Term } from 'n3'; + +/** + * Handles most web access control predicates such as + * `acl:mode`, `acl:agentClass`, `acl:agent`, `acl:default` and `acl:accessTo`. + * Does not support `acl:agentGroup`, `acl:origin` and `acl:trustedApp` yet. + */ +export class SimpleAclAuthorizer extends Authorizer { + private readonly aclManager: AclManager; + private readonly containerManager: ContainerManager; + private readonly resourceStore: ResourceStore; + + public constructor(aclManager: AclManager, containerManager: ContainerManager, resourceStore: ResourceStore) { + super(); + this.aclManager = aclManager; + this.containerManager = containerManager; + this.resourceStore = resourceStore; + } + + public async canHandle(): Promise { + // Can handle everything + } + + /** + * Checks if an agent is allowed to execute the requested actions. + * 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(input: AuthorizerArgs): Promise { + const store = await this.getAclRecursive(input.identifier); + if (await this.aclManager.isAcl(input.identifier)) { + this.checkPermission(input.credentials, store, 'control'); + } else { + (Object.keys(input.permissions) as (keyof PermissionSet)[]).forEach((key): void => { + if (input.permissions[key]) { + this.checkPermission(input.credentials, store, key); + } + }); + } + } + + /** + * Checks if any of the triples in the store grant the agent permission to use the given mode. + * Throws a {@link ForbiddenHttpError} or {@link UnauthorizedHttpError} depending on the credentials + * if access is not allowed. + * @param agent - Agent that wants access. + * @param store - A store containing the relevant triples for authorization. + * @param mode - Which mode is requested. Probable one of ('write' | 'read' | 'append' | 'control'). + */ + private checkPermission(agent: Credentials, store: Store, mode: string): void { + const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control']; + const auths = store.getQuads(null, ACL.mode, modeString, null).map((quad: Quad): Term => quad.subject); + if (!auths.some((term): boolean => this.hasAccess(agent, term, store))) { + throw typeof agent.webID === 'string' ? new ForbiddenHttpError() : new UnauthorizedHttpError(); + } + } + + /** + * Capitalizes the input string. + * @param mode - String to transform. + * + * @returns The capitalized string. + */ + private capitalize(mode: string): string { + return `${mode[0].toUpperCase()}${mode.slice(1).toLowerCase()}`; + } + + /** + * Checks if the given agent has access to the modes specified by the given authorization. + * @param agent - Credentials of agent that needs access. + * @param auth - acl:Authorization that needs to be checked. + * @param store - A store containing the relevant triples of the authorization. + * + * @returns If the agent has access. + */ + private hasAccess(agent: Credentials, auth: Term, store: Store): boolean { + if (store.countQuads(auth, ACL.agentClass, FOAF.Agent, null) > 0) { + return true; + } + if (typeof agent.webID !== 'string') { + return false; + } + if (store.countQuads(auth, ACL.agentClass, FOAF.AuthenticatedAgent, null) > 0) { + return true; + } + return store.countQuads(auth, ACL.agent, agent.webID, null) > 0; + } + + /** + * Returns the acl triples that are relevant for the given identifier. + * These can either be from a corresponding acl file or an acl file higher up with defaults. + * Rethrows any non-NotFoundHttpErrors thrown by the AclManager or ResourceStore. + * @param id - ResourceIdentifier of which we need the acl triples. + * @param recurse - Only used internally for recursion. + * + * @returns A store containing the relevant acl triples. + */ + private async getAclRecursive(id: ResourceIdentifier, recurse?: boolean): Promise { + try { + const acl = await this.aclManager.getAcl(id); + const data = await this.resourceStore.getRepresentation(acl, { type: [{ value: CONTENT_TYPE_QUADS, weight: 1 }]}); + return this.filterData(data, recurse ? ACL.default : ACL.accessTo, id.path); + } catch (error) { + if (!(error instanceof NotFoundHttpError)) { + throw error; + } + + const parent = await this.containerManager.getContainer(id); + return this.getAclRecursive(parent, true); + } + } + + /** + * Finds all triples in the data stream of the given representation that use the given predicate and object. + * Then extracts the unique subjects from those triples, + * and returns a Store containing all triples from the data stream that have such a subject. + * + * This can be useful for finding the `acl:Authorization` objects corresponding to a specific URI + * and returning all relevant information on them. + * @param data - Representation with data stream of internal/quads. + * @param predicate - Predicate to match. + * @param object - Object to match. + * + * @returns A store containing the relevant triples. + */ + private async filterData(data: Representation, predicate: string, object: string): Promise { + const store = new Store(); + const importEmitter = store.import(data.data); + await new Promise((resolve, reject): void => { + importEmitter.on('end', resolve); + importEmitter.on('error', reject); + }); + + const auths = store.getQuads(null, predicate, object, null).map((quad: Quad): Term => quad.subject); + const newStore = new Store(); + auths.forEach((subject): any => newStore.addQuads(store.getQuads(subject, null, null, null))); + return newStore; + } +} diff --git a/src/authorization/SimpleExtensionAclManager.ts b/src/authorization/SimpleExtensionAclManager.ts new file mode 100644 index 000000000..b54d40a4b --- /dev/null +++ b/src/authorization/SimpleExtensionAclManager.ts @@ -0,0 +1,26 @@ +import { AclManager } from './AclManager'; +import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; + +/** + * Generates acl URIs by adding an .acl file extension. + * + * What actually should happen in getAcl: + * 1. Return id if it isAcl + * 2. Check store if id exists + * 3a. (true) Close/destroy data stream! To prevent potential locking issues. + * 4a. Check metadata if it is a container or a resource. + * 3b. (false) Use input metadata/heuristic to check if container or resource. + * 5. Generate the correct identifier (.acl right of / for containers, left for resources if there is a /) + * + * It is potentially possible that an agent wants to generate the acl file before generating the actual file. + * (Unless this is not allowed by the spec, need to verify). + */ +export class SimpleExtensionAclManager implements AclManager { + public async getAcl(id: ResourceIdentifier): Promise { + return await this.isAcl(id) ? id : { path: `${id.path}.acl` }; + } + + public async isAcl(id: ResourceIdentifier): Promise { + return /\.acl\/?/u.test(id.path); + } +} diff --git a/src/ldp/permissions/PermissionSet.ts b/src/ldp/permissions/PermissionSet.ts index 13eb7e02f..b3da3fe6a 100644 --- a/src/ldp/permissions/PermissionSet.ts +++ b/src/ldp/permissions/PermissionSet.ts @@ -5,5 +5,4 @@ export interface PermissionSet { read: boolean; append: boolean; write: boolean; - delete: boolean; } diff --git a/src/ldp/permissions/SimplePermissionsExtractor.ts b/src/ldp/permissions/SimplePermissionsExtractor.ts index b6c24c210..34b0b1b7e 100644 --- a/src/ldp/permissions/SimplePermissionsExtractor.ts +++ b/src/ldp/permissions/SimplePermissionsExtractor.ts @@ -11,11 +11,12 @@ export class SimplePermissionsExtractor extends PermissionsExtractor { } public async handle(input: Operation): Promise { - return { + const result = { read: input.method === 'GET', append: false, - write: input.method === 'POST' || input.method === 'PUT', - delete: input.method === 'DELETE', + write: input.method === 'POST' || input.method === 'PUT' || input.method === 'DELETE', }; + result.append = result.write; + return result; } } diff --git a/src/storage/ContainerManager.ts b/src/storage/ContainerManager.ts new file mode 100644 index 000000000..b2d5ea26d --- /dev/null +++ b/src/storage/ContainerManager.ts @@ -0,0 +1,16 @@ +import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; + +/** + * Handles the identification of containers in which a resource is contained. + */ +export interface ContainerManager { + /** + * Finds the corresponding container. + * Should throw an error if there is no such container (in the case of root). + * + * @param id - Identifier to find container of. + * + * @returns The identifier of the container this resource is in. + */ + getContainer: (id: ResourceIdentifier) => Promise; +} diff --git a/src/storage/UrlContainerManager.ts b/src/storage/UrlContainerManager.ts new file mode 100644 index 000000000..e6ce845ea --- /dev/null +++ b/src/storage/UrlContainerManager.ts @@ -0,0 +1,34 @@ +import { ContainerManager } from './ContainerManager'; +import { ensureTrailingSlash } from '../util/Util'; +import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; + +/** + * Determines containers based on URL decomposition. + */ +export class UrlContainerManager implements ContainerManager { + private readonly root: string; + + public constructor(root: string) { + this.root = this.canonicalUrl(root); + } + + public async getContainer(id: ResourceIdentifier): Promise { + const path = this.canonicalUrl(id.path); + if (this.root === path) { + throw new Error('Root does not have a container.'); + } + + const parentPath = new URL('..', path).toString(); + + // This probably means there is an issue with the root + if (parentPath === path) { + throw new Error('URL root reached.'); + } + + return { path: parentPath }; + } + + private canonicalUrl(path: string): string { + return ensureTrailingSlash(new URL(path).toString()); + } +} diff --git a/src/util/errors/ForbiddenHttpError.ts b/src/util/errors/ForbiddenHttpError.ts new file mode 100644 index 000000000..f12466793 --- /dev/null +++ b/src/util/errors/ForbiddenHttpError.ts @@ -0,0 +1,10 @@ +import { HttpError } from './HttpError'; + +/** + * An error thrown when an agent is not allowed to access data. + */ +export class ForbiddenHttpError extends HttpError { + public constructor(message?: string) { + super(403, 'ForbiddenHttpError', message); + } +} diff --git a/src/util/errors/UnauthorizedHttpError.ts b/src/util/errors/UnauthorizedHttpError.ts new file mode 100644 index 000000000..fcbfc1c79 --- /dev/null +++ b/src/util/errors/UnauthorizedHttpError.ts @@ -0,0 +1,10 @@ +import { HttpError } from './HttpError'; + +/** + * An error thrown when an agent is not authorized. + */ +export class UnauthorizedHttpError extends HttpError { + public constructor(message?: string) { + super(401, 'UnauthorizedHttpError', message); + } +} diff --git a/test/unit/authorization/SimpleAclAuthorizer.test.ts b/test/unit/authorization/SimpleAclAuthorizer.test.ts new file mode 100644 index 000000000..26c46c39e --- /dev/null +++ b/test/unit/authorization/SimpleAclAuthorizer.test.ts @@ -0,0 +1,170 @@ +import { AclManager } from '../../../src/authorization/AclManager'; +import { ContainerManager } from '../../../src/storage/ContainerManager'; +import { Credentials } from '../../../src/authentication/Credentials'; +import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError'; +import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; +import { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; +import { Representation } from '../../../src/ldp/representation/Representation'; +import { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; +import { ResourceStore } from '../../../src/storage/ResourceStore'; +import { SimpleAclAuthorizer } from '../../../src/authorization/SimpleAclAuthorizer'; +import streamifyArray from 'streamify-array'; +import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError'; +import { namedNode, quad } from '@rdfjs/data-model'; + +const nn = namedNode; + +const acl = 'http://www.w3.org/ns/auth/acl#'; + +describe('A SimpleAclAuthorizer', (): void => { + let authorizer: SimpleAclAuthorizer; + const aclManager: AclManager = { + getAcl: async(id: ResourceIdentifier): Promise => + id.path.endsWith('.acl') ? id : { path: `${id.path}.acl` }, + isAcl: async(id: ResourceIdentifier): Promise => id.path.endsWith('.acl'), + }; + const containerManager: ContainerManager = { + getContainer: async(id: ResourceIdentifier): Promise => + ({ path: new URL('..', id.path).toString() }), + }; + let permissions: PermissionSet; + let credentials: Credentials; + let identifier: ResourceIdentifier; + + beforeEach(async(): Promise => { + permissions = { + read: true, + append: false, + write: false, + }; + credentials = {}; + identifier = { path: 'http://test.com/foo' }; + }); + + it('handles all inputs.', async(): Promise => { + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, null as any); + await expect(authorizer.canHandle()).resolves.toBeUndefined(); + }); + + it('allows access if the acl file allows all agents.', async(): Promise => { + const store = { + getRepresentation: async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation), + } as unknown as ResourceStore; + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); + }); + + it('allows access if there is a parent acl file allowing all agents.', async(): Promise => { + const store = { + async getRepresentation(id: ResourceIdentifier): Promise { + if (id.path.endsWith('foo.acl')) { + throw new NotFoundHttpError(); + } + return { + data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')), + quad(nn('auth'), nn(`${acl}default`), nn((await containerManager.getContainer(identifier)).path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]), + } as Representation; + }, + } as unknown as ResourceStore; + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); + }); + + it('allows access to authorized agents if the acl files allows all authorized users.', async(): Promise => { + const store = { + getRepresentation: async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation), + } as unknown as ResourceStore; + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + credentials.webID = 'http://test.com/user'; + await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); + }); + + it('errors if authorization is required but the agent is not authorized.', async(): Promise => { + const store = { + getRepresentation: async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation), + } as unknown as ResourceStore; + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(UnauthorizedHttpError); + }); + + it('allows access to specific agents if the acl files identifies them.', async(): Promise => { + credentials.webID = 'http://test.com/user'; + const store = { + getRepresentation: async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webID!)), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation), + } as unknown as ResourceStore; + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); + }); + + it('errors if a specific agents wants to access files not assigned to them.', async(): Promise => { + credentials.webID = 'http://test.com/user'; + const store = { + getRepresentation: async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agent`), nn('http://test.com/differentUser')), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation), + } as unknown as ResourceStore; + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError); + }); + + it('allows access to the acl file if control is allowed.', async(): Promise => { + credentials.webID = 'http://test.com/user'; + identifier.path = 'http://test.com/foo'; + const store = { + getRepresentation: async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webID!)), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)), + ]) } as Representation), + } as unknown as ResourceStore; + identifier = await aclManager.getAcl(identifier); + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); + }); + + it('errors if an agent tries to edit the acl file without control permissions.', async(): Promise => { + credentials.webID = 'http://test.com/user'; + identifier.path = 'http://test.com/foo'; + const store = { + getRepresentation: async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webID!)), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation), + } as unknown as ResourceStore; + identifier = await aclManager.getAcl(identifier); + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError); + }); + + it('passes errors of the ResourceStore along.', async(): Promise => { + const store = { + async getRepresentation(): Promise { + throw new Error('TEST!'); + }, + } as unknown as ResourceStore; + authorizer = new SimpleAclAuthorizer(aclManager, containerManager, store); + await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow('TEST!'); + }); +}); diff --git a/test/unit/authorization/SimpleExtensionAclManager.test.ts b/test/unit/authorization/SimpleExtensionAclManager.test.ts new file mode 100644 index 000000000..7604968e3 --- /dev/null +++ b/test/unit/authorization/SimpleExtensionAclManager.test.ts @@ -0,0 +1,20 @@ +import { SimpleExtensionAclManager } from '../../../src/authorization/SimpleExtensionAclManager'; + +describe('A SimpleExtensionAclManager', (): void => { + const manager = new SimpleExtensionAclManager(); + + it('generates acl URLs by adding an .acl extension.', async(): Promise => { + await expect(manager.getAcl({ path: '/foo/bar' })).resolves.toEqual({ path: '/foo/bar.acl' }); + }); + + it('returns the identifier if the input is already an acl file.', async(): Promise => { + await expect(manager.getAcl({ path: '/foo/bar.acl' })).resolves.toEqual({ path: '/foo/bar.acl' }); + }); + + it('checks if a resource is an acl file by looking at the extension.', async(): Promise => { + await expect(manager.isAcl({ path: '/foo/bar' })).resolves.toBeFalsy(); + await expect(manager.isAcl({ path: '/foo/bar/' })).resolves.toBeFalsy(); + await expect(manager.isAcl({ path: '/foo/bar.acl' })).resolves.toBeTruthy(); + await expect(manager.isAcl({ path: '/foo/bar.acl/' })).resolves.toBeTruthy(); + }); +}); diff --git a/test/unit/ldp/permissions/SimplePermissionsExtractor.test.ts b/test/unit/ldp/permissions/SimplePermissionsExtractor.test.ts index ab68a6f52..637f4a6a9 100644 --- a/test/unit/ldp/permissions/SimplePermissionsExtractor.test.ts +++ b/test/unit/ldp/permissions/SimplePermissionsExtractor.test.ts @@ -13,34 +13,30 @@ describe('A SimplePermissionsExtractor', (): void => { read: true, append: false, write: false, - delete: false, }); }); it('requires write for POST operations.', async(): Promise => { await expect(extractor.handle({ method: 'POST' } as Operation)).resolves.toEqual({ read: false, - append: false, + append: true, write: true, - delete: false, }); }); it('requires write for PUT operations.', async(): Promise => { await expect(extractor.handle({ method: 'PUT' } as Operation)).resolves.toEqual({ read: false, - append: false, + append: true, write: true, - delete: false, }); }); - it('requires delete for DELETE operations.', async(): Promise => { + it('requires write for DELETE operations.', async(): Promise => { await expect(extractor.handle({ method: 'DELETE' } as Operation)).resolves.toEqual({ read: false, - append: false, - write: false, - delete: true, + append: true, + write: true, }); }); }); diff --git a/test/unit/storage/UrlContainerManager.test.ts b/test/unit/storage/UrlContainerManager.test.ts new file mode 100644 index 000000000..a6c786071 --- /dev/null +++ b/test/unit/storage/UrlContainerManager.test.ts @@ -0,0 +1,31 @@ +import { UrlContainerManager } from '../../../src/storage/UrlContainerManager'; + +describe('An UrlContainerManager', (): void => { + it('returns the parent URl for a single call.', async(): Promise => { + const manager = new UrlContainerManager('http://test.com/foo/'); + await expect(manager.getContainer({ path: 'http://test.com/foo/bar' })) + .resolves.toEqual({ path: 'http://test.com/foo/' }); + await expect(manager.getContainer({ path: 'http://test.com/foo/bar/' })) + .resolves.toEqual({ path: 'http://test.com/foo/' }); + }); + + it('errors when getting the container of root.', async(): Promise => { + let manager = new UrlContainerManager('http://test.com/foo/'); + await expect(manager.getContainer({ path: 'http://test.com/foo/' })) + .rejects.toThrow('Root does not have a container.'); + await expect(manager.getContainer({ path: 'http://test.com/foo' })) + .rejects.toThrow('Root does not have a container.'); + + manager = new UrlContainerManager('http://test.com/foo'); + await expect(manager.getContainer({ path: 'http://test.com/foo/' })) + .rejects.toThrow('Root does not have a container.'); + await expect(manager.getContainer({ path: 'http://test.com/foo' })) + .rejects.toThrow('Root does not have a container.'); + }); + + it('errors when the root of an URl is reached that does not match the input root.', async(): Promise => { + const manager = new UrlContainerManager('http://test.com/foo/'); + await expect(manager.getContainer({ path: 'http://test.com/' })) + .rejects.toThrow('URL root reached.'); + }); +});