feat: Integrate acl with rest of server

This commit is contained in:
Joachim Van Herwegen 2020-08-07 11:55:04 +02:00
parent 0545ca121e
commit 769b49293c
7 changed files with 280 additions and 43 deletions

View File

@ -1,3 +1,6 @@
#!/usr/bin/env node
import { DATA_TYPE_BINARY } from '../src/util/ContentTypes';
import streamifyArray from 'streamify-array';
import yargs from 'yargs'; import yargs from 'yargs';
import { import {
AcceptPreferenceParser, AcceptPreferenceParser,
@ -9,14 +12,16 @@ import {
QuadToTurtleConverter, QuadToTurtleConverter,
Representation, Representation,
RepresentationConvertingStore, RepresentationConvertingStore,
SimpleAuthorizer, SimpleAclAuthorizer,
SimpleBodyParser, SimpleBodyParser,
SimpleCredentialsExtractor, SimpleCredentialsExtractor,
SimpleDeleteOperationHandler, SimpleDeleteOperationHandler,
SimpleExtensionAclManager,
SimpleGetOperationHandler, SimpleGetOperationHandler,
SimplePatchOperationHandler, SimplePatchOperationHandler,
SimplePermissionsExtractor, SimplePermissionsExtractor,
SimplePostOperationHandler, SimplePostOperationHandler,
SimplePutOperationHandler,
SimpleRequestParser, SimpleRequestParser,
SimpleResourceStore, SimpleResourceStore,
SimpleResponseWriter, SimpleResponseWriter,
@ -25,6 +30,7 @@ import {
SimpleTargetExtractor, SimpleTargetExtractor,
SingleThreadedResourceLocker, SingleThreadedResourceLocker,
TurtleToQuadConverter, TurtleToQuadConverter,
UrlContainerManager,
} from '..'; } from '..';
const { argv } = yargs const { argv } = yargs
@ -36,6 +42,8 @@ const { argv } = yargs
const { port } = argv; const { port } = argv;
const base = `http://localhost:${port}/`;
// This is instead of the dependency injection that still needs to be added // This is instead of the dependency injection that still needs to be added
const bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([ const bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
new SimpleSparqlUpdateBodyParser(), new SimpleSparqlUpdateBodyParser(),
@ -49,10 +57,9 @@ const requestParser = new SimpleRequestParser({
const credentialsExtractor = new SimpleCredentialsExtractor(); const credentialsExtractor = new SimpleCredentialsExtractor();
const permissionsExtractor = new SimplePermissionsExtractor(); const permissionsExtractor = new SimplePermissionsExtractor();
const authorizer = new SimpleAuthorizer();
// Will have to see how to best handle this // Will have to see how to best handle this
const store = new SimpleResourceStore(`http://localhost:${port}/`); const store = new SimpleResourceStore(base);
const converter = new CompositeAsyncHandler([ const converter = new CompositeAsyncHandler([
new TurtleToQuadConverter(), new TurtleToQuadConverter(),
new QuadToTurtleConverter(), new QuadToTurtleConverter(),
@ -62,11 +69,16 @@ const locker = new SingleThreadedResourceLocker();
const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker); const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
const patchingStore = new PatchingStore(convertingStore, patcher); const patchingStore = new PatchingStore(convertingStore, patcher);
const aclManager = new SimpleExtensionAclManager();
const containerManager = new UrlContainerManager(base);
const authorizer = new SimpleAclAuthorizer(aclManager, containerManager, patchingStore);
const operationHandler = new CompositeAsyncHandler([ const operationHandler = new CompositeAsyncHandler([
new SimpleDeleteOperationHandler(patchingStore), new SimpleDeleteOperationHandler(patchingStore),
new SimpleGetOperationHandler(patchingStore), new SimpleGetOperationHandler(patchingStore),
new SimplePatchOperationHandler(patchingStore), new SimplePatchOperationHandler(patchingStore),
new SimplePostOperationHandler(patchingStore), new SimplePostOperationHandler(patchingStore),
new SimplePutOperationHandler(patchingStore),
]); ]);
const responseWriter = new SimpleResponseWriter(); const responseWriter = new SimpleResponseWriter();
@ -82,6 +94,40 @@ const httpHandler = new AuthenticatedLdpHandler({
const httpServer = new ExpressHttpServer(httpHandler); const httpServer = new ExpressHttpServer(httpHandler);
httpServer.listen(port); // 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
const aclSetup = async(): Promise<void> => {
const acl = `@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
process.stdout.write(`Running at http://localhost:${port}/\n`); <#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 <${base}>;
acl:default <${base}>.`;
await store.setRepresentation(
await aclManager.getAcl({ path: base }),
{
dataType: DATA_TYPE_BINARY,
data: streamifyArray([ acl ]),
metadata: {
raw: [],
profiles: [],
contentType: 'text/turtle',
},
},
);
};
aclSetup().then((): void => {
httpServer.listen(port);
process.stdout.write(`Running at ${base}\n`);
}).catch((error): void => {
process.stderr.write(`${error}\n`);
process.exit(1);
});

View File

@ -4,8 +4,11 @@ export * from './src/authentication/CredentialsExtractor';
export * from './src/authentication/SimpleCredentialsExtractor'; export * from './src/authentication/SimpleCredentialsExtractor';
// Authorization // Authorization
export * from './src/authorization/AclManager';
export * from './src/authorization/Authorizer'; export * from './src/authorization/Authorizer';
export * from './src/authorization/SimpleAclAuthorizer';
export * from './src/authorization/SimpleAuthorizer'; export * from './src/authorization/SimpleAuthorizer';
export * from './src/authorization/SimpleExtensionAclManager';
// LDP/HTTP // LDP/HTTP
export * from './src/ldp/http/AcceptPreferenceParser'; export * from './src/ldp/http/AcceptPreferenceParser';
@ -30,6 +33,7 @@ export * from './src/ldp/operations/SimpleDeleteOperationHandler';
export * from './src/ldp/operations/SimpleGetOperationHandler'; export * from './src/ldp/operations/SimpleGetOperationHandler';
export * from './src/ldp/operations/SimplePatchOperationHandler'; export * from './src/ldp/operations/SimplePatchOperationHandler';
export * from './src/ldp/operations/SimplePostOperationHandler'; export * from './src/ldp/operations/SimplePostOperationHandler';
export * from './src/ldp/operations/SimplePutOperationHandler';
// LDP/Permissions // LDP/Permissions
export * from './src/ldp/permissions/PermissionSet'; export * from './src/ldp/permissions/PermissionSet';
@ -67,6 +71,7 @@ export * from './src/storage/patch/SimpleSparqlUpdatePatchHandler';
// Storage // Storage
export * from './src/storage/AtomicResourceStore'; export * from './src/storage/AtomicResourceStore';
export * from './src/storage/Conditions'; export * from './src/storage/Conditions';
export * from './src/storage/ContainerManager';
export * from './src/storage/Lock'; export * from './src/storage/Lock';
export * from './src/storage/LockingResourceStore'; export * from './src/storage/LockingResourceStore';
export * from './src/storage/PassthroughStore'; export * from './src/storage/PassthroughStore';
@ -77,10 +82,13 @@ export * from './src/storage/ResourceMapper';
export * from './src/storage/ResourceStore'; export * from './src/storage/ResourceStore';
export * from './src/storage/SingleThreadedResourceLocker'; export * from './src/storage/SingleThreadedResourceLocker';
export * from './src/storage/SimpleResourceStore'; export * from './src/storage/SimpleResourceStore';
export * from './src/storage/UrlContainerManager';
// Util/Errors // Util/Errors
export * from './src/util/errors/ForbiddenHttpError';
export * from './src/util/errors/HttpError'; export * from './src/util/errors/HttpError';
export * from './src/util/errors/NotFoundHttpError'; export * from './src/util/errors/NotFoundHttpError';
export * from './src/util/errors/UnauthorizedHttpError';
export * from './src/util/errors/UnsupportedHttpError'; export * from './src/util/errors/UnsupportedHttpError';
export * from './src/util/errors/UnsupportedMediaTypeHttpError'; export * from './src/util/errors/UnsupportedMediaTypeHttpError';

View File

@ -1,5 +0,0 @@
describe('A basic test', (): void => {
it('to have something pass.', async(): Promise<void> => {
expect(true).toBeTruthy();
});
});

View File

@ -1,11 +1,10 @@
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser'; import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler'; import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler';
import { BodyParser } from '../../src/ldp/http/BodyParser'; import { BodyParser } from '../../src/ldp/http/BodyParser';
import { call } from '../util/Util';
import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler'; import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler';
import { EventEmitter } from 'events';
import { HttpHandler } from '../../src/server/HttpHandler';
import { HttpRequest } from '../../src/server/HttpRequest'; import { HttpRequest } from '../../src/server/HttpRequest';
import { IncomingHttpHeaders } from 'http'; import { MockResponse } from 'node-mocks-http';
import { Operation } from '../../src/ldp/operations/Operation'; import { Operation } from '../../src/ldp/operations/Operation';
import { Parser } from 'n3'; import { Parser } from 'n3';
import { PatchingStore } from '../../src/storage/PatchingStore'; import { PatchingStore } from '../../src/storage/PatchingStore';
@ -28,35 +27,11 @@ import { SimpleSparqlUpdateBodyParser } from '../../src/ldp/http/SimpleSparqlUpd
import { SimpleSparqlUpdatePatchHandler } from '../../src/storage/patch/SimpleSparqlUpdatePatchHandler'; import { SimpleSparqlUpdatePatchHandler } from '../../src/storage/patch/SimpleSparqlUpdatePatchHandler';
import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor'; import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker'; import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker';
import streamifyArray from 'streamify-array';
import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter'; import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter';
import { createResponse, MockResponse } from 'node-mocks-http';
import { namedNode, quad } from '@rdfjs/data-model'; import { namedNode, quad } from '@rdfjs/data-model';
import * as url from 'url'; import * as url from 'url';
const call = async(handler: HttpHandler, requestUrl: url.URL, method: string, describe('An integrated AuthenticatedLdpHandler', (): void => {
headers: IncomingHttpHeaders, data: string[]): Promise<MockResponse<any>> => {
const request = streamifyArray(data) as HttpRequest;
request.url = requestUrl.pathname;
request.method = method;
request.headers = headers;
request.headers.host = requestUrl.host;
const response: MockResponse<any> = createResponse({ eventEmitter: EventEmitter });
const endPromise = new Promise((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
resolve();
});
});
await handler.handleSafe({ request, response });
await endPromise;
return response;
};
describe('An AuthenticatedLdpHandler', (): void => {
describe('with simple handlers', (): void => { describe('with simple handlers', (): void => {
const requestParser = new SimpleRequestParser({ const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(), targetExtractor: new SimpleTargetExtractor(),
@ -88,7 +63,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
it('can add, read and delete data based on incoming requests.', async(): Promise<void> => { it('can add, read and delete data based on incoming requests.', async(): Promise<void> => {
// POST // POST
let requestUrl = new url.URL('http://test.com/'); let requestUrl = new URL('http://test.com/');
let response: MockResponse<any> = await call( let response: MockResponse<any> = await call(
handler, handler,
requestUrl, requestUrl,
@ -102,7 +77,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
expect(id).toContain(url.format(requestUrl)); expect(id).toContain(url.format(requestUrl));
// GET // GET
requestUrl = new url.URL(id); requestUrl = new URL(id);
response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []); response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response._getData()).toContain('<http://test.com/s> <http://test.com/p> <http://test.com/o>.'); expect(response._getData()).toContain('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
@ -166,7 +141,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
it('can handle simple SPARQL updates.', async(): Promise<void> => { it('can handle simple SPARQL updates.', async(): Promise<void> => {
// POST // POST
let requestUrl = new url.URL('http://test.com/'); let requestUrl = new URL('http://test.com/');
let response: MockResponse<any> = await call( let response: MockResponse<any> = await call(
handler, handler,
requestUrl, requestUrl,
@ -181,7 +156,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
expect(id).toContain(url.format(requestUrl)); expect(id).toContain(url.format(requestUrl));
// PATCH // PATCH
requestUrl = new url.URL(id); requestUrl = new URL(id);
response = await call( response = await call(
handler, handler,
requestUrl, requestUrl,
@ -196,7 +171,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
expect(response._getHeaders().location).toBe(id); expect(response._getHeaders().location).toBe(id);
// GET // GET
requestUrl = new url.URL(id); requestUrl = new URL(id);
response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []); response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response._getData()).toContain('<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.'); expect(response._getData()).toContain('<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.');

View File

@ -0,0 +1,181 @@
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler';
import { BodyParser } from '../../src/ldp/http/BodyParser';
import { call } from '../util/Util';
import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler';
import { DATA_TYPE_BINARY } from '../../src/util/ContentTypes';
import { MockResponse } from 'node-mocks-http';
import { Operation } from '../../src/ldp/operations/Operation';
import { PermissionSet } from '../../src/ldp/permissions/PermissionSet';
import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtleConverter';
import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore';
import { ResourceStore } from '../../src/storage/ResourceStore';
import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription';
import { SimpleAclAuthorizer } from '../../src/authorization/SimpleAclAuthorizer';
import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor';
import { SimpleDeleteOperationHandler } from '../../src/ldp/operations/SimpleDeleteOperationHandler';
import { SimpleExtensionAclManager } from '../../src/authorization/SimpleExtensionAclManager';
import { SimpleGetOperationHandler } from '../../src/ldp/operations/SimpleGetOperationHandler';
import { SimplePermissionsExtractor } from '../../src/ldp/permissions/SimplePermissionsExtractor';
import { SimplePostOperationHandler } from '../../src/ldp/operations/SimplePostOperationHandler';
import { SimplePutOperationHandler } from '../../src/ldp/operations/SimplePutOperationHandler';
import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser';
import { SimpleResourceStore } from '../../src/storage/SimpleResourceStore';
import { SimpleResponseWriter } from '../../src/ldp/http/SimpleResponseWriter';
import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
import streamifyArray from 'streamify-array';
import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter';
import { UrlContainerManager } from '../../src/storage/UrlContainerManager';
const setAcl = async(store: ResourceStore, id: string, permissions: PermissionSet, control: boolean,
access: boolean, def: boolean, agent?: string, agentClass?: 'agent' | 'authenticated'): Promise<void> => {
const acl: string[] = [
'@prefix acl: <http://www.w3.org/ns/auth/acl#>.\n',
'@prefix foaf: <http://xmlns.com/foaf/0.1/>.\n',
'<http://test.com/#auth> a acl:Authorization',
];
for (const perm of [ 'Read', 'Append', 'Write', 'Delete' ]) {
if (permissions[perm.toLowerCase() as keyof PermissionSet]) {
acl.push(`;\n acl:mode acl:${perm}`);
}
}
if (control) {
acl.push(';\n acl:mode acl:Control');
}
if (access) {
acl.push(`;\n acl:accessTo <${id}>`);
}
if (def) {
acl.push(`;\n acl:default <${id}>`);
}
if (agent) {
acl.push(`;\n acl:agent <${agent}>`);
}
if (agentClass) {
acl.push(`;\n acl:agentClass ${agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent'}`);
}
acl.push('.');
const representation = {
data: streamifyArray(acl),
dataType: DATA_TYPE_BINARY,
metadata: {
raw: [],
profiles: [],
contentType: 'text/turtle',
},
};
return store.setRepresentation({ path: `${id}.acl` }, representation);
};
describe('A server with authorization', (): void => {
const bodyParser: BodyParser = new SimpleBodyParser();
const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(),
preferenceParser: new AcceptPreferenceParser(),
bodyParser,
});
const store = new SimpleResourceStore('http://test.com/');
const converter = new CompositeAsyncHandler([
new QuadToTurtleConverter(),
new TurtleToQuadConverter(),
]);
const convertingStore = new RepresentationConvertingStore(store, converter);
const credentialsExtractor = new SimpleCredentialsExtractor();
const permissionsExtractor = new SimplePermissionsExtractor();
const authorizer = new SimpleAclAuthorizer(
new SimpleExtensionAclManager(),
new UrlContainerManager('http://test.com/'),
convertingStore,
);
const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
new SimpleGetOperationHandler(convertingStore),
new SimplePostOperationHandler(convertingStore),
new SimpleDeleteOperationHandler(convertingStore),
new SimplePutOperationHandler(convertingStore),
]);
const responseWriter = new SimpleResponseWriter();
const handler = new AuthenticatedLdpHandler({
requestParser,
credentialsExtractor,
permissionsExtractor,
authorizer,
operationHandler,
responseWriter,
});
it('can create new entries.', async(): Promise<void> => {
await setAcl(convertingStore,
'http://test.com/',
{ read: true, write: true, append: true },
true,
true,
true,
undefined,
'agent');
// POST
let requestUrl = new URL('http://test.com/');
let response: MockResponse<any> = await call(
handler,
requestUrl,
'POST',
{ 'content-type': 'text/turtle' },
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
expect(response.statusCode).toBe(200);
// PUT
requestUrl = new URL('http://test.com/foo/bar');
response = await call(
handler,
requestUrl,
'PUT',
{ 'content-type': 'text/turtle' },
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
expect(response.statusCode).toBe(200);
});
it('can not create new entries if not allowed.', async(): Promise<void> => {
await setAcl(convertingStore,
'http://test.com/',
{ read: true, write: true, append: true },
true,
true,
true,
undefined,
'authenticated');
// POST
let requestUrl = new URL('http://test.com/');
let response: MockResponse<any> = await call(
handler,
requestUrl,
'POST',
{ 'content-type': 'text/turtle' },
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
expect(response.statusCode).toBe(401);
// PUT
requestUrl = new URL('http://test.com/foo/bar');
response = await call(
handler,
requestUrl,
'PUT',
{ 'content-type': 'text/turtle' },
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
expect(response.statusCode).toBe(401);
});
});

View File

@ -81,6 +81,10 @@ describe('ExpressHttpServer', (): void => {
handler.handle = async(): Promise<void> => { handler.handle = async(): Promise<void> => {
throw new Error('dummyError'); throw new Error('dummyError');
}; };
// Prevent test from writing to stderr
jest.spyOn(process.stderr, 'write').mockImplementation((): boolean => true);
const res = await request(server).get('/').expect(500); const res = await request(server).get('/').expect(500);
expect(res.text).toContain('dummyError'); expect(res.text).toContain('dummyError');
}); });

28
test/util/Util.ts Normal file
View File

@ -0,0 +1,28 @@
import { EventEmitter } from 'events';
import { HttpHandler } from '../../src/server/HttpHandler';
import { HttpRequest } from '../../src/server/HttpRequest';
import { IncomingHttpHeaders } from 'http';
import streamifyArray from 'streamify-array';
import { createResponse, MockResponse } from 'node-mocks-http';
export const call = async(handler: HttpHandler, requestUrl: URL, method: string,
headers: IncomingHttpHeaders, data: string[]): Promise<MockResponse<any>> => {
const request = streamifyArray(data) as HttpRequest;
request.url = requestUrl.pathname;
request.method = method;
request.headers = headers;
request.headers.host = requestUrl.host;
const response: MockResponse<any> = createResponse({ eventEmitter: EventEmitter });
const endPromise = new Promise((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
resolve();
});
});
await handler.handleSafe({ request, response });
await endPromise;
return response;
};