From 8fbb4f592e6873afca1ae7e1aa7062588630fcf9 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Mon, 7 Dec 2020 22:03:12 +0100 Subject: [PATCH] refactor: Split off AclInitializer. --- config/presets/setup.json | 27 +++++++-- src/index.ts | 2 + src/init/AclInitializer.ts | 80 +++++++++++++++++++++++++++ src/init/Initializer.ts | 3 + src/init/Setup.ts | 65 ++-------------------- src/util/AsyncHandler.ts | 2 +- test/unit/init/AclInitializer.test.ts | 52 +++++++++++++++++ test/unit/init/Setup.test.ts | 52 +++-------------- 8 files changed, 172 insertions(+), 111 deletions(-) create mode 100644 src/init/AclInitializer.ts create mode 100644 src/init/Initializer.ts create mode 100644 test/unit/init/AclInitializer.test.ts diff --git a/config/presets/setup.json b/config/presets/setup.json index a1d01bb12..4b95f96e8 100644 --- a/config/presets/setup.json +++ b/config/presets/setup.json @@ -7,21 +7,36 @@ "Setup:_serverFactory": { "@id": "urn:solid-server:default:ServerFactory" }, - "Setup:_store": { - "@id": "urn:solid-server:default:ResourceStore" - }, - "Setup:_aclManager": { - "@id": "urn:solid-server:default:AclManager" - }, "Setup:_loggerFactory": { "@id": "urn:solid-server:default:LoggerFactory" }, + "Setup:_initializer": { + "@id": "urn:solid-server:default:Initializer" + }, "Setup:_base": { "@id": "urn:solid-server:default:variable:baseUrl" }, "Setup:_port": { "@id": "urn:solid-server:default:variable:port" } + }, + { + "@id": "urn:solid-server:default:Initializer", + "@type": "AllVoidCompositeHandler", + "AllVoidCompositeHandler:_handlers": [ + { + "@type": "AclInitializer", + "AclInitializer:_baseUrl": { + "@id": "urn:solid-server:default:variable:baseUrl" + }, + "AclInitializer:_store": { + "@id": "urn:solid-server:default:ResourceStore" + }, + "AclInitializer:_aclManager": { + "@id": "urn:solid-server:default:AclManager" + } + } + ] } ] } diff --git a/src/index.ts b/src/index.ts index 336c09b78..f8c761026 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,9 @@ export * from './authorization/UrlBasedAclManager'; export * from './authorization/WebAclAuthorizer'; // Init +export * from './init/AclInitializer'; export * from './init/CliRunner'; +export * from './init/Initializer'; export * from './init/Setup'; // LDP/HTTP/Metadata diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts new file mode 100644 index 000000000..18199ee18 --- /dev/null +++ b/src/init/AclInitializer.ts @@ -0,0 +1,80 @@ +import type { AclManager } from '../authorization/AclManager'; +import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; +import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; +import { getLoggerFor } from '../logging/LogUtil'; +import type { ResourceStore } from '../storage/ResourceStore'; +import { TEXT_TURTLE } from '../util/ContentTypes'; +import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; +import { guardedStreamFrom } from '../util/StreamUtil'; +import { CONTENT_TYPE } from '../util/UriConstants'; +import { Initializer } from './Initializer'; + +/** + * Ensures that a root ACL is present. + */ +export class AclInitializer extends Initializer { + protected readonly logger = getLoggerFor(this); + private readonly store: ResourceStore; + private readonly aclManager: AclManager; + private readonly baseUrl: string; + + public constructor( + baseUrl: string, + store: ResourceStore, + aclManager: AclManager, + ) { + super(); + this.baseUrl = baseUrl; + this.store = store; + this.aclManager = aclManager; + } + + public async handle(): Promise { + const rootAcl = await this.aclManager.getAclDocument({ path: this.baseUrl }); + if (!await this.hasRootAclDocument(rootAcl)) { + await this.setRootAclDocument(rootAcl); + } + } + + protected async hasRootAclDocument(rootAcl: ResourceIdentifier): Promise { + try { + const result = await this.store.getRepresentation(rootAcl, {}); + this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`); + result.data.destroy(); + return true; + } catch (error: unknown) { + if (error instanceof NotFoundHttpError) { + return false; + } + throw error; + } + } + + // 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 + protected async setRootAclDocument(rootAcl: ResourceIdentifier): Promise { + const acl = `@prefix acl: . +@prefix foaf: . + +<#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 <${this.baseUrl}>; + acl:default <${this.baseUrl}>.`; + const metadata = new RepresentationMetadata(rootAcl.path, { [CONTENT_TYPE]: TEXT_TURTLE }); + this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); + await this.store.setRepresentation( + rootAcl, + { + binary: true, + data: guardedStreamFrom([ acl ]), + metadata, + }, + ); + } +} diff --git a/src/init/Initializer.ts b/src/init/Initializer.ts new file mode 100644 index 000000000..9348beb2e --- /dev/null +++ b/src/init/Initializer.ts @@ -0,0 +1,3 @@ +import { AsyncHandler } from '../util/AsyncHandler'; + +export abstract class Initializer extends AsyncHandler {} diff --git a/src/init/Setup.ts b/src/init/Setup.ts index 3d7a8554a..b35a8d967 100644 --- a/src/init/Setup.ts +++ b/src/init/Setup.ts @@ -1,14 +1,7 @@ -import type { AclManager } from '../authorization/AclManager'; -import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; -import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import type { LoggerFactory } from '../logging/LoggerFactory'; import { getLoggerFor, setGlobalLoggerFactory } from '../logging/LogUtil'; import type { HttpServerFactory } from '../server/HttpServerFactory'; -import type { ResourceStore } from '../storage/ResourceStore'; -import { TEXT_TURTLE } from '../util/ContentTypes'; -import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; -import { guardedStreamFrom } from '../util/StreamUtil'; -import { CONTENT_TYPE } from '../util/UriConstants'; +import type { Initializer } from './Initializer'; /** * Invokes all logic to setup a server. @@ -16,24 +9,21 @@ import { CONTENT_TYPE } from '../util/UriConstants'; export class Setup { protected readonly logger = getLoggerFor(this); private readonly serverFactory: HttpServerFactory; - private readonly store: ResourceStore; - private readonly aclManager: AclManager; private readonly loggerFactory: LoggerFactory; + private readonly initializer: Initializer; private readonly base: string; private readonly port: number; public constructor( serverFactory: HttpServerFactory, - store: ResourceStore, - aclManager: AclManager, loggerFactory: LoggerFactory, + initializer: Initializer, base: string, port: number, ) { this.serverFactory = serverFactory; - this.store = store; - this.aclManager = aclManager; this.loggerFactory = loggerFactory; + this.initializer = initializer; this.base = base; this.port = port; } @@ -44,54 +34,9 @@ export class Setup { public async setup(): Promise { setGlobalLoggerFactory(this.loggerFactory); - const rootAcl = await this.aclManager.getAclDocument({ path: this.base }); - if (!await this.hasRootAclDocument(rootAcl)) { - await this.setRootAclDocument(rootAcl); - } + await this.initializer.handleSafe(); this.serverFactory.startServer(this.port); return this.base; } - - protected async hasRootAclDocument(rootAcl: ResourceIdentifier): Promise { - try { - const result = await this.store.getRepresentation(rootAcl, {}); - this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`); - result.data.destroy(); - return true; - } catch (error: unknown) { - if (error instanceof NotFoundHttpError) { - return false; - } - throw error; - } - } - - // 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 - protected async setRootAclDocument(rootAcl: ResourceIdentifier): Promise { - const acl = `@prefix acl: . -@prefix foaf: . - -<#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 <${this.base}>; - acl:default <${this.base}>.`; - const metadata = new RepresentationMetadata(rootAcl.path, { [CONTENT_TYPE]: TEXT_TURTLE }); - this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); - await this.store.setRepresentation( - rootAcl, - { - binary: true, - data: guardedStreamFrom([ acl ]), - metadata, - }, - ); - } } diff --git a/src/util/AsyncHandler.ts b/src/util/AsyncHandler.ts index 78d9dca22..3658565a6 100644 --- a/src/util/AsyncHandler.ts +++ b/src/util/AsyncHandler.ts @@ -1,7 +1,7 @@ /** * Simple interface for classes that can potentially handle a specific kind of data asynchronously. */ -export abstract class AsyncHandler { +export abstract class AsyncHandler { /** * Checks if the input data can be handled by this class. * Throws an error if it can't handle the data. diff --git a/test/unit/init/AclInitializer.test.ts b/test/unit/init/AclInitializer.test.ts new file mode 100644 index 000000000..b5f02393b --- /dev/null +++ b/test/unit/init/AclInitializer.test.ts @@ -0,0 +1,52 @@ +import type { AclManager } from '../../../src/authorization/AclManager'; +import { AclInitializer } from '../../../src/init/AclInitializer'; +import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; +import type { ResourceStore } from '../../../src/storage/ResourceStore'; +import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; + +describe('AclInitializer', (): void => { + const store: jest.Mocked = { + getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()), + setRepresentation: jest.fn(), + } as any; + const aclManager: jest.Mocked = { + getAclDocument: jest.fn(async(): Promise => ({ path: 'http://test.com/.acl' })), + } 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 => { + 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); + }); + + it('does not invoke ACL initialization when a root ACL already exists.', async(): Promise => { + store.getRepresentation.mockReturnValueOnce(Promise.resolve({ + data: { destroy: jest.fn() }, + } as any)); + + 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(0); + }); + + it('errors when the root ACL check errors.', async(): Promise => { + store.getRepresentation.mockRejectedValueOnce(new Error('Fatal')); + await expect(initializer.handle()).rejects.toThrow('Fatal'); + }); +}); diff --git a/test/unit/init/Setup.test.ts b/test/unit/init/Setup.test.ts index c6e2034c9..2074ee27f 100644 --- a/test/unit/init/Setup.test.ts +++ b/test/unit/init/Setup.test.ts @@ -1,62 +1,26 @@ -import type { AclManager } from '../../../src/authorization/AclManager'; +import type { Initializer } from '../../../src/init/Initializer'; import { Setup } from '../../../src/init/Setup'; -import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import { VoidLoggerFactory } from '../../../src/logging/VoidLoggerFactory'; import type { HttpServerFactory } from '../../../src/server/HttpServerFactory'; -import type { ResourceStore } from '../../../src/storage/ResourceStore'; -import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; describe('Setup', (): void => { const serverFactory: jest.Mocked = { startServer: jest.fn(), }; - const store: jest.Mocked = { - getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()), - setRepresentation: jest.fn(), - } as any; - const aclManager: jest.Mocked = { - getAclDocument: jest.fn(async(): Promise => ({ path: 'http://test.com/.acl' })), + const initializer: jest.Mocked = { + handleSafe: jest.fn(), } as any; - let setup: Setup; - beforeEach(async(): Promise => { - setup = new Setup(serverFactory, store, aclManager, new VoidLoggerFactory(), 'http://localhost:3000/', 3000); - }); - - afterEach((): void => { - jest.clearAllMocks(); + beforeAll(async(): Promise => { + const setup = new Setup(serverFactory, new VoidLoggerFactory(), initializer, 'http://localhost:3000/', 3000); + await setup.setup(); }); it('starts an HTTP server.', async(): Promise => { - await setup.setup(); - expect(serverFactory.startServer).toHaveBeenCalledWith(3000); }); - it('invokes ACL initialization.', async(): Promise => { - await setup.setup(); - - 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); - }); - - it('does not invoke ACL initialization when a root ACL already exists.', async(): Promise => { - store.getRepresentation.mockReturnValueOnce(Promise.resolve({ - data: { destroy: jest.fn() }, - } as any)); - - await setup.setup(); - - 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(0); - }); - - it('errors when the root ACL check errors.', async(): Promise => { - store.getRepresentation.mockRejectedValueOnce(new Error('Fatal')); - await expect(setup.setup()).rejects.toThrow('Fatal'); + it('calls the initializer.', async(): Promise => { + expect(initializer.handleSafe).toHaveBeenCalledTimes(1); }); });