From e544e6dc1111eac16124ec29f65d58e7fc29a7b0 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Fri, 15 Jan 2021 12:38:22 +0100 Subject: [PATCH] feat: Allow custom root ACL. --- config/presets/init.json | 18 ++++----- src/init/AclInitializer.ts | 22 ++++++----- src/init/RootContainerInitializer.ts | 8 ++-- test/integration/LockingResourceStore.test.ts | 2 +- test/unit/init/AclInitializer.test.ts | 39 ++++++++++++++++--- .../init/RootContainerInitializer.test.ts | 2 +- 6 files changed, 61 insertions(+), 30 deletions(-) diff --git a/config/presets/init.json b/config/presets/init.json index cc0b898de..afd1f0b11 100644 --- a/config/presets/init.json +++ b/config/presets/init.json @@ -15,24 +15,24 @@ { "@id": "urn:solid-server:default:RootContainerInitializer", "@type": "RootContainerInitializer", - "RootContainerInitializer:_baseUrl": { - "@id": "urn:solid-server:default:variable:baseUrl" - }, - "RootContainerInitializer:_store": { + "RootContainerInitializer:_settings_store": { "@id": "urn:solid-server:default:ResourceStore" + }, + "RootContainerInitializer:_settings_baseUrl": { + "@id": "urn:solid-server:default:variable:baseUrl" } }, { "@id": "urn:solid-server:default:AclInitializer", "@type": "AclInitializer", - "AclInitializer:_baseUrl": { - "@id": "urn:solid-server:default:variable:baseUrl" - }, - "AclInitializer:_store": { + "AclInitializer:_settings_store": { "@id": "urn:solid-server:default:ResourceStore" }, - "AclInitializer:_aclManager": { + "AclInitializer:_settings_aclManager": { "@id": "urn:solid-server:default:AclManager" + }, + "AclInitializer:_settings_baseUrl": { + "@id": "urn:solid-server:default:variable:baseUrl" } }, { diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts index 4415d8f70..28fb4a3f3 100644 --- a/src/init/AclInitializer.ts +++ b/src/init/AclInitializer.ts @@ -9,6 +9,7 @@ import { TEXT_TURTLE } from '../util/ContentTypes'; import { ensureTrailingSlash, joinFilePath } from '../util/PathUtil'; import { Initializer } from './Initializer'; +const DEFAULT_ACL_PATH = joinFilePath(__dirname, '../../templates/root/.acl'); /** * Ensures that a root ACL is present. */ @@ -17,16 +18,19 @@ export class AclInitializer extends Initializer { private readonly store: ResourceStore; private readonly aclManager: AclManager; private readonly root: ResourceIdentifier; + private readonly aclPath: string; - public constructor( - baseUrl: string, - store: ResourceStore, - aclManager: AclManager, - ) { + public constructor(settings: { + store: ResourceStore; + aclManager: AclManager; + baseUrl: string; + aclPath?: string; + }) { super(); - this.store = store; - this.aclManager = aclManager; - this.root = { path: ensureTrailingSlash(baseUrl) }; + this.store = settings.store; + this.aclManager = settings.aclManager; + this.root = { path: ensureTrailingSlash(settings.baseUrl) }; + this.aclPath = settings.aclPath ?? DEFAULT_ACL_PATH; } public async handle(): Promise { @@ -42,7 +46,7 @@ export class AclInitializer extends Initializer { // The associated ACL document MUST include an authorization policy with acl:Control access privilege." // https://solid.github.io/specification/protocol#storage protected async setRootAclDocument(rootAcl: ResourceIdentifier): Promise { - const acl = await fsPromises.readFile(joinFilePath(__dirname, '../../templates/root/.acl'), 'utf8'); + const acl = await fsPromises.readFile(this.aclPath, 'utf8'); this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); await this.store.setRepresentation(rootAcl, new BasicRepresentation(acl, rootAcl, TEXT_TURTLE)); } diff --git a/src/init/RootContainerInitializer.ts b/src/init/RootContainerInitializer.ts index c24bd048f..f614e6a26 100644 --- a/src/init/RootContainerInitializer.ts +++ b/src/init/RootContainerInitializer.ts @@ -21,13 +21,13 @@ import namedNode = DataFactory.namedNode; */ export class RootContainerInitializer extends Initializer { protected readonly logger = getLoggerFor(this); - private readonly baseId: ResourceIdentifier; private readonly store: ResourceStore; + private readonly baseId: ResourceIdentifier; - public constructor(baseUrl: string, store: ResourceStore) { + public constructor(settings: { store: ResourceStore; baseUrl: string }) { super(); - this.baseId = { path: ensureTrailingSlash(baseUrl) }; - this.store = store; + this.store = settings.store; + this.baseId = { path: ensureTrailingSlash(settings.baseUrl) }; } public async handle(): Promise { diff --git a/test/integration/LockingResourceStore.test.ts b/test/integration/LockingResourceStore.test.ts index e87cb0265..ba99cc63e 100644 --- a/test/integration/LockingResourceStore.test.ts +++ b/test/integration/LockingResourceStore.test.ts @@ -29,7 +29,7 @@ describe('A LockingResourceStore', (): void => { source = new DataAccessorBasedStore(new InMemoryDataAccessor(base), new SingleRootIdentifierStrategy(base)); // Initialize store - const initializer = new RootContainerInitializer(BASE, source); + const initializer = new RootContainerInitializer({ store: source, baseUrl: BASE }); await initializer.handleSafe(); locker = new SingleThreadedResourceLocker(); diff --git a/test/unit/init/AclInitializer.test.ts b/test/unit/init/AclInitializer.test.ts index b5f02393b..5b9d4d670 100644 --- a/test/unit/init/AclInitializer.test.ts +++ b/test/unit/init/AclInitializer.test.ts @@ -1,9 +1,15 @@ import type { AclManager } from '../../../src/authorization/AclManager'; import { AclInitializer } from '../../../src/init/AclInitializer'; +import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceStore } from '../../../src/storage/ResourceStore'; import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; +jest.mock('../../../src/ldp/representation/BasicRepresentation'); + +// eslint-disable-next-line @typescript-eslint/naming-convention +const RepresentationMock: jest.Mock = BasicRepresentation as any; + describe('AclInitializer', (): void => { const store: jest.Mocked = { getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()), @@ -14,22 +20,40 @@ describe('AclInitializer', (): void => { } as any; const baseUrl = 'http://localhost:3000/'; - let initializer: AclInitializer; - beforeEach(async(): Promise => { - initializer = new AclInitializer(baseUrl, store, aclManager); - }); - afterEach((): void => { jest.clearAllMocks(); }); - it('invokes ACL initialization.', async(): Promise => { + it('sets the default ACL when none exists already.', async(): Promise => { + const initializer = new AclInitializer({ baseUrl, store, aclManager }); await initializer.handle(); expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); expect(store.getRepresentation).toHaveBeenCalledTimes(1); expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {}); expect(store.setRepresentation).toHaveBeenCalledTimes(1); + expect(store.setRepresentation).toHaveBeenCalledWith( + { path: 'http://test.com/.acl' }, RepresentationMock.mock.instances[0], + ); + expect(RepresentationMock).toHaveBeenCalledWith( + expect.stringMatching('<#authorization>'), { path: 'http://test.com/.acl' }, 'text/turtle', + ); + }); + + it('sets the specific ACL when one was specified.', async(): Promise => { + const initializer = new AclInitializer({ baseUrl, store, aclManager, aclPath: __filename }); + await initializer.handle(); + + expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); + expect(store.getRepresentation).toHaveBeenCalledTimes(1); + expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {}); + expect(store.setRepresentation).toHaveBeenCalledTimes(1); + expect(store.setRepresentation).toHaveBeenCalledWith( + { path: 'http://test.com/.acl' }, RepresentationMock.mock.instances[0], + ); + expect(RepresentationMock).toHaveBeenCalledWith( + expect.stringMatching('Joachim'), { path: 'http://test.com/.acl' }, 'text/turtle', + ); }); it('does not invoke ACL initialization when a root ACL already exists.', async(): Promise => { @@ -37,6 +61,7 @@ describe('AclInitializer', (): void => { data: { destroy: jest.fn() }, } as any)); + const initializer = new AclInitializer({ baseUrl, store, aclManager }); await initializer.handle(); expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' }); @@ -47,6 +72,8 @@ describe('AclInitializer', (): void => { it('errors when the root ACL check errors.', async(): Promise => { store.getRepresentation.mockRejectedValueOnce(new Error('Fatal')); + + const initializer = new AclInitializer({ baseUrl, store, aclManager }); await expect(initializer.handle()).rejects.toThrow('Fatal'); }); }); diff --git a/test/unit/init/RootContainerInitializer.test.ts b/test/unit/init/RootContainerInitializer.test.ts index ba7942f82..7c3c978c5 100644 --- a/test/unit/init/RootContainerInitializer.test.ts +++ b/test/unit/init/RootContainerInitializer.test.ts @@ -8,7 +8,7 @@ describe('A RootContainerInitializer', (): void => { getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()), setRepresentation: jest.fn(), } as any; - const initializer = new RootContainerInitializer(baseUrl, store); + const initializer = new RootContainerInitializer({ store, baseUrl }); afterEach((): void => { jest.clearAllMocks();