From 2c3300028e0bd182a2296db83dfb74db3daaf219 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 26 Jan 2021 10:32:43 +0100 Subject: [PATCH] feat: Add acl link header writer This writer will add a link to the corresponding acl file for all LDP requests. --- config/presets/ldp/response-writer.json | 6 ++++ src/index.ts | 1 + .../http/metadata/AclLinkMetadataWriter.ts | 25 +++++++++++++++++ test/integration/LdpHandlerWithAuth.test.ts | 6 ++-- .../integration/LdpHandlerWithoutAuth.test.ts | 27 +++++++++++------- test/integration/LpdHandlerOperations.test.ts | 9 ++++-- .../config/server-without-auth.json | 5 ++++ .../metadata/AclLinkMetadataWriter.test.ts | 28 +++++++++++++++++++ 8 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 src/ldp/http/metadata/AclLinkMetadataWriter.ts create mode 100644 test/unit/ldp/http/metadata/AclLinkMetadataWriter.test.ts diff --git a/config/presets/ldp/response-writer.json b/config/presets/ldp/response-writer.json index 3a474dd16..6582754d2 100644 --- a/config/presets/ldp/response-writer.json +++ b/config/presets/ldp/response-writer.json @@ -39,6 +39,12 @@ "LinkRelMetadataWriter:_linkRelMap_value": "type" } ] + }, + { + "@type": "AclLinkMetadataWriter", + "AclLinkMetadataWriter:_aclManager": { + "@id": "urn:solid-server:default:AclManager" + } } ] }, diff --git a/src/index.ts b/src/index.ts index 3701496b7..b86bbaef1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ export * from './init/RootContainerInitializer'; export * from './init/ServerInitializer'; // LDP/HTTP/Metadata +export * from './ldp/http/metadata/AclLinkMetadataWriter'; export * from './ldp/http/metadata/BasicMetadataExtractor'; export * from './ldp/http/metadata/ConstantMetadataWriter'; export * from './ldp/http/metadata/ContentTypeParser'; diff --git a/src/ldp/http/metadata/AclLinkMetadataWriter.ts b/src/ldp/http/metadata/AclLinkMetadataWriter.ts new file mode 100644 index 000000000..2d9bfba43 --- /dev/null +++ b/src/ldp/http/metadata/AclLinkMetadataWriter.ts @@ -0,0 +1,25 @@ +import type { AclManager } from '../../../authorization/AclManager'; +import type { HttpResponse } from '../../../server/HttpResponse'; +import { addHeader } from '../../../util/HeaderUtil'; +import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; +import { MetadataWriter } from './MetadataWriter'; + +/** + * A MetadataWriter that always adds a rel="acl" link header to a response. + * The `rel` parameter can be used if a different `rel` value is needed (such as http://www.w3.org/ns/solid/terms#acl). + */ +export class AclLinkMetadataWriter extends MetadataWriter { + private readonly aclManager: AclManager; + private readonly rel: string; + + public constructor(aclManager: AclManager, rel = 'acl') { + super(); + this.aclManager = aclManager; + this.rel = rel; + } + + public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise { + const identifier = await this.aclManager.getAclDocument({ path: input.metadata.identifier.value }); + addHeader(input.response, 'Link', `<${identifier.path}>; rel="${this.rel}"`); + } +} diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts index 0690e3ffc..1e0b42313 100644 --- a/test/integration/LdpHandlerWithAuth.test.ts +++ b/test/integration/LdpHandlerWithAuth.test.ts @@ -76,7 +76,8 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, se response = await resourceHelper.getResource(id); expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain('TESTFILE2'); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); // DELETE file await resourceHelper.deleteResource(id); @@ -109,7 +110,8 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, se // GET permanent file response = await resourceHelper.getResource('http://test.com/permanent.txt'); expect(response._getBuffer().toString()).toContain('TEST'); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`; rel="acl"`); // Try to delete permanent file response = await resourceHelper.deleteResource('http://test.com/permanent.txt', true); diff --git a/test/integration/LdpHandlerWithoutAuth.test.ts b/test/integration/LdpHandlerWithoutAuth.test.ts index 492f7ac67..92ecd9355 100644 --- a/test/integration/LdpHandlerWithoutAuth.test.ts +++ b/test/integration/LdpHandlerWithoutAuth.test.ts @@ -66,6 +66,7 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, const data = response._getData().toString(); expect(data).toContain(`<> a ldp:Container`); expect(response.getHeaders().link).toContain(`<${LDP.Container}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${BASE}/.acl>; rel="acl"`); }); it('can add a file to the store, read it and delete it.', async(): @@ -80,7 +81,8 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, response = await resourceHelper.getResource(id); expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain('TESTFILE0'); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); expect(response.getHeaders()['accept-patch']).toBe('application/sparql-update'); expect(response.getHeaders()['ms-author-via']).toBe('SPARQL'); @@ -99,7 +101,8 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, response = await resourceHelper.getResource(id); expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain('TESTFILE0'); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); // PUT response = await resourceHelper.replaceResource( @@ -110,7 +113,8 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, response = await resourceHelper.getResource(id); expect(response.statusCode).toBe(200); expect(response._getBuffer().toString()).toContain('TESTFILE1'); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); // DELETE await resourceHelper.deleteResource(id); @@ -125,9 +129,10 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, // GET response = await resourceHelper.getContainer(id); expect(response.statusCode).toBe(200); - expect(response.getHeaders().link).toEqual( - [ `<${LDP.Container}>; rel="type"`, `<${LDP.BasicContainer}>; rel="type"`, `<${LDP.Resource}>; rel="type"` ], - ); + expect(response.getHeaders().link).toContain(`<${LDP.Container}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.BasicContainer}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); // DELETE await resourceHelper.deleteResource(id); @@ -147,7 +152,8 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, // GET File response = await resourceHelper.getResource(id); expect(response.statusCode).toBe(200); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); // DELETE await resourceHelper.deleteResource(id); @@ -218,9 +224,10 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn, expect(response.statusCode).toBe(200); expect(response._getData()).toContain(' .'); expect(response._getData()).toContain(' .'); - expect(response.getHeaders().link).toEqual( - [ `<${LDP.Container}>; rel="type"`, `<${LDP.BasicContainer}>; rel="type"`, `<${LDP.Resource}>; rel="type"` ], - ); + expect(response.getHeaders().link).toContain(`<${LDP.Container}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.BasicContainer}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${folderId}.acl>; rel="acl"`); // DELETE await resourceHelper.deleteResource(fileId); diff --git a/test/integration/LpdHandlerOperations.test.ts b/test/integration/LpdHandlerOperations.test.ts index 5d1aa45c0..c4254d952 100644 --- a/test/integration/LpdHandlerOperations.test.ts +++ b/test/integration/LpdHandlerOperations.test.ts @@ -47,7 +47,8 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { expect(response._getData()).toContain( ' .', ); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); // DELETE response = await performRequest(handler, requestUrl, 'DELETE', {}, []); @@ -103,7 +104,8 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { expect(response._getData()).toContain( ' .', ); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); const parser = new Parser(); let triples = parser.parse(response._getData()); expect(triples).toBeRdfIsomorphic([ @@ -190,7 +192,8 @@ describe('An integrated AuthenticatedLdpHandler', (): void => { [], ); expect(response.statusCode).toBe(200); - expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); + expect(response.getHeaders().link).toContain(`<${id}.acl>; rel="acl"`); const parser = new Parser(); const triples = parser.parse(response._getData()); expect(triples).toBeRdfIsomorphic([ diff --git a/test/integration/config/server-without-auth.json b/test/integration/config/server-without-auth.json index 096fb874d..6e167c41b 100644 --- a/test/integration/config/server-without-auth.json +++ b/test/integration/config/server-without-auth.json @@ -46,6 +46,11 @@ "PassthroughStore:_source": { "@id": "urn:solid-server:default:MemoryResourceStore" } + }, + { + "@id": "urn:solid-server:default:AclManager", + "@type": "UrlBasedAclManager", + "comment": "Needed for AclLinkMetadataWriter" } ] } diff --git a/test/unit/ldp/http/metadata/AclLinkMetadataWriter.test.ts b/test/unit/ldp/http/metadata/AclLinkMetadataWriter.test.ts new file mode 100644 index 000000000..1a022fa0b --- /dev/null +++ b/test/unit/ldp/http/metadata/AclLinkMetadataWriter.test.ts @@ -0,0 +1,28 @@ +import { createResponse } from 'node-mocks-http'; +import type { AclManager } from '../../../../../src/authorization/AclManager'; +import { AclLinkMetadataWriter } from '../../../../../src/ldp/http/metadata/AclLinkMetadataWriter'; +import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata'; +import type { ResourceIdentifier } from '../../../../../src/ldp/representation/ResourceIdentifier'; + +describe('An AclLinkMetadataWriter', (): void => { + const manager = { + getAclDocument: async(id: ResourceIdentifier): Promise => ({ path: `${id.path}.acl` }), + } as AclManager; + const identifier = { path: 'http://test.com/foo' }; + + it('adds the acl link header.', async(): Promise => { + const writer = new AclLinkMetadataWriter(manager); + const response = createResponse(); + const metadata = new RepresentationMetadata(identifier); + await expect(writer.handle({ response, metadata })).resolves.toBeUndefined(); + expect(response.getHeaders()).toEqual({ link: `<${identifier.path}.acl>; rel="acl"` }); + }); + + it('can use a custom rel attribute.', async(): Promise => { + const writer = new AclLinkMetadataWriter(manager, 'http://www.w3.org/ns/solid/terms#acl'); + const response = createResponse(); + const metadata = new RepresentationMetadata(identifier); + await expect(writer.handle({ response, metadata })).resolves.toBeUndefined(); + expect(response.getHeaders()).toEqual({ link: `<${identifier.path}.acl>; rel="http://www.w3.org/ns/solid/terms#acl"` }); + }); +});