diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts index 51c73cfba..f67d4c0e6 100644 --- a/test/integration/LdpHandlerWithAuth.test.ts +++ b/test/integration/LdpHandlerWithAuth.test.ts @@ -1,8 +1,15 @@ -import { createReadStream } from 'fs'; -import type { HttpHandler, Initializer, ResourceStore } from '../../src/'; -import { LDP, BasicRepresentation, joinFilePath } from '../../src/'; -import { AclHelper, ResourceHelper } from '../util/TestHelpers'; -import { BASE, getTestFolder, removeFolder, instantiateFromConfig } from './Config'; +import type { Server } from 'http'; +import fetch from 'cross-fetch'; +import type { Initializer, ResourceStore } from '../../src/'; +import { BasicRepresentation } from '../../src/'; +import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; +import { deleteResource, getResource, postResource, putResource } from '../util/FetchUtil'; +import { AclHelper } from '../util/TestHelpers'; +import { getPort } from '../util/Util'; +import { getTestFolder, instantiateFromConfig, removeFolder } from './Config'; + +const port = getPort('LpdHandlerWithAuth'); +const baseUrl = `http://localhost:${port}/`; const rootFilePath = getTestFolder('full-config-acl'); const stores: [string, any][] = [ @@ -17,13 +24,17 @@ const stores: [string, any][] = [ ]; describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, teardown }): void => { - let handler: HttpHandler; + let server: Server; + let initializer: Initializer; + let factory: HttpServerFactory; + let store: ResourceStore; let aclHelper: AclHelper; - let resourceHelper: ResourceHelper; + const permanent = `${baseUrl}document.txt`; beforeAll(async(): Promise => { const variables: Record = { - 'urn:solid-server:default:variable:baseUrl': BASE, + 'urn:solid-server:default:variable:port': port, + 'urn:solid-server:default:variable:baseUrl': baseUrl, 'urn:solid-server:default:variable:rootFilePath': rootFilePath, }; const internalStore = await instantiateFromConfig( @@ -33,128 +44,145 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, te ) as ResourceStore; variables['urn:solid-server:default:variable:store'] = internalStore; - // Create and initialize the HTTP handler and related components - let initializer: Initializer; - let store: ResourceStore; + // Create and initialize the server const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', 'ldp-with-auth.json', variables, ) as Record; - ({ handler, store, initializer } = instances); - // Set up the internal store + ({ factory, initializer, store } = instances); + await initializer.handleSafe(); + server = factory.startServer(port); - // Create test helpers for manipulating the components - aclHelper = new AclHelper(store, BASE); - resourceHelper = new ResourceHelper(handler, BASE); + // Create test helper for manipulating acl + aclHelper = new AclHelper(store); + }); - // Write test resource - await store.setRepresentation({ path: `${BASE}/permanent.txt` }, - new BasicRepresentation(createReadStream(joinFilePath(__dirname, '../assets/permanent.txt')), 'text/plain')); + beforeEach(async(): Promise => { + // Set the root acl file to allow everything and create a single document + await store.setRepresentation({ path: permanent }, new BasicRepresentation('PERMANENT', 'text/plain')); + await aclHelper.setSimpleAcl(baseUrl, { + permissions: { read: true, write: true, append: true, control: true }, + agentClass: 'agent', + accessTo: true, + default: true, + }); }); afterAll(async(): Promise => { await teardown(); + await new Promise((resolve, reject): void => { + server.close((error): void => error ? reject(error) : resolve()); + }); }); - it('can add a file to the store, read it and delete it if allowed.', async(): Promise => { - // Set acl - await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'agent'); + it('can add a document, read it and delete it if allowed.', async(): Promise => { + await aclHelper.setSimpleAcl(baseUrl, { + permissions: { read: true, write: true, append: true }, + agentClass: 'agent', + accessTo: true, + default: true, + }); - // Create file - const filePath = 'testfile2.txt'; - const fileUrl = `${BASE}/${filePath}`; - let response = await resourceHelper.createResource( - '../assets/testfile2.txt', filePath, 'text/plain', - ); + // PUT + const document = `${baseUrl}test.txt`; + await putResource(document, { contentType: 'text/plain', body: 'TESTDATA' }); - // Get file - response = await resourceHelper.getResource(fileUrl); - expect(response.statusCode).toBe(200); - expect(response._getBuffer().toString()).toContain('TESTFILE2'); - expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); - expect(response.getHeaders().link).toContain(`<${fileUrl}.acl>; rel="acl"`); - expect(response.getHeaders()['wac-allow']).toBe('user="read write append",public="read write append"'); + // GET + const response = await getResource(document); + await expect(response.text()).resolves.toBe('TESTDATA'); + expect(response.headers.get('wac-allow')).toBe('user="read write append",public="read write append"'); - // DELETE file - await resourceHelper.deleteResource(fileUrl); - await resourceHelper.shouldNotExist(fileUrl); + // DELETE + await deleteResource(document); }); it('can not add a file to the store if not allowed.', async(): Promise => { - // Set acl - await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'authenticated'); + await aclHelper.setSimpleAcl(baseUrl, { + permissions: { read: true, write: true, append: true }, + agentClass: 'authenticated', + accessTo: true, + default: true, + }); - // Try to create file - const filePath = 'testfile2.txt'; - const response = await resourceHelper.createResource( - '../assets/testfile2.txt', filePath, 'text/plain', true, - ); - expect(response.statusCode).toBe(401); + // PUT fail + const documentUrl = `${baseUrl}test.txt`; + const response = await fetch(documentUrl, { method: 'PUT' }); + expect(response.status).toBe(401); }); - it('can not add/delete, but only read files if allowed.', async(): Promise => { - // Set acl - await aclHelper.setSimpleAcl({ read: true, write: false, append: false, control: false }, 'agent'); + it('can not add/delete if only read is allowed.', async(): Promise => { + await aclHelper.setSimpleAcl(baseUrl, { + permissions: { read: true }, + agentClass: 'agent', + accessTo: true, + default: true, + }); - // Try to create file - const filePath = 'testfile2.txt'; - let response = await resourceHelper.createResource( - '../assets/testfile2.txt', filePath, 'text/plain', true, - ); - expect(response.statusCode).toBe(401); + // PUT fail + const document = `${baseUrl}test.txt`; + let response = await fetch(document, { method: 'PUT' }); + expect(response.status).toBe(401); // GET permanent file - response = await resourceHelper.getResource('http://test.com/permanent.txt'); - expect(response._getBuffer().toString()).toContain('TEST'); - expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); - expect(response.getHeaders().link).toContain(`; rel="acl"`); - expect(response.getHeaders()['wac-allow']).toBe('user="read",public="read"'); + response = await getResource(permanent); + await expect(response.text()).resolves.toBe('PERMANENT'); + expect(response.headers.get('wac-allow')).toBe('user="read",public="read"'); - // Try to delete permanent file - response = await resourceHelper.deleteResource('http://test.com/permanent.txt', true); - expect(response.statusCode).toBe(401); + // DELETE fail + response = await fetch(permanent, { method: 'DELETE' }); + expect(response.status).toBe(401); }); it('can add files but not write to them if append is allowed.', async(): Promise => { - // Set acl - await aclHelper.setSimpleAcl({ read: true, write: false, append: true, control: false }, 'agent'); + await aclHelper.setSimpleAcl(baseUrl, { + permissions: { append: true }, + agentClass: 'agent', + accessTo: true, + default: true, + }); - // Add a file - const filePath = 'testfile2.txt'; - let response = await resourceHelper.performRequestWithBody( - new URL(`${BASE}/`), - 'POST', - { - 'content-type': 'text/plain', - 'transfer-encoding': 'chunked', - slug: filePath, - }, - Buffer.from('data'), - ); - expect(response.statusCode).toBe(201); + // POST + const slug = 'slug'; + let response = await postResource(baseUrl, { contentType: 'text/plain', slug, body: 'SLUGDATA' }); + const document = response.headers.get('location')!; - response = await resourceHelper.createResource( - '../assets/testfile2.txt', filePath, 'text/plain', true, - ); - expect(response.statusCode).toBe(401); + // PUT fail + response = await fetch(document, { method: 'PUT' }); + expect(response.status).toBe(401); + + // DELETE fail + response = await fetch(document, { method: 'DELETE' }); + expect(response.status).toBe(401); + + // Clean up resource + await store.deleteResource({ path: document }); }); it('can not access an acl file if no control rights are provided.', async(): Promise => { - // Set acl - await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'agent'); + await aclHelper.setSimpleAcl(baseUrl, { + permissions: { read: true, write: true, append: true }, + agentClass: 'agent', + accessTo: true, + }); - const response = await resourceHelper.performRequest(new URL('http://test.com/.acl'), 'GET', { accept: '*/*' }); - expect(response.statusCode).toBe(401); + const response = await fetch(`${baseUrl}.acl`); + expect(response.status).toBe(401); }); it('can only access an acl file if control rights are provided.', async(): Promise => { - // Set acl - await aclHelper.setSimpleAcl({ read: false, write: false, append: false, control: true }, 'agent'); + await aclHelper.setSimpleAcl(baseUrl, { + permissions: { control: true }, + agentClass: 'agent', + accessTo: true, + }); - const response = await resourceHelper.performRequest(new URL('http://test.com/.acl'), 'GET', { accept: '*/*' }); - expect(response.statusCode).toBe(200); - expect(response.getHeaders()['wac-allow']).toBe('user="control",public="control"'); + const response = await fetch(`${baseUrl}.acl`); + expect(response.status).toBe(200); + expect(response.headers.get('wac-allow')).toBe('user="control",public="control"'); + + // Close response + await response.text(); }); }); diff --git a/test/integration/LdpHandlerWithoutAuth.test.ts b/test/integration/LdpHandlerWithoutAuth.test.ts index 58890dc8c..57916be16 100644 --- a/test/integration/LdpHandlerWithoutAuth.test.ts +++ b/test/integration/LdpHandlerWithoutAuth.test.ts @@ -253,4 +253,22 @@ describe.each(stores)('An LDP handler allowing all request %s', (name, { storeUr // DELETE expect(await deleteResource(containerUrl)).toBeUndefined(); }); + + // https://github.com/solid/community-server/issues/498 + it('accepts a GET with Content-Length: 0.', async(): Promise => { + // PUT + const documentUrl = `${baseUrl}foo/bar`; + const response = await fetch(documentUrl, { + method: 'PUT', + headers: { 'content-length': '0', 'content-type': 'text/turtle' }, + body: '', + }); + expect(response.status).toBe(205); + + // GET + await getResource(documentUrl); + + // DELETE + expect(await deleteResource(documentUrl)).toBeUndefined(); + }); }); diff --git a/test/integration/ServerWithAuth.test.ts b/test/integration/ServerWithAuth.test.ts deleted file mode 100644 index 7e000933b..000000000 --- a/test/integration/ServerWithAuth.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { MockResponse } from 'node-mocks-http'; -import type { HttpHandler, Initializer, ResourceStore } from '../../src/'; -import { AclHelper } from '../util/TestHelpers'; -import { performRequest } from '../util/Util'; -import { BASE, instantiateFromConfig } from './Config'; - -describe('A server with authorization', (): void => { - let handler: HttpHandler; - let aclHelper: AclHelper; - - beforeAll(async(): Promise => { - // Set up the internal store - const variables: Record = { - 'urn:solid-server:default:variable:baseUrl': BASE, - }; - const internalStore = await instantiateFromConfig( - 'urn:solid-server:default:MemoryResourceStore', - 'ldp-with-auth.json', - variables, - ) as ResourceStore; - variables['urn:solid-server:default:variable:store'] = internalStore; - - // Create and initialize the HTTP handler and related components - let initializer: Initializer; - let store: ResourceStore; - const instances = await instantiateFromConfig( - 'urn:solid-server:test:Instances', - 'ldp-with-auth.json', - variables, - ) as Record; - ({ handler, store, initializer } = instances); - await initializer.handleSafe(); - - // Create test helpers for manipulating the components - aclHelper = new AclHelper(store, BASE); - }); - - it('can create new entries.', async(): Promise => { - await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'agent'); - - // POST - let requestUrl = new URL('http://test.com/'); - let response: MockResponse = await performRequest( - handler, - requestUrl, - 'POST', - { 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' }, - [ ' .' ], - ); - expect(response.statusCode).toBe(201); - - // PUT - requestUrl = new URL('http://test.com/foo/bar'); - response = await performRequest( - handler, - requestUrl, - 'PUT', - { 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' }, - [ ' .' ], - ); - expect(response.statusCode).toBe(205); - }); - - it('cannot create new entries if not allowed.', async(): Promise => { - await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'authenticated'); - - // POST - let requestUrl = new URL('http://test.com/'); - let response: MockResponse = await performRequest( - handler, - requestUrl, - 'POST', - { 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' }, - [ ' .' ], - ); - expect(response.statusCode).toBe(401); - - // PUT - requestUrl = new URL('http://test.com/foo/bar'); - response = await performRequest( - handler, - requestUrl, - 'PUT', - { 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' }, - [ ' .' ], - ); - expect(response.statusCode).toBe(401); - }); - - // https://github.com/solid/community-server/issues/498 - it('accepts a GET with Content-Length: 0.', async(): Promise => { - await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'agent'); - - // PUT - let requestUrl = new URL('http://test.com/foo/bar'); - let response: MockResponse = await performRequest( - handler, - requestUrl, - 'PUT', - { 'content-length': '0', 'content-type': 'text/turtle' }, - [], - ); - expect(response.statusCode).toBe(205); - - // GET - requestUrl = new URL('http://test.com/foo/bar'); - response = await performRequest( - handler, - requestUrl, - 'GET', - { 'content-length': '0' }, - [], - ); - expect(response.statusCode).toBe(200); - expect(response.getHeaders()['wac-allow']).toBe('user="read write append",public="read write append"'); - }); -}); diff --git a/test/integration/config/ldp-with-auth.json b/test/integration/config/ldp-with-auth.json index 78ea98f14..dea091453 100644 --- a/test/integration/config/ldp-with-auth.json +++ b/test/integration/config/ldp-with-auth.json @@ -4,7 +4,6 @@ "files-scs:config/presets/acl.json", "files-scs:config/presets/identifiers/suffix-identifiers.json", "files-scs:config/presets/init.json", - "files-scs:config/presets/ldp.json", "files-scs:config/presets/ldp/credentials-extractor.json", "files-scs:config/presets/ldp/metadata-handler.json", "files-scs:config/presets/ldp/operation-handler.json", @@ -24,7 +23,7 @@ "comment": "Sets up an HTTP server with only the LDP handler as HttpHandler.", "@id": "urn:solid-server:test:Instances", "@type": "RecordObject", - "RecordObject:_record": [ + "record": [ { "RecordObject:_record_key": "initializer", "RecordObject:_record_value": { @@ -35,10 +34,6 @@ ] } }, - { - "RecordObject:_record_key": "handler", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:LdpHandler" } - }, { "RecordObject:_record_key": "store", "RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore" } @@ -50,6 +45,38 @@ ] }, + { + "comment": "The default LDP handler, but with an unsecure webId extractor.", + "@id": "urn:solid-server:default:LdpHandler", + "@type": "AuthenticatedLdpHandler", + "args_requestParser": { + "@id": "urn:solid-server:default:RequestParser" + }, + "args_credentialsExtractor": { + "@type": "WaterfallHandler", + "WaterfallHandler:_handlers": [ + { + "@type": "UnsecureWebIdExtractor" + }, + { + "@type": "EmptyCredentialsExtractor" + } + ] + }, + "args_permissionsExtractor": { + "@id": "urn:solid-server:default:PermissionsExtractor" + }, + "args_authorizer": { + "@id": "urn:solid-server:default:AclBasedAuthorizer" + }, + "args_operationHandler": { + "@id": "urn:solid-server:default:OperationHandler" + }, + "args_responseWriter": { + "@id": "urn:solid-server:default:ResponseWriter" + } + }, + { "@id": "urn:solid-server:default:ServerFactory", "@type": "BaseHttpServerFactory", @@ -60,7 +87,7 @@ { "@id": "urn:solid-server:default:RoutingResourceStore", "@type": "PassthroughStore", - "PassthroughStore:_source": { + "source": { "@id": "urn:solid-server:default:variable:store" } }, diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts index 4f2fd7417..6d5ab477a 100644 --- a/test/util/TestHelpers.ts +++ b/test/util/TestHelpers.ts @@ -5,23 +5,34 @@ import { Readable } from 'stream'; import type { MockResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http'; import type { ResourceStore, PermissionSet, HttpHandler, HttpRequest } from '../../src/'; -import { BasicRepresentation, joinFilePath, ensureTrailingSlash } from '../../src/'; +import { BasicRepresentation, joinFilePath } from '../../src/'; import { performRequest } from './Util'; /* eslint-disable jest/no-standalone-expect */ export class AclHelper { public readonly store: ResourceStore; - public id: string; - public constructor(store: ResourceStore, id: string) { + public constructor(store: ResourceStore) { this.store = store; - this.id = ensureTrailingSlash(id); } public async setSimpleAcl( - permissions: PermissionSet, - agentClass: 'agent' | 'authenticated', + resource: string, + options: { + permissions: Partial; + agentClass?: 'agent' | 'authenticated'; + agent?: string; + accessTo?: boolean; + default?: boolean; + }, ): Promise { + if (!options.agentClass && !options.agent) { + throw new Error('At least one of agentClass or agent have to be provided for this to make sense.'); + } + if (!options.accessTo && !options.default) { + throw new Error('At least one of accessTo or default have to be true for this to make sense.'); + } + const acl: string[] = [ '@prefix acl: .\n', '@prefix foaf: .\n', @@ -29,21 +40,30 @@ export class AclHelper { ]; for (const perm of [ 'Read', 'Append', 'Write', 'Control' ]) { - if (permissions[perm.toLowerCase() as keyof PermissionSet]) { + if (options.permissions[perm.toLowerCase() as keyof PermissionSet]) { acl.push(`;\n acl:mode acl:${perm}`); } } - acl.push(`;\n acl:accessTo <${this.id}>`); - acl.push(`;\n acl:default <${this.id}>`); - acl.push( - `;\n acl:agentClass ${ - agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent' - }`, - ); + if (options.accessTo) { + acl.push(`;\n acl:accessTo <${resource}>`); + } + if (options.default) { + acl.push(`;\n acl:default <${resource}>`); + } + if (options.agentClass) { + acl.push( + `;\n acl:agentClass ${ + options.agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent' + }`, + ); + } + if (options.agent) { + acl.push(`;\n acl:agent ${options.agent}`); + } acl.push('.'); - await this.store.setRepresentation({ path: `${this.id}.acl` }, new BasicRepresentation(acl, 'text/turtle')); + await this.store.setRepresentation({ path: `${resource}.acl` }, new BasicRepresentation(acl, 'text/turtle')); } } diff --git a/test/util/Util.ts b/test/util/Util.ts index 08dc59e59..0759c6e5b 100644 --- a/test/util/Util.ts +++ b/test/util/Util.ts @@ -12,6 +12,7 @@ import type { SystemError } from '../../src/util/errors/SystemError'; /* eslint-disable @typescript-eslint/naming-convention */ const portNames = [ 'DynamicPods', + 'LpdHandlerWithAuth', 'LpdHandlerWithoutAuth', 'Middleware', 'PodCreation',