Repository: solid/community-server
Pull request #414 (2020-12-09 15:14:20)

Re-introduce ContainerManager
Based on https://github.com/solid/community-server/issues/394#issuecomment-739805093

In preparation for some other issues where `isRootContainer` is needed.

I added the `toRelative` and `fromRelative` functions initially, but there are some problems with that interface so will have to rethink later. Pod provisioning would have same issues (which can be fixed I think, but I would prefer looking into that later).

RubenVerborgh: > I added the `toRelative` and `fromRelative` functions initially, but there are some problems with that interface so will have to rethink later.

Curious to hear about those.

I think `toRelative` for `https://alice.solid.example/foo/bar` could be `/alice/foo/bar` for multi-host setup.
joachimvh: > Curious to hear about those.
> 
> I think `toRelative` for `https://alice.solid.example/foo/bar` could be `/alice/foo/bar` for multi-host setup.

The main issue was with the pod provisioning. Since we would use this instead of a base url for the mappers, this also requires the mapper factory to take this as a parameter instead. Question then is what the `TemplatedResourcesGenerator` would do since it's using a `FileIdentifierMapperFactory`. 

Using the same implementation as it does now but with a fixed IdentifierStrategy would not work: the main reason for the factory was having a variable base url based on the generated container identifier. Internally the mapper would call `toRelative` which would return `/.acl` (while we would want `/alice/.acl` if the pod is named `alice`), and `fromRelative` would not know that this would need to be mapped to a new root.

One solution would be to also have an `IdentifierStrategyFactory` which would generate a new strategy with the pod url as single fixed root. But that is getting very messy since that factory would have to feed into the mapper factory.

Another solution, which is what I was thinking of going for, is using a fixed mapper and strategy. As mentioned above, this would generate the same identifiers for the resources independent of the pod which they were for (so `http://test.com/.acl` instead of `http://alice.test.com/.acl`). But what we could do is call `toRelative` on those identifiers, prepend them with `/alice` and call `fromRelative` again to now get the correct identifier relative to the root of the pod. The main disadvantage of this method is that this does require the resources generator to have knowledge of how relative paths are supposed to look like. Semantically it's also less nice that we can't use a mapper to map directly to the space we want it to map to.

Patch:
diff --git a/config/presets/acl.json b/config/presets/acl.json
index 75705289c..6af9d0983 100644
--- a/config/presets/acl.json
+++ b/config/presets/acl.json
@@ -13,6 +13,9 @@
       },
       "WebAclAuthorizer:_resourceStore": {
         "@id": "urn:solid-server:default:ResourceStore"
+      },
+      "WebAclAuthorizer:_identifierStrategy": {
+        "@id": "urn:solid-server:default:IdentifierStrategy"
       }
     }
   ]
diff --git a/config/presets/storage-wrapper.json b/config/presets/storage-wrapper.json
index 2e8e0ae89..e76a33e5a 100644
--- a/config/presets/storage-wrapper.json
+++ b/config/presets/storage-wrapper.json
@@ -6,6 +6,17 @@
       "@type": "MonitoringStore",
       "MonitoringStore:_source": {
         "@id": "urn:solid-server:default:ResourceStore_Patching"
+      },
+      "MonitoringStore:_identifierStrategy": {
+        "@id": "urn:solid-server:default:IdentifierStrategy"
+      }
+    },
+
+    {
+      "@id": "urn:solid-server:default:IdentifierStrategy",
+      "@type": "SingleRootIdentifierStrategy",
+      "SingleRootIdentifierStrategy:_baseUrl": {
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
 
diff --git a/config/presets/storage/backend/storage-filesystem.json b/config/presets/storage/backend/storage-filesystem.json
index 9e8c7bbb7..5b09bf56d 100644
--- a/config/presets/storage/backend/storage-filesystem.json
+++ b/config/presets/storage/backend/storage-filesystem.json
@@ -26,8 +26,8 @@
       "DataAccessorBasedStore:_accessor": {
         "@id": "urn:solid-server:default:FileDataAccessor"
       },
-      "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:baseUrl"
+      "DataAccessorBasedStore:_identifierStrategy": {
+        "@id": "urn:solid-server:default:IdentifierStrategy"
       }
     }
   ]
diff --git a/config/presets/storage/backend/storage-memory.json b/config/presets/storage/backend/storage-memory.json
index 2ae772557..ef8d090eb 100644
--- a/config/presets/storage/backend/storage-memory.json
+++ b/config/presets/storage/backend/storage-memory.json
@@ -14,8 +14,8 @@
       "DataAccessorBasedStore:_accessor": {
         "@id": "urn:solid-server:default:MemoryDataAccessor"
       },
