refactor: Replace getParentContainer util function with ContainerManager

This commit is contained in:
Joachim Van Herwegen
2020-12-09 14:46:41 +01:00
parent 9c080c2101
commit f0db9e501f
18 changed files with 139 additions and 136 deletions

View File

@@ -35,6 +35,7 @@ import {
RawBodyParser,
RepresentationConvertingStore,
SequenceHandler,
SingleRootIdentifierStrategy,
SingleThreadedResourceLocker,
SlugParser,
SparqlUpdatePatchHandler,
@@ -60,7 +61,7 @@ export const getRootFilePath = (subfolder: string): string => join(__dirname, '.
* @returns The data accessor based store.
*/
export const getDataAccessorStore = (base: string, dataAccessor: DataAccessor): DataAccessorBasedStore =>
new DataAccessorBasedStore(dataAccessor, base);
new DataAccessorBasedStore(dataAccessor, base, new SingleRootIdentifierStrategy(base));
/**
* Gives an in memory resource store based on (default) base url.
@@ -175,7 +176,7 @@ export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequ
* @returns The acl authorizer.
*/
export const getWebAclAuthorizer = (store: ResourceStore, aclManager = new UrlBasedAclManager()): WebAclAuthorizer =>
new WebAclAuthorizer(aclManager, store);
new WebAclAuthorizer(aclManager, store, new SingleRootIdentifierStrategy(BASE));
/**
* Returns a component instantiated from a Components.js configuration.

View File

@@ -6,6 +6,7 @@ import { DataAccessorBasedStore } from '../../src/storage/DataAccessorBasedStore
import { LockingResourceStore } from '../../src/storage/LockingResourceStore';
import type { ResourceStore } from '../../src/storage/ResourceStore';
import { APPLICATION_OCTET_STREAM } from '../../src/util/ContentTypes';
import { SingleRootIdentifierStrategy } from '../../src/util/identifiers/SingleRootIdentifierStrategy';
import type { ExpiringResourceLocker } from '../../src/util/locking/ExpiringResourceLocker';
import type { ResourceLocker } from '../../src/util/locking/ResourceLocker';
import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker';
@@ -25,7 +26,7 @@ describe('A LockingResourceStore', (): void => {
const base = 'http://test.com/';
path = `${base}path`;
source = new DataAccessorBasedStore(new InMemoryDataAccessor(base), base);
source = new DataAccessorBasedStore(new InMemoryDataAccessor(base), base, new SingleRootIdentifierStrategy(base));
locker = new SingleThreadedResourceLocker();
expiringLocker = new WrappedExpiringResourceLocker(locker, 1000);

View File

@@ -1,5 +1,6 @@
import { SparqlDataAccessor } from '../../src/storage/accessors/SparqlDataAccessor';
import { INTERNAL_QUADS } from '../../src/util/ContentTypes';
import { SingleRootIdentifierStrategy } from '../../src/util/identifiers/SingleRootIdentifierStrategy';
import { DataAccessorBasedConfig } from '../configs/DataAccessorBasedConfig';
import { BASE } from '../configs/Util';
import { describeIf, FileTestHelper } from '../util/TestHelpers';
@@ -7,7 +8,7 @@ import { describeIf, FileTestHelper } from '../util/TestHelpers';
describeIf('docker', 'a server with a SPARQL endpoint as storage', (): void => {
describe('without acl', (): void => {
const config = new DataAccessorBasedConfig(BASE,
new SparqlDataAccessor('http://localhost:4000/sparql', BASE),
new SparqlDataAccessor('http://localhost:4000/sparql', BASE, new SingleRootIdentifierStrategy(BASE)),
INTERNAL_QUADS);
const handler = config.getHttpHandler();
const fileHelper = new FileTestHelper(handler, new URL(BASE));

View File

@@ -8,9 +8,10 @@ import type { Representation } from '../../../src/ldp/representation/Representat
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
import type { ResourceStore } from '../../../src/storage/ResourceStore';
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
import { InternalServerError } from '../../../src/util/errors/InternalServerError';
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError';
import { getParentContainer } from '../../../src/util/PathUtil';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
const nn = namedNode;
@@ -25,6 +26,8 @@ describe('A WebAclAuthorizer', (): void => {
getAclConstrainedResource: async(id: ResourceIdentifier): Promise<ResourceIdentifier> =>
!id.path.endsWith('.acl') ? id : { path: id.path.slice(0, -4) },
};
let store: ResourceStore;
const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/');
let permissions: PermissionSet;
let credentials: Credentials;
let identifier: ResourceIdentifier;
@@ -37,132 +40,119 @@ describe('A WebAclAuthorizer', (): void => {
};
credentials = {};
identifier = { path: 'http://test.com/foo' };
store = {
getRepresentation: jest.fn(),
} as any;
authorizer = new WebAclAuthorizer(aclManager, store, identifierStrategy);
});
it('handles all inputs.', async(): Promise<void> => {
authorizer = new WebAclAuthorizer(aclManager, null as any);
authorizer = new WebAclAuthorizer(aclManager, null as any, identifierStrategy);
await expect(authorizer.canHandle({} as any)).resolves.toBeUndefined();
});
it('allows access if the acl file allows all agents.', async(): Promise<void> => {
const store = {
getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation),
} as unknown as ResourceStore;
authorizer = new WebAclAuthorizer(aclManager, store);
store.getRepresentation = async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation);
await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined();
});
it('allows access if there is a parent acl file allowing all agents.', async(): Promise<void> => {
const store = {
async getRepresentation(id: ResourceIdentifier): Promise<Representation> {
if (id.path.endsWith('foo.acl')) {
throw new NotFoundHttpError();
}
return {
data: streamifyArray([
quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')),
quad(nn('auth'), nn(`${acl}default`), nn(getParentContainer(identifier).path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]),
} as Representation;
},
} as unknown as ResourceStore;
authorizer = new WebAclAuthorizer(aclManager, store);
store.getRepresentation = async(id: ResourceIdentifier): Promise<Representation> => {
if (id.path.endsWith('foo.acl')) {
throw new NotFoundHttpError();
}
return {
data: streamifyArray([
quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')),
quad(nn('auth'), nn(`${acl}default`), nn(identifierStrategy.getParentContainer(identifier).path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]),
} as Representation;
};
await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined();
});
it('allows access to authorized agents if the acl files allows all authorized users.', async(): Promise<void> => {
const store = {
getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation),
} as unknown as ResourceStore;
authorizer = new WebAclAuthorizer(aclManager, store);
store.getRepresentation = async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation);
credentials.webId = 'http://test.com/user';
await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined();
});
it('errors if authorization is required but the agent is not authorized.', async(): Promise<void> => {
const store = {
getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation),
} as unknown as ResourceStore;
authorizer = new WebAclAuthorizer(aclManager, store);
store.getRepresentation = async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/AuthenticatedAgent')),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation);
await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(UnauthorizedHttpError);
});
it('allows access to specific agents if the acl files identifies them.', async(): Promise<void> => {
credentials.webId = 'http://test.com/user';
const store = {
getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation),
} as unknown as ResourceStore;
authorizer = new WebAclAuthorizer(aclManager, store);
store.getRepresentation = async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation);
await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined();
});
it('errors if a specific agents wants to access files not assigned to them.', async(): Promise<void> => {
credentials.webId = 'http://test.com/user';
const store = {
getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agent`), nn('http://test.com/differentUser')),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation),
} as unknown as ResourceStore;
authorizer = new WebAclAuthorizer(aclManager, store);
store.getRepresentation = async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agent`), nn('http://test.com/differentUser')),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation);
await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError);
});
it('allows access to the acl file if control is allowed.', async(): Promise<void> => {
credentials.webId = 'http://test.com/user';
identifier.path = 'http://test.com/foo';
const store = {
getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)),
]) } as Representation),
} as unknown as ResourceStore;
store.getRepresentation = async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)),
]) } as Representation);
const aclIdentifier = await aclManager.getAclDocument(identifier);
authorizer = new WebAclAuthorizer(aclManager, store);
await expect(authorizer.handle({ identifier: aclIdentifier, permissions, credentials })).resolves.toBeUndefined();
});
it('errors if an agent tries to edit the acl file without control permissions.', async(): Promise<void> => {
credentials.webId = 'http://test.com/user';
identifier.path = 'http://test.com/foo';
const store = {
getRepresentation: async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation),
} as unknown as ResourceStore;
store.getRepresentation = async(): Promise<Representation> => ({ data: streamifyArray([
quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation);
identifier = await aclManager.getAclDocument(identifier);
authorizer = new WebAclAuthorizer(aclManager, store);
await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError);
});
it('passes errors of the ResourceStore along.', async(): Promise<void> => {
const store = {
async getRepresentation(): Promise<Representation> {
throw new Error('TEST!');
},
} as unknown as ResourceStore;
authorizer = new WebAclAuthorizer(aclManager, store);
store.getRepresentation = async(): Promise<Representation> => {
throw new Error('TEST!');
};
await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow('TEST!');
});
it('errors if the root container has no corresponding acl document.', async(): Promise<void> => {
store.getRepresentation = async(): Promise<Representation> => {
throw new NotFoundHttpError();
};
const promise = authorizer.handle({ identifier, permissions, credentials });
await expect(promise).rejects.toThrow('No ACL document found for root container');
await expect(promise).rejects.toThrow(InternalServerError);
});
});

View File

@@ -14,6 +14,7 @@ import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAll
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
import type { Guarded } from '../../../src/util/GuardedStream';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
import * as quadUtil from '../../../src/util/QuadUtil';
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../../../src/util/UriConstants';
@@ -71,6 +72,7 @@ describe('A DataAccessorBasedStore', (): void => {
let store: DataAccessorBasedStore;
let accessor: SimpleDataAccessor;
const root = 'http://test.com/';
const identifierStrategy = new SingleRootIdentifierStrategy(root);
let containerMetadata: RepresentationMetadata;
let representation: Representation;
const resourceData = 'text';
@@ -78,7 +80,7 @@ describe('A DataAccessorBasedStore', (): void => {
beforeEach(async(): Promise<void> => {
accessor = new SimpleDataAccessor();
store = new DataAccessorBasedStore(accessor, root);
store = new DataAccessorBasedStore(accessor, root, identifierStrategy);
containerMetadata = new RepresentationMetadata(
{ [RDF.type]: [ DataFactory.namedNode(LDP.Container), DataFactory.namedNode(LDP.BasicContainer) ]},

View File

@@ -2,10 +2,12 @@ import type { Patch } from '../../../src/ldp/http/Patch';
import type { Representation } from '../../../src/ldp/representation/Representation';
import { MonitoringStore } from '../../../src/storage/MonitoringStore';
import type { ResourceStore } from '../../../src/storage/ResourceStore';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
describe('A MonitoringStore', (): void => {
let store: MonitoringStore;
let source: ResourceStore;
const identifierStrategy = new SingleRootIdentifierStrategy('http://example.org/');
let changedCallback: () => void;
beforeEach(async(): Promise<void> => {
@@ -16,7 +18,7 @@ describe('A MonitoringStore', (): void => {
deleteResource: jest.fn(async(): Promise<any> => undefined),
modifyResource: jest.fn(async(): Promise<any> => undefined),
};
store = new MonitoringStore(source);
store = new MonitoringStore(source, identifierStrategy);
changedCallback = jest.fn();
store.on('changed', changedCallback);
});

View File

@@ -12,6 +12,7 @@ import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import type { Guarded } from '../../../../src/util/GuardedStream';
import { SingleRootIdentifierStrategy } from '../../../../src/util/identifiers/SingleRootIdentifierStrategy';
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
import { toNamedNode } from '../../../../src/util/UriUtil';
@@ -30,6 +31,7 @@ const simplifyQuery = (query: string | string[]): string => {
describe('A SparqlDataAccessor', (): void => {
const endpoint = 'http://test.com/sparql';
const base = 'http://test.com/';
const identifierStrategy = new SingleRootIdentifierStrategy(base);
let accessor: SparqlDataAccessor;
let metadata: RepresentationMetadata;
let data: Guarded<Readable>;
@@ -64,7 +66,7 @@ describe('A SparqlDataAccessor', (): void => {
}));
// This needs to be last so the fetcher can be mocked first
accessor = new SparqlDataAccessor(endpoint, base);
accessor = new SparqlDataAccessor(endpoint, base, identifierStrategy);
});
it('can only handle quad data.', async(): Promise<void> => {

View File

@@ -2,7 +2,6 @@ import {
decodeUriPathComponents,
encodeUriPathComponents,
ensureTrailingSlash,
getParentContainer,
toCanonicalUriPath,
} from '../../../src/util/PathUtil';
@@ -29,16 +28,4 @@ describe('PathUtil', (): void => {
expect(encodeUriPathComponents('/a%20path&/name')).toEqual('/a%2520path%26/name');
});
});
describe('#getParentContainer', (): void => {
it('returns the parent URl for a single call.', async(): Promise<void> => {
expect(getParentContainer({ path: 'http://test.com/foo/bar' })).toEqual({ path: 'http://test.com/foo/' });
expect(getParentContainer({ path: 'http://test.com/foo/bar/' })).toEqual({ path: 'http://test.com/foo/' });
});
it('errors when the root of an URl is reached that does not match the input root.', async(): Promise<void> => {
expect((): any => getParentContainer({ path: 'http://test.com/' }))
.toThrow('Resource http://test.com/ does not have a parent container');
});
});
});