From f0db9e501f45c265855071e6dc3be77d28e98c80 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Wed, 9 Dec 2020 14:46:41 +0100 Subject: [PATCH] refactor: Replace getParentContainer util function with ContainerManager --- config/presets/acl.json | 3 + config/presets/storage-wrapper.json | 11 ++ .../storage/backend/storage-filesystem.json | 3 + .../storage/backend/storage-memory.json | 3 + .../backend/storage-sparql-endpoint.json | 6 + src/authorization/WebAclAuthorizer.ts | 13 +- src/storage/DataAccessorBasedStore.ts | 10 +- src/storage/MonitoringStore.ts | 12 +- src/storage/accessors/SparqlDataAccessor.ts | 9 +- src/util/PathUtil.ts | 21 --- test/configs/Util.ts | 5 +- test/integration/LockingResourceStore.test.ts | 3 +- test/integration/SparqlStorage.test.ts | 3 +- .../authorization/WebAclAuthorizer.test.ts | 148 ++++++++---------- .../storage/DataAccessorBasedStore.test.ts | 4 +- test/unit/storage/MonitoringStore.test.ts | 4 +- .../accessors/SparqlDataAccessor.test.ts | 4 +- test/unit/util/PathUtil.test.ts | 13 -- 18 files changed, 139 insertions(+), 136 deletions(-) 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..2feabbf1e 100644 --- a/config/presets/storage/backend/storage-filesystem.json +++ b/config/presets/storage/backend/storage-filesystem.json @@ -28,6 +28,9 @@ }, "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..a3680178a 100644 --- a/config/presets/storage/backend/storage-memory.json +++ b/config/presets/storage/backend/storage-memory.json @@ -16,6 +16,9 @@ }, "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..7d990d505 100644 --- a/config/presets/storage/backend/storage-sparql-endpoint.json +++ b/config/presets/storage/backend/storage-sparql-endpoint.json @@ -9,6 +9,9 @@ }, "SparqlDataAccessor:_base": { "@id": "urn:solid-server:default:variable:baseUrl" + }, + "SparqlDataAccessor:_identifierStrategy": { + "@id": "urn:solid-server:default:IdentifierStrategy" } }, @@ -20,6 +23,9 @@ }, "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/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index 48611d4ce..be415351f 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, @@ -51,10 +51,12 @@ 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, base: string, identifierStrategy: IdentifierStrategy) { this.accessor = accessor; this.base = ensureTrailingSlash(base); + this.identifierStrategy = identifierStrategy; } public async getRepresentation(identifier: ResourceIdentifier): Promise { @@ -219,7 +221,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 +356,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 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 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..cbc3eac8f 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 { ensureTrailingSlash, isContainerIdentifier } from '../../util/PathUtil'; import { generateResourceQuads } from '../../util/ResourceUtil'; import { CONTENT_TYPE, LDP } from '../../util/UriConstants'; import { toNamedNode } from '../../util/UriUtil'; @@ -49,12 +50,14 @@ 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, base: string, identifierStrategy: IdentifierStrategy) { this.endpoint = endpoint; this.base = ensureTrailingSlash(base); + this.identifierStrategy = identifierStrategy; this.fetcher = new SparqlEndpointFetcher(); this.generator = new Generator(); } @@ -149,7 +152,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/test/configs/Util.ts b/test/configs/Util.ts index 99729cbe1..ad70acb7a 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, base, 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..b1d50ebea 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), 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..5c11d164c 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', BASE, 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 => !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 => { - 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 => { - const store = { - getRepresentation: async(): Promise => ({ data: streamifyArray([ - quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), - ]) } as Representation), - } as unknown as ResourceStore; - authorizer = new WebAclAuthorizer(aclManager, store); + store.getRepresentation = async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation); await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); }); it('allows access if there is a parent acl file allowing all agents.', async(): Promise => { - const store = { - async getRepresentation(id: ResourceIdentifier): Promise { - if (id.path.endsWith('foo.acl')) { - throw new NotFoundHttpError(); - } - return { - data: streamifyArray([ - quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')), - quad(nn('auth'), nn(`${acl}default`), nn(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 => { + 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 => { - const store = { - getRepresentation: async(): Promise => ({ data: streamifyArray([ - quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), - ]) } as Representation), - } as unknown as ResourceStore; - authorizer = new WebAclAuthorizer(aclManager, store); + store.getRepresentation = async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation); credentials.webId = 'http://test.com/user'; await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); }); it('errors if authorization is required but the agent is not authorized.', async(): Promise => { - const store = { - getRepresentation: async(): Promise => ({ data: streamifyArray([ - quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), - ]) } as Representation), - } as unknown as ResourceStore; - authorizer = new WebAclAuthorizer(aclManager, store); + store.getRepresentation = async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation); await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(UnauthorizedHttpError); }); it('allows access to specific agents if the acl files identifies them.', async(): Promise => { credentials.webId = 'http://test.com/user'; - const store = { - getRepresentation: async(): Promise => ({ data: streamifyArray([ - quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), - ]) } as Representation), - } as unknown as ResourceStore; - authorizer = new WebAclAuthorizer(aclManager, store); + store.getRepresentation = async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation); await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); }); it('errors if a specific agents wants to access files not assigned to them.', async(): Promise => { credentials.webId = 'http://test.com/user'; - const store = { - getRepresentation: async(): Promise => ({ data: streamifyArray([ - quad(nn('auth'), nn(`${acl}agent`), nn('http://test.com/differentUser')), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), - ]) } as Representation), - } as unknown as ResourceStore; - authorizer = new WebAclAuthorizer(aclManager, store); + store.getRepresentation = async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agent`), nn('http://test.com/differentUser')), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation); await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError); }); it('allows access to the acl file if control is allowed.', async(): Promise => { credentials.webId = 'http://test.com/user'; identifier.path = 'http://test.com/foo'; - const store = { - getRepresentation: async(): Promise => ({ data: streamifyArray([ - quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)), - ]) } as Representation), - } as unknown as ResourceStore; + store.getRepresentation = async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)), + ]) } as Representation); 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 => { credentials.webId = 'http://test.com/user'; identifier.path = 'http://test.com/foo'; - const store = { - getRepresentation: async(): Promise => ({ data: streamifyArray([ - quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), - ]) } as Representation), - } as unknown as ResourceStore; + store.getRepresentation = async(): Promise => ({ data: streamifyArray([ + quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)), + quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), + ]) } as Representation); 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 => { - const store = { - async getRepresentation(): Promise { - throw new Error('TEST!'); - }, - } as unknown as ResourceStore; - authorizer = new WebAclAuthorizer(aclManager, store); + store.getRepresentation = async(): Promise => { + 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 => { + store.getRepresentation = async(): Promise => { + 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..02c489898 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 => { accessor = new SimpleDataAccessor(); - store = new DataAccessorBasedStore(accessor, root); + store = new DataAccessorBasedStore(accessor, root, 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 => { @@ -16,7 +18,7 @@ describe('A MonitoringStore', (): void => { deleteResource: jest.fn(async(): Promise => undefined), modifyResource: jest.fn(async(): Promise => 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..901331144 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; @@ -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, base, identifierStrategy); }); it('can only handle quad data.', async(): Promise => { 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 => { - 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 => { - expect((): any => getParentContainer({ path: 'http://test.com/' })) - .toThrow('Resource http://test.com/ does not have a parent container'); - }); - }); });