-      "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:baseUrl"
+      "DataAccessorBasedStore:_identifierStrategy": {
+        "@id": "urn:solid-server:default:IdentifierStrategy"
       }
     }
   ]
diff --git a/config/presets/storage/backend/storage-sparql-endpoint.json b/config/presets/storage/backend/storage-sparql-endpoint.json
index 0aa123bce..b547f31af 100644
--- a/config/presets/storage/backend/storage-sparql-endpoint.json
+++ b/config/presets/storage/backend/storage-sparql-endpoint.json
@@ -7,8 +7,8 @@
       "SparqlDataAccessor:_endpoint": {
         "@id": "urn:solid-server:default:variable:sparqlEndpoint"
       },
-      "SparqlDataAccessor:_base": {
-        "@id": "urn:solid-server:default:variable:baseUrl"
+      "SparqlDataAccessor:_identifierStrategy": {
+        "@id": "urn:solid-server:default:IdentifierStrategy"
       }
     },
 
@@ -18,8 +18,8 @@
       "DataAccessorBasedStore:_accessor": {
         "@id": "urn:solid-server:default:SparqlDataAccessor"
       },
-      "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:baseUrl"
+      "DataAccessorBasedStore:_identifierStrategy": {
+        "@id": "urn:solid-server:default:IdentifierStrategy"
       }
     },
 
diff --git a/src/authorization/WebAclAuthorizer.ts b/src/authorization/WebAclAuthorizer.ts
index 62da50f13..0e4b2b102 100644
--- a/src/authorization/WebAclAuthorizer.ts
+++ b/src/authorization/WebAclAuthorizer.ts
@@ -8,9 +8,10 @@ import { getLoggerFor } from '../logging/LogUtil';
 import type { ResourceStore } from '../storage/ResourceStore';
 import { INTERNAL_QUADS } from '../util/ContentTypes';
 import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
+import { InternalServerError } from '../util/errors/InternalServerError';
 import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
-import { getParentContainer } from '../util/PathUtil';
+import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
 import { ACL, FOAF } from '../util/UriConstants';
 import type { AclManager } from './AclManager';
 import type { AuthorizerArgs } from './Authorizer';
