Repository: solid/community-server
Pull request #50 (2020-08-07 10:01:52)

Add acl support
Only depends on #48 for integration, so first 3 commits here are irrelevant.

Adds support for most acl options, except groups and origin.

I added two new interfaces:
 * `AclManager` which is responsible for determining where the acl files are located.
 * `ContainerManager` which determines what the container is of a resource.

I'm expecting the ContainerManager interface to be extended in the future once we realize some more functions we need related to containers.

When starting the server a new acl file gets created at the root that gives everyone all permissions since it is required to have an acl file at root now.

joachimvh: Note: will have to update since this supports `acl:Delete` which is actually not something that exists.
joachimvh: >  Note: will have to update since this supports `acl:Delete` which is actually not something that exists.

This has been resolved by removing `delete` from the `PermissionSet` interface for consistency.

Patch:
diff --git a/bin/server.ts b/bin/server.ts
index ae4bdc2fb..213e8e0db 100644
--- a/bin/server.ts
+++ b/bin/server.ts
@@ -1,3 +1,6 @@
+#!/usr/bin/env node
+import { DATA_TYPE_BINARY } from '../src/util/ContentTypes';
+import streamifyArray from 'streamify-array';
 import yargs from 'yargs';
 import {
   AcceptPreferenceParser,
@@ -9,14 +12,16 @@ import {
   QuadToTurtleConverter,
   Representation,
   RepresentationConvertingStore,
-  SimpleAuthorizer,
+  SimpleAclAuthorizer,
   SimpleBodyParser,
   SimpleCredentialsExtractor,
   SimpleDeleteOperationHandler,
+  SimpleExtensionAclManager,
   SimpleGetOperationHandler,
   SimplePatchOperationHandler,
   SimplePermissionsExtractor,
   SimplePostOperationHandler,
+  SimplePutOperationHandler,
   SimpleRequestParser,
   SimpleResourceStore,
   SimpleResponseWriter,
@@ -25,6 +30,7 @@ import {
   SimpleTargetExtractor,
   SingleThreadedResourceLocker,
   TurtleToQuadConverter,
+  UrlContainerManager,
 } from '..';
 
 const { argv } = yargs
@@ -36,6 +42,8 @@ const { argv } = yargs
 
 const { port } = argv;
 
+const base = `http://localhost:${port}/`;
+
 // This is instead of the dependency injection that still needs to be added
 const bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
   new SimpleSparqlUpdateBodyParser(),
@@ -49,10 +57,9 @@ const requestParser = new SimpleRequestParser({
 
 const credentialsExtractor = new SimpleCredentialsExtractor();
 const permissionsExtractor = new SimplePermissionsExtractor();
-const authorizer = new SimpleAuthorizer();
 
 // Will have to see how to best handle this
-const store = new SimpleResourceStore(`http://localhost:${port}/`);
+const store = new SimpleResourceStore(base);
 const converter = new CompositeAsyncHandler([
   new TurtleToQuadConverter(),
   new QuadToTurtleConverter(),
@@ -62,11 +69,16 @@ const locker = new SingleThreadedResourceLocker();
 const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
 const patchingStore = new PatchingStore(convertingStore, patcher);
 
+const aclManager = new SimpleExtensionAclManager();
+const containerManager = new UrlContainerManager(base);
+const authorizer = new SimpleAclAuthorizer(aclManager, containerManager, patchingStore);
+
 const operationHandler = new CompositeAsyncHandler([
   new SimpleDeleteOperationHandler(patchingStore),
   new SimpleGetOperationHandler(patchingStore),
   new SimplePatchOperationHandler(patchingStore),
   new SimplePostOperationHandler(patchingStore),
+  new SimplePutOperationHandler(patchingStore),
 ]);
 
 const responseWriter = new SimpleResponseWriter();
@@ -82,6 +94,40 @@ const httpHandler = new AuthenticatedLdpHandler({
 
 const httpServer = new ExpressHttpServer(httpHandler);
 
-httpServer.listen(port);
+// Set up acl so everything can still be done by default
+// Note that this will need to be adapted to go through all the correct channels later on
+const aclSetup = async(): Promise<void> => {
+  const acl = `@prefix   acl:  <http://www.w3.org/ns/auth/acl#>.
+@prefix  foaf:  <http://xmlns.com/foaf/0.1/>.
 
-process.stdout.write(`Running at http://localhost:${port}/\n`);
+<#authorization>
+    a               acl:Authorization;
+    acl:agentClass  foaf:Agent;
+    acl:mode        acl:Read;
+    acl:mode        acl:Write;
+    acl:mode        acl:Append;
+    acl:mode        acl:Delete;
+    acl:mode        acl:Control;
+    acl:accessTo    <${base}>;
+    acl:default     <${base}>.`;
+  await store.setRepresentation(
+    await aclManager.getAcl({ path: base }),
+    {
+      dataType: DATA_TYPE_BINARY,
+      data: streamifyArray([ acl ]),
+      metadata: {
+        raw: [],
+        profiles: [],
+        contentType: 'text/turtle',
+      },
+    },
+  );
+};
+aclSetup().then((): void => {
+  httpServer.listen(port);
+
+  process.stdout.write(`Running at ${base}\n`);
+}).catch((error): void => {
+  process.stderr.write(`${error}\n`);
+  process.exit(1);
+});
diff --git a/index.ts b/index.ts
index 766877812..cbae5a621 100644
--- a/index.ts
+++ b/index.ts
@@ -4,8 +4,11 @@ export * from './src/authentication/CredentialsExtractor';
 export * from './src/authentication/SimpleCredentialsExtractor';
 
 // Authorization
+export * from './src/authorization/AclManager';
 export * from './src/authorization/Authorizer';
+export * from './src/authorization/SimpleAclAuthorizer';
 export * from './src/authorization/SimpleAuthorizer';
+export * from './src/authorization/SimpleExtensionAclManager';
 
 // LDP/HTTP
 export * from './src/ldp/http/AcceptPreferenceParser';
@@ -30,6 +33,7 @@ export * from './src/ldp/operations/SimpleDeleteOperationHandler';
 export * from './src/ldp/operations/SimpleGetOperationHandler';
 export * from './src/ldp/operations/SimplePatchOperationHandler';
 export * from './src/ldp/operations/SimplePostOperationHandler';
+export * from './src/ldp/operations/SimplePutOperationHandler';
 
 // LDP/Permissions
 export * from './src/ldp/permissions/PermissionSet';
@@ -67,6 +71,7 @@ export * from './src/storage/patch/SimpleSparqlUpdatePatchHandler';
 // Storage
 export * from './src/storage/AtomicResourceStore';
 export * from './src/storage/Conditions';
+export * from './src/storage/ContainerManager';
 export * from './src/storage/Lock';
 export * from './src/storage/LockingResourceStore';
 export * from './src/storage/PassthroughStore';
@@ -77,10 +82,13 @@ export * from './src/storage/ResourceMapper';
 export * from './src/storage/ResourceStore';
 export * from './src/storage/SingleThreadedResourceLocker';
 export * from './src/storage/SimpleResourceStore';
+export * from './src/storage/UrlContainerManager';
 
 // Util/Errors
+export * from './src/util/errors/ForbiddenHttpError';
 export * from './src/util/errors/HttpError';
 export * from './src/util/errors/NotFoundHttpError';
+export * from './src/util/errors/UnauthorizedHttpError';
 export * from './src/util/errors/UnsupportedHttpError';
 export * from './src/util/errors/UnsupportedMediaTypeHttpError';
 
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<ResourceIdentifier>;
+
+  /**
+   * 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<boolean>;
+}
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<void> {
+    // 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<void> {
+    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<Store> {
+    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<Store> {
+    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<ResourceIdentifier> {
+    return await this.isAcl(id) ? id : { path: `${id.path}.acl` };
+  }
+
+  public async isAcl(id: ResourceIdentifier): Promise<boolean> {
+    return /\.acl\/?/u.test(id.path);
+  }
+}
diff --git a/src/ldp/operations/SimplePutOperationHandler.ts b/src/ldp/operations/SimplePutOperationHandler.ts
new file mode 100644
index 000000000..3dc9bceb5
--- /dev/null
+++ b/src/ldp/operations/SimplePutOperationHandler.ts
@@ -0,0 +1,32 @@
+import { Operation } from './Operation';
+import { OperationHandler } from './OperationHandler';
+import { ResourceStore } from '../../storage/ResourceStore';
+import { ResponseDescription } from './ResponseDescription';
+import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
+
+/**
+ * Handles PUT {@link Operation}s.
+ * Calls the setRepresentation function from a {@link ResourceStore}.
+ */
+export class SimplePutOperationHandler extends OperationHandler {
+  private readonly store: ResourceStore;
+
+  public constructor(store: ResourceStore) {
+    super();
+    this.store = store;
+  }
+
+  public async canHandle(input: Operation): Promise<void> {
+    if (input.method !== 'PUT') {
+      throw new UnsupportedHttpError('This handler only supports PUT operations.');
+    }
+    if (typeof input.body !== 'object') {
+      throw new UnsupportedHttpError('PUT operations require a body.');
+    }
+  }
+
+  public async handle(input: Operation): Promise<ResponseDescription> {
+    await this.store.setRepresentation(input.target, input.body!);
+    return { identifier: input.target };
+  }
+}
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<PermissionSet> {
-    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<ResourceIdentifier>;
+}
diff --git a/src/storage/SimpleResourceStore.ts b/src/storage/SimpleResourceStore.ts
index 57f547dd4..5ba58c7c6 100644
--- a/src/storage/SimpleResourceStore.ts
+++ b/src/storage/SimpleResourceStore.ts
@@ -42,10 +42,12 @@ export class SimpleResourceStore implements ResourceStore {
    */
   public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
     const containerPath = this.parseIdentifier(container);
-    const newPath = `${ensureTrailingSlash(containerPath)}${this.index}`;
+    this.checkPath(containerPath);
+    const newID = { path: `${ensureTrailingSlash(container.path)}${this.index}` };
+    const newPath = this.parseIdentifier(newID);
     this.index += 1;
     this.store[newPath] = await this.copyRepresentation(representation);
-    return { path: `${this.base}${newPath}` };
+    return newID;
   }
 
   /**
@@ -54,6 +56,7 @@ export class SimpleResourceStore implements ResourceStore {
    */
   public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
     const path = this.parseIdentifier(identifier);
+    this.checkPath(path);
     // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
     delete this.store[path];
   }
@@ -68,6 +71,7 @@ export class SimpleResourceStore implements ResourceStore {
    */
   public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
     const path = this.parseIdentifier(identifier);
+    this.checkPath(path);
     return this.generateRepresentation(path);
   }
 
@@ -79,7 +83,7 @@ export class SimpleResourceStore implements ResourceStore {
   }
 
   /**
-   * Replaces the stored Representation with the new one for the given identifier.
+   * Puts the given data in the given location.
    * @param identifier - Identifier to replace.
    * @param representation - New Representation.
    */
@@ -89,22 +93,35 @@ export class SimpleResourceStore implements ResourceStore {
   }
 
   /**
-   * Strips the base from the identifier and checks if it is in the store.
+   * Strips the base from the identifier and checks if it is valid.
    * @param identifier - Incoming identifier.
    *
    * @throws {@link NotFoundHttpError}
-   * If the identifier is not in the store.
+   * If the identifier doesn't start with the base ID.
    *
    * @returns A string representing the relative path.
    */
   private parseIdentifier(identifier: ResourceIdentifier): string {
     const path = identifier.path.slice(this.base.length);
-    if (!this.store[path] || !identifier.path.startsWith(this.base)) {
+    if (!identifier.path.startsWith(this.base)) {
       throw new NotFoundHttpError();
     }
     return path;
   }
 
+  /**
+   * Checks if the relative path is in the store.
+   * @param identifier - Incoming identifier.
+   *
+   * @throws {@link NotFoundHttpError}
+   * If the path is not in the store.
+   */
+  private checkPath(path: string): void {
+    if (!this.store[path]) {
+      throw new NotFoundHttpError();
+    }
+  }
+
   /**
    * Copies the Representation by draining the original data stream and creating a new one.
    *
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<ResourceIdentifier> {
+    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);
+  }
+}


Test patch:
diff --git a/test/index.ts b/test/index.ts
deleted file mode 100644
index dca32b868..000000000
--- a/test/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-describe('A basic test', (): void => {
-  it('to have something pass.', async(): Promise<void> => {
-    expect(true).toBeTruthy();
-  });
-});
diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts
index b5018b78d..04e527def 100644
--- a/test/integration/AuthenticatedLdpHandler.test.ts
+++ b/test/integration/AuthenticatedLdpHandler.test.ts
@@ -1,11 +1,10 @@
 import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
 import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler';
 import { BodyParser } from '../../src/ldp/http/BodyParser';
+import { call } from '../util/Util';
 import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler';
-import { EventEmitter } from 'events';
-import { HttpHandler } from '../../src/server/HttpHandler';
 import { HttpRequest } from '../../src/server/HttpRequest';
-import { IncomingHttpHeaders } from 'http';
+import { MockResponse } from 'node-mocks-http';
 import { Operation } from '../../src/ldp/operations/Operation';
 import { Parser } from 'n3';
 import { PatchingStore } from '../../src/storage/PatchingStore';
@@ -28,35 +27,11 @@ import { SimpleSparqlUpdateBodyParser } from '../../src/ldp/http/SimpleSparqlUpd
 import { SimpleSparqlUpdatePatchHandler } from '../../src/storage/patch/SimpleSparqlUpdatePatchHandler';
 import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
 import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker';
-import streamifyArray from 'streamify-array';
 import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter';
-import { createResponse, MockResponse } from 'node-mocks-http';
 import { namedNode, quad } from '@rdfjs/data-model';
 import * as url from 'url';
 
-const call = async(handler: HttpHandler, requestUrl: url.URL, method: string,
-  headers: IncomingHttpHeaders, data: string[]): Promise<MockResponse<any>> => {
-  const request = streamifyArray(data) as HttpRequest;
-  request.url = requestUrl.pathname;
-  request.method = method;
-  request.headers = headers;
-  request.headers.host = requestUrl.host;
-  const response: MockResponse<any> = createResponse({ eventEmitter: EventEmitter });
-
-  const endPromise = new Promise((resolve): void => {
-    response.on('end', (): void => {
-      expect(response._isEndCalled()).toBeTruthy();
-      resolve();
-    });
-  });
-
-  await handler.handleSafe({ request, response });
-  await endPromise;
-
-  return response;
-};
-
-describe('An AuthenticatedLdpHandler', (): void => {
+describe('An integrated AuthenticatedLdpHandler', (): void => {
   describe('with simple handlers', (): void => {
     const requestParser = new SimpleRequestParser({
       targetExtractor: new SimpleTargetExtractor(),
@@ -88,7 +63,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
 
     it('can add, read and delete data based on incoming requests.', async(): Promise<void> => {
       // POST
-      let requestUrl = new url.URL('http://test.com/');
+      let requestUrl = new URL('http://test.com/');
       let response: MockResponse<any> = await call(
         handler,
         requestUrl,
@@ -102,7 +77,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
       expect(id).toContain(url.format(requestUrl));
 
       // GET
-      requestUrl = new url.URL(id);
+      requestUrl = new URL(id);
       response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
       expect(response.statusCode).toBe(200);
       expect(response._getData()).toContain('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
@@ -166,7 +141,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
 
     it('can handle simple SPARQL updates.', async(): Promise<void> => {
       // POST
-      let requestUrl = new url.URL('http://test.com/');
+      let requestUrl = new URL('http://test.com/');
       let response: MockResponse<any> = await call(
         handler,
         requestUrl,
@@ -181,7 +156,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
       expect(id).toContain(url.format(requestUrl));
 
       // PATCH
-      requestUrl = new url.URL(id);
+      requestUrl = new URL(id);
       response = await call(
         handler,
         requestUrl,
@@ -196,7 +171,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
       expect(response._getHeaders().location).toBe(id);
 
       // GET
-      requestUrl = new url.URL(id);
+      requestUrl = new URL(id);
       response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
       expect(response.statusCode).toBe(200);
       expect(response._getData()).toContain('<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.');
diff --git a/test/integration/Authorization.test.ts b/test/integration/Authorization.test.ts
new file mode 100644
index 000000000..8cfec2e89
--- /dev/null
+++ b/test/integration/Authorization.test.ts
@@ -0,0 +1,181 @@
+import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
+import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler';
+import { BodyParser } from '../../src/ldp/http/BodyParser';
+import { call } from '../util/Util';
+import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler';
+import { DATA_TYPE_BINARY } from '../../src/util/ContentTypes';
+import { MockResponse } from 'node-mocks-http';
+import { Operation } from '../../src/ldp/operations/Operation';
+import { PermissionSet } from '../../src/ldp/permissions/PermissionSet';
+import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtleConverter';
+import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore';
+import { ResourceStore } from '../../src/storage/ResourceStore';
+import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription';
+import { SimpleAclAuthorizer } from '../../src/authorization/SimpleAclAuthorizer';
+import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
+import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor';
+import { SimpleDeleteOperationHandler } from '../../src/ldp/operations/SimpleDeleteOperationHandler';
+import { SimpleExtensionAclManager } from '../../src/authorization/SimpleExtensionAclManager';
+import { SimpleGetOperationHandler } from '../../src/ldp/operations/SimpleGetOperationHandler';
+import { SimplePermissionsExtractor } from '../../src/ldp/permissions/SimplePermissionsExtractor';
+import { SimplePostOperationHandler } from '../../src/ldp/operations/SimplePostOperationHandler';
+import { SimplePutOperationHandler } from '../../src/ldp/operations/SimplePutOperationHandler';
+import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser';
+import { SimpleResourceStore } from '../../src/storage/SimpleResourceStore';
+import { SimpleResponseWriter } from '../../src/ldp/http/SimpleResponseWriter';
+import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
+import streamifyArray from 'streamify-array';
+import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter';
+import { UrlContainerManager } from '../../src/storage/UrlContainerManager';
+
+const setAcl = async(store: ResourceStore, id: string, permissions: PermissionSet, control: boolean,
+  access: boolean, def: boolean, agent?: string, agentClass?: 'agent' | 'authenticated'): Promise<void> => {
+  const acl: string[] = [
+    '@prefix   acl:  <http://www.w3.org/ns/auth/acl#>.\n',
+    '@prefix  foaf:  <http://xmlns.com/foaf/0.1/>.\n',
+    '<http://test.com/#auth> a acl:Authorization',
+  ];
+
+  for (const perm of [ 'Read', 'Append', 'Write', 'Delete' ]) {
+    if (permissions[perm.toLowerCase() as keyof PermissionSet]) {
+      acl.push(`;\n acl:mode acl:${perm}`);
+    }
+  }
+  if (control) {
+    acl.push(';\n acl:mode acl:Control');
+  }
+  if (access) {
+    acl.push(`;\n acl:accessTo <${id}>`);
+  }
+  if (def) {
+    acl.push(`;\n acl:default <${id}>`);
+  }
+  if (agent) {
+    acl.push(`;\n acl:agent <${agent}>`);
+  }
+  if (agentClass) {
+    acl.push(`;\n acl:agentClass ${agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent'}`);
+  }
+
+  acl.push('.');
+
+  const representation = {
+    data: streamifyArray(acl),
+    dataType: DATA_TYPE_BINARY,
+    metadata: {
+      raw: [],
+      profiles: [],
+      contentType: 'text/turtle',
+    },
+  };
+
+  return store.setRepresentation({ path: `${id}.acl` }, representation);
+};
+
+describe('A server with authorization', (): void => {
+  const bodyParser: BodyParser = new SimpleBodyParser();
+  const requestParser = new SimpleRequestParser({
+    targetExtractor: new SimpleTargetExtractor(),
+    preferenceParser: new AcceptPreferenceParser(),
+    bodyParser,
+  });
+
+  const store = new SimpleResourceStore('http://test.com/');
+  const converter = new CompositeAsyncHandler([
+    new QuadToTurtleConverter(),
+    new TurtleToQuadConverter(),
+  ]);
+  const convertingStore = new RepresentationConvertingStore(store, converter);
+
+  const credentialsExtractor = new SimpleCredentialsExtractor();
+  const permissionsExtractor = new SimplePermissionsExtractor();
+  const authorizer = new SimpleAclAuthorizer(
+    new SimpleExtensionAclManager(),
+    new UrlContainerManager('http://test.com/'),
+    convertingStore,
+  );
+
+  const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
+    new SimpleGetOperationHandler(convertingStore),
+    new SimplePostOperationHandler(convertingStore),
+    new SimpleDeleteOperationHandler(convertingStore),
+    new SimplePutOperationHandler(convertingStore),
+  ]);
+
+  const responseWriter = new SimpleResponseWriter();
+
+  const handler = new AuthenticatedLdpHandler({
+    requestParser,
+    credentialsExtractor,
+    permissionsExtractor,
+    authorizer,
+    operationHandler,
+    responseWriter,
+  });
+
+  it('can create new entries.', async(): Promise<void> => {
+    await setAcl(convertingStore,
+      'http://test.com/',
+      { read: true, write: true, append: true },
+      true,
+      true,
+      true,
+      undefined,
+      'agent');
+
+    // POST
+    let requestUrl = new URL('http://test.com/');
+    let response: MockResponse<any> = await call(
+      handler,
+      requestUrl,
+      'POST',
+      { 'content-type': 'text/turtle' },
+      [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
+    );
+    expect(response.statusCode).toBe(200);
+
+    // PUT
+    requestUrl = new URL('http://test.com/foo/bar');
+    response = await call(
+      handler,
+      requestUrl,
+      'PUT',
+      { 'content-type': 'text/turtle' },
+      [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
+    );
+    expect(response.statusCode).toBe(200);
+  });
+
+  it('can not create new entries if not allowed.', async(): Promise<void> => {
+    await setAcl(convertingStore,
+      'http://test.com/',
+      { read: true, write: true, append: true },
+      true,
+      true,
+      true,
+      undefined,
+      'authenticated');
+
+    // POST
+    let requestUrl = new URL('http://test.com/');
+    let response: MockResponse<any> = await call(
+      handler,
+      requestUrl,
+      'POST',
+      { 'content-type': 'text/turtle' },
+      [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
+    );
+    expect(response.statusCode).toBe(401);
+
+    // PUT
+    requestUrl = new URL('http://test.com/foo/bar');
+    response = await call(
+      handler,
+      requestUrl,
+      'PUT',
+      { 'content-type': 'text/turtle' },
+      [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
+    );
+    expect(response.statusCode).toBe(401);
+  });
+});
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<ResourceIdentifier> =>
+      id.path.endsWith('.acl') ? id : { path: `${id.path}.acl` },
+    isAcl: async(id: ResourceIdentifier): Promise<boolean> => id.path.endsWith('.acl'),
+  };
+  const containerManager: ContainerManager = {
+    getContainer: async(id: ResourceIdentifier): Promise<ResourceIdentifier> =>
+      ({ path: new URL('..', id.path).toString() }),
+  };
+  let permissions: PermissionSet;
+  let credentials: Credentials;
+  let identifier: ResourceIdentifier;
+
+  beforeEach(async(): Promise<void> => {
+    permissions = {
+      read: true,
+      append: false,
+      write: false,
+    };
+    credentials = {};
+    identifier = { path: 'http://test.com/foo' };
+  });
+
+  it('handles all inputs.', async(): Promise<void> => {
+    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<void> => {
+    const store = {
+      getRepresentation: async(): Promise<Representation> => ({ 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<void> => {
+    const store = {
+      async getRepresentation(id: ResourceIdentifier): Promise<Representation> {
+        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<void> => {
+    const store = {
+      getRepresentation: async(): Promise<Representation> => ({ 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<void> => {
+    const store = {
+      getRepresentation: async(): Promise<Representation> => ({ 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<void> => {
+    credentials.webID = 'http://test.com/user';
+    const store = {
+      getRepresentation: async(): Promise<Representation> => ({ 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<void> => {
+    credentials.webID = 'http://test.com/user';
+    const store = {
+      getRepresentation: async(): Promise<Representation> => ({ 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<void> => {
+    credentials.webID = 'http://test.com/user';
+    identifier.path = 'http://test.com/foo';
+    const store = {
+      getRepresentation: async(): Promise<Representation> => ({ 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<void> => {
+    credentials.webID = 'http://test.com/user';
+    identifier.path = 'http://test.com/foo';
+    const store = {
+      getRepresentation: async(): Promise<Representation> => ({ 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<void> => {
+    const store = {
+      async getRepresentation(): Promise<Representation> {
+        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<void> => {
+    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<void> => {
+    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<void> => {
+    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/operations/SimpleDeleteOperationHandler.test.ts b/test/unit/ldp/operations/SimpleDeleteOperationHandler.test.ts
index 8c359289e..2fadd7160 100644
--- a/test/unit/ldp/operations/SimpleDeleteOperationHandler.test.ts
+++ b/test/unit/ldp/operations/SimpleDeleteOperationHandler.test.ts
@@ -11,7 +11,7 @@ describe('A SimpleDeleteOperationHandler', (): void => {
     store.deleteResource = jest.fn(async(): Promise<void> => {});
   });
 
-  it('only supports GET operations.', async(): Promise<void> => {
+  it('only supports DELETE operations.', async(): Promise<void> => {
     await expect(handler.canHandle({ method: 'DELETE' } as Operation)).resolves.toBeUndefined();
     await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError);
   });
diff --git a/test/unit/ldp/operations/SimplePutOperationHandler.test.ts b/test/unit/ldp/operations/SimplePutOperationHandler.test.ts
new file mode 100644
index 000000000..2ddbf59a5
--- /dev/null
+++ b/test/unit/ldp/operations/SimplePutOperationHandler.test.ts
@@ -0,0 +1,26 @@
+import { Operation } from '../../../../src/ldp/operations/Operation';
+import { ResourceStore } from '../../../../src/storage/ResourceStore';
+import { SimplePutOperationHandler } from '../../../../src/ldp/operations/SimplePutOperationHandler';
+import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
+
+describe('A SimplePutOperationHandler', (): void => {
+  const store = {} as unknown as ResourceStore;
+  const handler = new SimplePutOperationHandler(store);
+  beforeEach(async(): Promise<void> => {
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
+    store.setRepresentation = jest.fn(async(): Promise<void> => {});
+  });
+
+  it('only supports PUT operations with a body.', async(): Promise<void> => {
+    await expect(handler.canHandle({ method: 'PUT' } as Operation)).rejects.toThrow(UnsupportedHttpError);
+    await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError);
+    await expect(handler.canHandle({ method: 'PUT', body: { dataType: 'test' }} as Operation)).resolves.toBeUndefined();
+  });
+
+  it('sets the representation in the store and returns its identifier.', async(): Promise<void> => {
+    await expect(handler.handle({ target: { path: 'url' }, body: { dataType: 'test' }} as Operation))
+      .resolves.toEqual({ identifier: { path: 'url' }});
+    expect(store.setRepresentation).toHaveBeenCalledTimes(1);
+    expect(store.setRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, { dataType: 'test' });
+  });
+});
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<void> => {
     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<void> => {
     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<void> => {
+  it('requires write for DELETE operations.', async(): Promise<void> => {
     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/server/ExpressHttpServer.test.ts b/test/unit/server/ExpressHttpServer.test.ts
index 3dbfda19b..d84f7eccc 100644
--- a/test/unit/server/ExpressHttpServer.test.ts
+++ b/test/unit/server/ExpressHttpServer.test.ts
@@ -81,6 +81,10 @@ describe('ExpressHttpServer', (): void => {
     handler.handle = async(): Promise<void> => {
       throw new Error('dummyError');
     };
+
+    // Prevent test from writing to stderr
+    jest.spyOn(process.stderr, 'write').mockImplementation((): boolean => true);
+
     const res = await request(server).get('/').expect(500);
     expect(res.text).toContain('dummyError');
   });
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<void> => {
+    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<void> => {
+    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<void> => {
+    const manager = new UrlContainerManager('http://test.com/foo/');
+    await expect(manager.getContainer({ path: 'http://test.com/' }))
+      .rejects.toThrow('URL root reached.');
+  });
+});
diff --git a/test/util/Util.ts b/test/util/Util.ts
new file mode 100644
index 000000000..a3ad93d1f
--- /dev/null
+++ b/test/util/Util.ts
@@ -0,0 +1,28 @@
+import { EventEmitter } from 'events';
+import { HttpHandler } from '../../src/server/HttpHandler';
+import { HttpRequest } from '../../src/server/HttpRequest';
+import { IncomingHttpHeaders } from 'http';
+import streamifyArray from 'streamify-array';
+import { createResponse, MockResponse } from 'node-mocks-http';
+
+export const call = async(handler: HttpHandler, requestUrl: URL, method: string,
+  headers: IncomingHttpHeaders, data: string[]): Promise<MockResponse<any>> => {
+  const request = streamifyArray(data) as HttpRequest;
+  request.url = requestUrl.pathname;
+  request.method = method;
+  request.headers = headers;
+  request.headers.host = requestUrl.host;
+  const response: MockResponse<any> = createResponse({ eventEmitter: EventEmitter });
+
+  const endPromise = new Promise((resolve): void => {
+    response.on('end', (): void => {
+      expect(response._isEndCalled()).toBeTruthy();
+      resolve();
+    });
+  });
+
+  await handler.handleSafe({ request, response });
+  await endPromise;
+
+  return response;
+};