@@ -26,11 +27,13 @@ export class WebAclAuthorizer extends Authorizer {
 
   private readonly aclManager: AclManager;
   private readonly resourceStore: ResourceStore;
+  private readonly identifierStrategy: IdentifierStrategy;
 
-  public constructor(aclManager: AclManager, resourceStore: ResourceStore) {
+  public constructor(aclManager: AclManager, resourceStore: ResourceStore, identifierStrategy: IdentifierStrategy) {
     super();
     this.aclManager = aclManager;
     this.resourceStore = resourceStore;
+    this.identifierStrategy = identifierStrategy;
   }
 
   /**
@@ -134,7 +137,11 @@ export class WebAclAuthorizer extends Authorizer {
     }
 
     this.logger.debug(`Traversing to the parent of ${id.path}`);
-    const parent = getParentContainer(id);
+    if (this.identifierStrategy.isRootContainer(id)) {
+      this.logger.error(`No ACL document found for root container ${id.path}`);
+      throw new InternalServerError('No ACL document found for root container');
+    }
+    const parent = this.identifierStrategy.getParentContainer(id);
     return this.getAclRecursive(parent, true);
   }
 
diff --git a/src/index.ts b/src/index.ts
index cdc5ff814..38d8bfd36 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -172,6 +172,10 @@ export * from './util/errors/SystemError';
 export * from './util/errors/UnauthorizedHttpError';
 export * from './util/errors/UnsupportedMediaTypeHttpError';
 
+// Util/Identifiers
+export * from './util/identifiers/IdentifierStrategy';
+export * from './util/identifiers/SingleRootIdentifierStrategy';
+
 // Util/Locking
 export * from './util/locking/ExpiringLock';
 export * from './util/locking/ExpiringResourceLocker';
diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts
index 48611d4ce..e71b7544a 100644
--- a/src/storage/DataAccessorBasedStore.ts
+++ b/src/storage/DataAccessorBasedStore.ts
@@ -11,9 +11,9 @@ import { ConflictHttpError } from '../util/errors/ConflictHttpError';
 import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpError';
 import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
+import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
 import {
   ensureTrailingSlash,
-  getParentContainer,
   isContainerIdentifier,
   isContainerPath,
   trimTrailingSlashes,
@@ -50,11 +50,11 @@ import type { ResourceStore } from './ResourceStore';
  */
 export class DataAccessorBasedStore implements ResourceStore {
   private readonly accessor: DataAccessor;
-  private readonly base: string;
+  private readonly identifierStrategy: IdentifierStrategy;
 
-  public constructor(accessor: DataAccessor, base: string) {
+  public constructor(accessor: DataAccessor, identifierStrategy: IdentifierStrategy) {
     this.accessor = accessor;
-    this.base = ensureTrailingSlash(base);
+    this.identifierStrategy = identifierStrategy;
   }
 
   public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
@@ -147,7 +147,7 @@ export class DataAccessorBasedStore implements ResourceStore {
 
   public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
     this.validateIdentifier(identifier);
-    if (ensureTrailingSlash(identifier.path) === this.base) {
+    if (this.identifierStrategy.isRootContainer(identifier)) {
       throw new MethodNotAllowedHttpError('Cannot delete root container.');
     }
     const metadata = await this.accessor.getMetadata(identifier);
@@ -161,7 +161,7 @@ export class DataAccessorBasedStore implements ResourceStore {
    * Verify if the given identifier matches the stored base.
    */
   protected validateIdentifier(identifier: ResourceIdentifier): void {
-    if (!identifier.path.startsWith(this.base)) {
+    if (!this.identifierStrategy.supportsIdentifier(identifier)) {
       throw new NotFoundHttpError();
     }
   }
@@ -219,7 +219,7 @@ export class DataAccessorBasedStore implements ResourceStore {
     }
 
     if (createContainers) {
-      await this.createRecursiveContainers(getParentContainer(identifier));
+      await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(identifier));
     }
 
     // Make sure the metadata has the correct identifier and correct type quads
@@ -354,7 +354,7 @@ export class DataAccessorBasedStore implements ResourceStore {
     } catch (error: unknown) {
       if (error instanceof NotFoundHttpError) {
         // Make sure the parent exists first
-        await this.createRecursiveContainers(getParentContainer(container));
+        await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container));
         await this.writeData(container, this.getEmptyContainerRepresentation(container), true);
       } else {
         throw error;
diff --git a/src/storage/MonitoringStore.ts b/src/storage/MonitoringStore.ts
index 69189a205..3e2074ece 100644
--- a/src/storage/MonitoringStore.ts
+++ b/src/storage/MonitoringStore.ts
@@ -3,7 +3,7 @@ import type { Patch } from '../ldp/http/Patch';
 import type { Representation } from '../ldp/representation/Representation';
 import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
 import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
-import { getParentContainer } from '../util/PathUtil';
+import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
 import type { Conditions } from './Conditions';
 import type { ResourceStore } from './ResourceStore';
 
@@ -14,10 +14,12 @@ import type { ResourceStore } from './ResourceStore';
 export class MonitoringStore<T extends ResourceStore = ResourceStore>
   extends EventEmitter implements ResourceStore {
   private readonly source: T;
+  private readonly identifierStrategy: IdentifierStrategy;
 
-  public constructor(source: T) {
+  public constructor(source: T, identifierStrategy: IdentifierStrategy) {
     super();
     this.source = source;
+    this.identifierStrategy = identifierStrategy;
   }
 
   public async addResource(container: ResourceIdentifier, representation: Representation,
@@ -35,11 +37,9 @@ export class MonitoringStore<T extends ResourceStore = ResourceStore>
     await this.source.deleteResource(identifier, conditions);
 
     // Both the container contents and the resource itself have changed
-    try {
-      const container = getParentContainer(identifier);
+    if (!this.identifierStrategy.isRootContainer(identifier)) {
+      const container = this.identifierStrategy.getParentContainer(identifier);
       this.emit('changed', container);
-    } catch {
-      // Parent container not found
     }
     this.emit('changed', identifier);
   }
diff --git a/src/storage/accessors/SparqlDataAccessor.ts b/src/storage/accessors/SparqlDataAccessor.ts
index 1819e4190..785d14dae 100644
--- a/src/storage/accessors/SparqlDataAccessor.ts
+++ b/src/storage/accessors/SparqlDataAccessor.ts
@@ -25,7 +25,8 @@ import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpErr
 import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
 import { guardStream } from '../../util/GuardedStream';
 import type { Guarded } from '../../util/GuardedStream';
-import { ensureTrailingSlash, getParentContainer, isContainerIdentifier } from '../../util/PathUtil';
+import type { IdentifierStrategy } from '../../util/identifiers/IdentifierStrategy';
+import { isContainerIdentifier } from '../../util/PathUtil';
 import { generateResourceQuads } from '../../util/ResourceUtil';
 import { CONTENT_TYPE, LDP } from '../../util/UriConstants';
 import { toNamedNode } from '../../util/UriUtil';
@@ -48,13 +49,13 @@ const { defaultGraph, namedNode, quad, variable } = DataFactory;
 export class SparqlDataAccessor implements DataAccessor {
   protected readonly logger = getLoggerFor(this);
   private readonly endpoint: string;
-  private readonly base: string;
+  private readonly identifierStrategy: IdentifierStrategy;
   private readonly fetcher: SparqlEndpointFetcher;
   private readonly generator: SparqlGenerator;
 
-  public constructor(endpoint: string, base: string) {
+  public constructor(endpoint: string, identifierStrategy: IdentifierStrategy) {
     this.endpoint = endpoint;
-    this.base = ensureTrailingSlash(base);
+    this.identifierStrategy = identifierStrategy;
     this.fetcher = new SparqlEndpointFetcher();
     this.generator = new Generator();
   }
@@ -90,7 +91,7 @@ export class SparqlDataAccessor implements DataAccessor {
     const quads = await arrayifyStream(stream);
 
     // Root container will not have metadata if there are no containment triples
-    if (quads.length === 0 && identifier.path !== this.base) {
+    if (quads.length === 0 && !this.identifierStrategy.isRootContainer(identifier)) {
       throw new NotFoundHttpError();
     }
 
@@ -100,7 +101,7 @@ export class SparqlDataAccessor implements DataAccessor {
     }
 
     // Need to generate type metadata for the root container since it's not stored
-    if (identifier.path === this.base) {
+    if (this.identifierStrategy.isRootContainer(identifier)) {
       metadata.addQuads(generateResourceQuads(name, true));
     }
 
@@ -149,7 +150,7 @@ export class SparqlDataAccessor implements DataAccessor {
    * Helper function to get named nodes corresponding to the identifier and its parent container.
    */
   private getRelatedNames(identifier: ResourceIdentifier): { name: NamedNode; parent: NamedNode } {
-    const parentIdentifier = getParentContainer(identifier);
+    const parentIdentifier = this.identifierStrategy.getParentContainer(identifier);
     const name = namedNode(identifier.path);
     const parent = namedNode(parentIdentifier.path);
     return { name, parent };
diff --git a/src/util/PathUtil.ts b/src/util/PathUtil.ts
index f0cbc2455..02992a32e 100644
--- a/src/util/PathUtil.ts
+++ b/src/util/PathUtil.ts
@@ -1,5 +1,4 @@
 import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
-import { InternalServerError } from './errors/InternalServerError';
 
 /**
  * Makes sure the input path has exactly 1 slash at the end.
@@ -39,26 +38,6 @@ export const decodeUriPathComponents = (path: string): string => path.split('/')
  */
 export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/');
 
-/**
- * Finds the container containing the given resource.
- * This does not ensure either the container or resource actually exist.
- *
- * @param id - Identifier to find container of.
- *
- * @returns The identifier of the container this resource is in.
- */
-export const getParentContainer = (id: ResourceIdentifier): ResourceIdentifier => {
-  // Trailing slash is necessary for URL library
-  const parentPath = new URL('..', ensureTrailingSlash(id.path)).toString();
-
-  // This probably means there is an issue with the root
-  if (parentPath === id.path) {
-    throw new InternalServerError(`Resource ${id.path} does not have a parent container`);
-  }
-
-  return { path: parentPath };
-};
-
 /**
  * Checks if the path corresponds to a container path (ending in a /).
  * @param path - Path to check.
diff --git a/src/util/identifiers/IdentifierStrategy.ts b/src/util/identifiers/IdentifierStrategy.ts
new file mode 100644
index 000000000..43f6caca3
--- /dev/null
+++ b/src/util/identifiers/IdentifierStrategy.ts
@@ -0,0 +1,26 @@
+import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
+
+/**
+ * Captures the behavior of container identifiers in a certain storage configuration.
+ */
+export interface IdentifierStrategy {
+  /**
+   * Verifies if this identifier is supported.
+   * This does not check if this identifier actually exists,
+   * but checks if the identifier is in scope for this class.
+   */
+  supportsIdentifier: (identifier: ResourceIdentifier) => boolean;
+
+  /**
+   * Generates the identifier of the container this resource would be a member of.
+   * This does not check if that identifier actually exists.
+   * Will throw an error if the input identifier is a root container or is not supported.
+   */
+  getParentContainer: (identifier: ResourceIdentifier) => ResourceIdentifier;
+
+  /**
+   * Checks if the input corresponds to the identifier of a root container.
+   * This does not check if this identifier actually exists.
+   */
+  isRootContainer: (identifier: ResourceIdentifier) => boolean;
+}
diff --git a/src/util/identifiers/SingleRootIdentifierStrategy.ts b/src/util/identifiers/SingleRootIdentifierStrategy.ts
new file mode 100644
index 000000000..bb16c2e01
--- /dev/null
+++ b/src/util/identifiers/SingleRootIdentifierStrategy.ts
@@ -0,0 +1,37 @@
+import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
+import { InternalServerError } from '../errors/InternalServerError';
+import { ensureTrailingSlash } from '../PathUtil';
+import type { IdentifierStrategy } from './IdentifierStrategy';
+
+/**
+ * An IdentifierStrategy that assumes there is only 1 root and all other identifiers are made by appending to that root.
+ */
+export class SingleRootIdentifierStrategy implements IdentifierStrategy {
+  private readonly baseUrl: string;
+
+  public constructor(baseUrl: string) {
+    this.baseUrl = ensureTrailingSlash(baseUrl);
+  }
+
+  public supportsIdentifier(identifier: ResourceIdentifier): boolean {
+    return identifier.path.startsWith(this.baseUrl);
+  }
+
+  public getParentContainer(identifier: ResourceIdentifier): ResourceIdentifier {
+    if (!this.supportsIdentifier(identifier)) {
+      throw new InternalServerError(`${identifier.path} is not supported`);
+    }
+    if (this.isRootContainer(identifier)) {
+      throw new InternalServerError(`${identifier.path} is a root container and has no parent`);
+    }
+
+    // Trailing slash is necessary for URL library
+    const parentPath = new URL('..', ensureTrailingSlash(identifier.path)).href;
+
+    return { path: parentPath };
+  }
+
+  public isRootContainer(identifier: ResourceIdentifier): boolean {
+    return identifier.path === this.baseUrl;
+  }
+}


Test patch:
diff --git a/test/configs/Util.ts b/test/configs/Util.ts
index 99729cbe1..a39f87fe1 100644
--- a/test/configs/Util.ts
+++ b/test/configs/Util.ts
@@ -35,6 +35,7 @@ import {
   RawBodyParser,
   RepresentationConvertingStore,
   SequenceHandler,
+  SingleRootIdentifierStrategy,
   SingleThreadedResourceLocker,
   SlugParser,
   SparqlUpdatePatchHandler,
@@ -60,7 +61,7 @@ export const getRootFilePath = (subfolder: string): string => join(__dirname, '.
  * @returns The data accessor based store.
  */
 export const getDataAccessorStore = (base: string, dataAccessor: DataAccessor): DataAccessorBasedStore =>
-  new DataAccessorBasedStore(dataAccessor, base);
+  new DataAccessorBasedStore(dataAccessor, new SingleRootIdentifierStrategy(base));
 
 /**
  * Gives an in memory resource store based on (default) base url.
@@ -175,7 +176,7 @@ export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequ
  * @returns The acl authorizer.
  */
 export const getWebAclAuthorizer = (store: ResourceStore, aclManager = new UrlBasedAclManager()): WebAclAuthorizer =>
-  new WebAclAuthorizer(aclManager, store);
+  new WebAclAuthorizer(aclManager, store, new SingleRootIdentifierStrategy(BASE));
 
 /**
  * Returns a component instantiated from a Components.js configuration.
diff --git a/test/integration/LockingResourceStore.test.ts b/test/integration/LockingResourceStore.test.ts
index a907555f3..3c04fd9a3 100644
--- a/test/integration/LockingResourceStore.test.ts
+++ b/test/integration/LockingResourceStore.test.ts
@@ -6,6 +6,7 @@ import { DataAccessorBasedStore } from '../../src/storage/DataAccessorBasedStore
 import { LockingResourceStore } from '../../src/storage/LockingResourceStore';
 import type { ResourceStore } from '../../src/storage/ResourceStore';
 import { APPLICATION_OCTET_STREAM } from '../../src/util/ContentTypes';
+import { SingleRootIdentifierStrategy } from '../../src/util/identifiers/SingleRootIdentifierStrategy';
 import type { ExpiringResourceLocker } from '../../src/util/locking/ExpiringResourceLocker';
 import type { ResourceLocker } from '../../src/util/locking/ResourceLocker';
 import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker';
@@ -25,7 +26,7 @@ describe('A LockingResourceStore', (): void => {
 
     const base = 'http://test.com/';
     path = `${base}path`;
-    source = new DataAccessorBasedStore(new InMemoryDataAccessor(base), base);
+    source = new DataAccessorBasedStore(new InMemoryDataAccessor(base), new SingleRootIdentifierStrategy(base));
 
     locker = new SingleThreadedResourceLocker();
     expiringLocker = new WrappedExpiringResourceLocker(locker, 1000);
diff --git a/test/integration/SparqlStorage.test.ts b/test/integration/SparqlStorage.test.ts
index 50ee47a37..e161cfad7 100644
--- a/test/integration/SparqlStorage.test.ts
+++ b/test/integration/SparqlStorage.test.ts
@@ -1,5 +1,6 @@
 import { SparqlDataAccessor } from '../../src/storage/accessors/SparqlDataAccessor';
 import { INTERNAL_QUADS } from '../../src/util/ContentTypes';
+import { SingleRootIdentifierStrategy } from '../../src/util/identifiers/SingleRootIdentifierStrategy';
 import { DataAccessorBasedConfig } from '../configs/DataAccessorBasedConfig';
 import { BASE } from '../configs/Util';
 import { describeIf, FileTestHelper } from '../util/TestHelpers';
@@ -7,7 +8,7 @@ import { describeIf, FileTestHelper } from '../util/TestHelpers';
 describeIf('docker', 'a server with a SPARQL endpoint as storage', (): void => {
   describe('without acl', (): void => {
     const config = new DataAccessorBasedConfig(BASE,
-      new SparqlDataAccessor('http://localhost:4000/sparql', BASE),
+      new SparqlDataAccessor('http://localhost:4000/sparql', new SingleRootIdentifierStrategy(BASE)),
       INTERNAL_QUADS);
     const handler = config.getHttpHandler();
     const fileHelper = new FileTestHelper(handler, new URL(BASE));
diff --git a/test/unit/authorization/WebAclAuthorizer.test.ts b/test/unit/authorization/WebAclAuthorizer.test.ts
index 56b2ecd32..c3719d68a 100644
--- a/test/unit/authorization/WebAclAuthorizer.test.ts
+++ b/test/unit/authorization/WebAclAuthorizer.test.ts
@@ -8,9 +8,10 @@ import type { Representation } from '../../../src/ldp/representation/Representat
 import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
 import type { ResourceStore } from '../../../src/storage/ResourceStore';
 import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
+import { InternalServerError } from '../../../src/util/errors/InternalServerError';
 import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError';
-import { getParentContainer } from '../../../src/util/PathUtil';
+import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
 
 const nn = namedNode;
 
@@ -25,6 +26,8 @@ describe('A WebAclAuthorizer', (): void => {
     getAclConstrainedResource: async(id: ResourceIdentifier): Promise<ResourceIdentifier> =>
       !id.path.endsWith('.acl') ? id : { path: id.path.slice(0, -4) },
   };
+  let store: ResourceStore;
+  const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/');
   let permissions: PermissionSet;
   let credentials: Credentials;
   let identifier: ResourceIdentifier;
@@ -37,132 +40,119 @@ describe('A WebAclAuthorizer', (): void => {
     };
     credentials = {};
     identifier = { path: 'http://test.com/foo' };
+
+    store = {
+      getRepresentation: jest.fn(),
+    } as any;
+    authorizer = new WebAclAuthorizer(aclManager, store, identifierStrategy);
   });
 
   it('handles all inputs.', async(): Promise<void> => {
-    authorizer = new WebAclAuthorizer(aclManager, null as any);
+    authorizer = new WebAclAuthorizer(aclManager, null as any, identifierStrategy);
     await expect(authorizer.canHandle({} as any)).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 WebAclAuthorizer(aclManager, store);
+    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);
     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(getParentContainer(identifier).path)),
-            quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
-          ]),
-        } as Representation;
-      },
-    } as unknown as ResourceStore;
-    authorizer = new WebAclAuthorizer(aclManager, store);
+    store.getRepresentation = async(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(identifierStrategy.getParentContainer(identifier).path)),
+          quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
+        ]),
+      } as Representation;
+    };
     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 WebAclAuthorizer(aclManager, store);
+    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);
     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 WebAclAuthorizer(aclManager, store);
+    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);
     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 WebAclAuthorizer(aclManager, store);
+    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);
     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 WebAclAuthorizer(aclManager, store);
+    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);
     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;
+    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);
     const aclIdentifier = await aclManager.getAclDocument(identifier);
-    authorizer = new WebAclAuthorizer(aclManager, store);
     await expect(authorizer.handle({ identifier: aclIdentifier, 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;
+    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);
     identifier = await aclManager.getAclDocument(identifier);
-    authorizer = new WebAclAuthorizer(aclManager, 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 WebAclAuthorizer(aclManager, store);
+    store.getRepresentation = async(): Promise<Representation> => {
+      throw new Error('TEST!');
+    };
     await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow('TEST!');
   });
+
+  it('errors if the root container has no corresponding acl document.', async(): Promise<void> => {
+    store.getRepresentation = async(): Promise<Representation> => {
+      throw new NotFoundHttpError();
+    };
+    const promise = authorizer.handle({ identifier, permissions, credentials });
+    await expect(promise).rejects.toThrow('No ACL document found for root container');
+    await expect(promise).rejects.toThrow(InternalServerError);
+  });
 });
diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts
index 7624ce284..29a14c642 100644
--- a/test/unit/storage/DataAccessorBasedStore.test.ts
+++ b/test/unit/storage/DataAccessorBasedStore.test.ts
@@ -14,6 +14,7 @@ import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAll
 import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
 import type { Guarded } from '../../../src/util/GuardedStream';
+import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
 import * as quadUtil from '../../../src/util/QuadUtil';
 import { guardedStreamFrom } from '../../../src/util/StreamUtil';
 import { CONTENT_TYPE, HTTP, LDP, RDF } from '../../../src/util/UriConstants';
@@ -71,6 +72,7 @@ describe('A DataAccessorBasedStore', (): void => {
   let store: DataAccessorBasedStore;
   let accessor: SimpleDataAccessor;
   const root = 'http://test.com/';
+  const identifierStrategy = new SingleRootIdentifierStrategy(root);
   let containerMetadata: RepresentationMetadata;
   let representation: Representation;
   const resourceData = 'text';
@@ -78,7 +80,7 @@ describe('A DataAccessorBasedStore', (): void => {
   beforeEach(async(): Promise<void> => {
     accessor = new SimpleDataAccessor();
 
-    store = new DataAccessorBasedStore(accessor, root);
+    store = new DataAccessorBasedStore(accessor, identifierStrategy);
 
     containerMetadata = new RepresentationMetadata(
       { [RDF.type]: [ DataFactory.namedNode(LDP.Container), DataFactory.namedNode(LDP.BasicContainer) ]},
diff --git a/test/unit/storage/MonitoringStore.test.ts b/test/unit/storage/MonitoringStore.test.ts
index 81531f7dc..f9aab4ee5 100644
--- a/test/unit/storage/MonitoringStore.test.ts
+++ b/test/unit/storage/MonitoringStore.test.ts
@@ -2,10 +2,12 @@ import type { Patch } from '../../../src/ldp/http/Patch';
 import type { Representation } from '../../../src/ldp/representation/Representation';
 import { MonitoringStore } from '../../../src/storage/MonitoringStore';
 import type { ResourceStore } from '../../../src/storage/ResourceStore';
+import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
 
 describe('A MonitoringStore', (): void => {
   let store: MonitoringStore;
   let source: ResourceStore;
+  const identifierStrategy = new SingleRootIdentifierStrategy('http://example.org/');
   let changedCallback: () => void;
 
   beforeEach(async(): Promise<void> => {
@@ -16,7 +18,7 @@ describe('A MonitoringStore', (): void => {
       deleteResource: jest.fn(async(): Promise<any> => undefined),
       modifyResource: jest.fn(async(): Promise<any> => undefined),
     };
-    store = new MonitoringStore(source);
+    store = new MonitoringStore(source, identifierStrategy);
     changedCallback = jest.fn();
     store.on('changed', changedCallback);
   });
diff --git a/test/unit/storage/accessors/SparqlDataAccessor.test.ts b/test/unit/storage/accessors/SparqlDataAccessor.test.ts
index ceaccda88..935c68780 100644
--- a/test/unit/storage/accessors/SparqlDataAccessor.test.ts
+++ b/test/unit/storage/accessors/SparqlDataAccessor.test.ts
@@ -12,6 +12,7 @@ import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError
 import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
 import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
 import type { Guarded } from '../../../../src/util/GuardedStream';
+import { SingleRootIdentifierStrategy } from '../../../../src/util/identifiers/SingleRootIdentifierStrategy';
 import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
 import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
 import { toNamedNode } from '../../../../src/util/UriUtil';
@@ -30,6 +31,7 @@ const simplifyQuery = (query: string | string[]): string => {
 describe('A SparqlDataAccessor', (): void => {
   const endpoint = 'http://test.com/sparql';
   const base = 'http://test.com/';
+  const identifierStrategy = new SingleRootIdentifierStrategy(base);
   let accessor: SparqlDataAccessor;
   let metadata: RepresentationMetadata;
   let data: Guarded<Readable>;
@@ -64,7 +66,7 @@ describe('A SparqlDataAccessor', (): void => {
     }));
 
     // This needs to be last so the fetcher can be mocked first
-    accessor = new SparqlDataAccessor(endpoint, base);
+    accessor = new SparqlDataAccessor(endpoint, identifierStrategy);
   });
 
   it('can only handle quad data.', async(): Promise<void> => {
diff --git a/test/unit/util/PathUtil.test.ts b/test/unit/util/PathUtil.test.ts
index e319b11d5..27d545ef1 100644
--- a/test/unit/util/PathUtil.test.ts
+++ b/test/unit/util/PathUtil.test.ts
@@ -2,7 +2,6 @@ import {
   decodeUriPathComponents,
   encodeUriPathComponents,
   ensureTrailingSlash,
-  getParentContainer,
   toCanonicalUriPath,
 } from '../../../src/util/PathUtil';
 
@@ -29,16 +28,4 @@ describe('PathUtil', (): void => {
       expect(encodeUriPathComponents('/a%20path&/name')).toEqual('/a%2520path%26/name');
     });
   });
-
-  describe('#getParentContainer', (): void => {
-    it('returns the parent URl for a single call.', async(): Promise<void> => {
-      expect(getParentContainer({ path: 'http://test.com/foo/bar' })).toEqual({ path: 'http://test.com/foo/' });
-      expect(getParentContainer({ path: 'http://test.com/foo/bar/' })).toEqual({ path: 'http://test.com/foo/' });
-    });
-
-    it('errors when the root of an URl is reached that does not match the input root.', async(): Promise<void> => {
-      expect((): any => getParentContainer({ path: 'http://test.com/' }))
-        .toThrow('Resource http://test.com/ does not have a parent container');
-    });
-  });
 });
diff --git a/test/unit/util/identifiers/SingleRootIdentifierStrategy.test.ts b/test/unit/util/identifiers/SingleRootIdentifierStrategy.test.ts
new file mode 100644
index 000000000..9c0df063f
--- /dev/null
+++ b/test/unit/util/identifiers/SingleRootIdentifierStrategy.test.ts
@@ -0,0 +1,33 @@
+import { SingleRootIdentifierStrategy } from '../../../../src/util/identifiers/SingleRootIdentifierStrategy';
+
+describe('A SingleRootIdentifierStrategy', (): void => {
+  const baseUrl = 'http://test.com/';
+  const manager = new SingleRootIdentifierStrategy(baseUrl);
+
+  it('verifies if identifiers are in its domain.', async(): Promise<void> => {
+    expect(manager.supportsIdentifier({ path: 'http://notest.com/' })).toBe(false);
+    expect(manager.supportsIdentifier({ path: baseUrl })).toBe(true);
+    expect(manager.supportsIdentifier({ path: `${baseUrl}foo/bar` })).toBe(true);
+  });
+
+  it('returns the parent identifier.', async(): Promise<void> => {
+    expect(manager.getParentContainer({ path: 'http://test.com/foo/bar' })).toEqual({ path: 'http://test.com/foo/' });
+    expect(manager.getParentContainer({ path: 'http://test.com/foo/bar/' })).toEqual({ path: 'http://test.com/foo/' });
+  });
+
+  it('errors when attempting to get the parent of an unsupported identifier.', async(): Promise<void> => {
+    expect((): any => manager.getParentContainer({ path: 'http://nottest.com/' }))
+      .toThrow('http://nottest.com/ is not supported');
+  });
+
+  it('errors when attempting to get the parent of a root container.', async(): Promise<void> => {
+    expect((): any => manager.getParentContainer({ path: 'http://test.com/' }))
+      .toThrow('http://test.com/ is a root container and has no parent');
+  });
+
+  it('checks for the root container by comparing with the base URL.', async(): Promise<void> => {
+    expect(manager.isRootContainer({ path: 'http://notest.com/' })).toBe(false);
+    expect(manager.isRootContainer({ path: baseUrl })).toBe(true);
+    expect(manager.isRootContainer({ path: `${baseUrl}foo/bar` })).toBe(false);
+  });
+});