fix: Hide internal data by making it auxiliary

This commit is contained in:
Joachim Van Herwegen 2021-09-09 13:51:44 +02:00
parent e31cd38bc5
commit 0271133d33
10 changed files with 46 additions and 30 deletions

View File

@ -56,6 +56,19 @@
"PathBasedAuthorizer:_paths_value": { "@type": "DenyAllAuthorizer" } "PathBasedAuthorizer:_paths_value": { "@type": "DenyAllAuthorizer" }
} }
] ]
},
{
"comment": "Marks the /.internal/ storage container as an auxiliary resource, thereby hiding it from container representations.",
"@id": "urn:solid-server:default:AuxiliaryStrategy",
"RoutingAuxiliaryStrategy:_sources": [
{
"@type": "ComposedAuxiliaryStrategy",
"identifierStrategy": {
"@type": "SuffixAuxiliaryIdentifierStrategy",
"suffix": "/.internal/"
}
}
]
} }
] ]
} }

View File

@ -15,7 +15,7 @@
"@type": "RdfValidator", "@type": "RdfValidator",
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" } "converter": { "@id": "urn:solid-server:default:RepresentationConverter" }
}, },
"isRootRequired": true "requiredInRoot": true
}, },
{ {
"@id": "urn:solid-server:default:AclIdentifierStrategy", "@id": "urn:solid-server:default:AclIdentifierStrategy",

View File

@ -10,11 +10,11 @@ import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'
*/ */
export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy { export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
/** /**
* Whether this auxiliary resource in a root storage container. * Whether the root storage container requires this auxiliary resource to be present.
* If yes, this means they can't be deleted individually from such a container. * If yes, this means they can't be deleted individually from such a container.
* @param identifier - Identifier of the auxiliary resource. * @param identifier - Identifier of the auxiliary resource.
*/ */
isRootRequired: (identifier: ResourceIdentifier) => boolean; isRequiredInRoot: (identifier: ResourceIdentifier) => boolean;
/** /**
* Adds metadata related to this auxiliary resource, * Adds metadata related to this auxiliary resource,

View File

@ -14,14 +14,14 @@ export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
private readonly identifierStrategy: AuxiliaryIdentifierStrategy; private readonly identifierStrategy: AuxiliaryIdentifierStrategy;
private readonly metadataGenerator?: MetadataGenerator; private readonly metadataGenerator?: MetadataGenerator;
private readonly validator?: Validator; private readonly validator?: Validator;
private readonly rootRequired: boolean; private readonly requiredInRoot: boolean;
public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator, public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator,
validator?: Validator, isRootRequired = false) { validator?: Validator, requiredInRoot = false) {
this.identifierStrategy = identifierStrategy; this.identifierStrategy = identifierStrategy;
this.metadataGenerator = metadataGenerator; this.metadataGenerator = metadataGenerator;
this.validator = validator; this.validator = validator;
this.rootRequired = isRootRequired; this.requiredInRoot = requiredInRoot;
} }
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier { public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
@ -40,8 +40,8 @@ export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
return this.identifierStrategy.getAssociatedIdentifier(identifier); return this.identifierStrategy.getAssociatedIdentifier(identifier);
} }
public isRootRequired(): boolean { public isRequiredInRoot(): boolean {
return this.rootRequired; return this.requiredInRoot;
} }
public async addMetadata(metadata: RepresentationMetadata): Promise<void> { public async addMetadata(metadata: RepresentationMetadata): Promise<void> {

View File

@ -18,9 +18,9 @@ export class RoutingAuxiliaryStrategy extends RoutingAuxiliaryIdentifierStrategy
super(sources); super(sources);
} }
public isRootRequired(identifier: ResourceIdentifier): boolean { public isRequiredInRoot(identifier: ResourceIdentifier): boolean {
const source = this.getMatchingSource(identifier); const source = this.getMatchingSource(identifier);
return source.isRootRequired(identifier); return source.isRequiredInRoot(identifier);
} }
public async addMetadata(metadata: RepresentationMetadata): Promise<void> { public async addMetadata(metadata: RepresentationMetadata): Promise<void> {

View File

@ -235,7 +235,8 @@ export class DataAccessorBasedStore implements ResourceStore {
if (this.isRootStorage(metadata)) { if (this.isRootStorage(metadata)) {
throw new MethodNotAllowedHttpError('Cannot delete a root storage container.'); throw new MethodNotAllowedHttpError('Cannot delete a root storage container.');
} }
if (this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier) && this.auxiliaryStrategy.isRootRequired(identifier)) { if (this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier) &&
this.auxiliaryStrategy.isRequiredInRoot(identifier)) {
const associatedIdentifier = this.auxiliaryStrategy.getAssociatedIdentifier(identifier); const associatedIdentifier = this.auxiliaryStrategy.getAssociatedIdentifier(identifier);
const parentMetadata = await this.accessor.getMetadata(associatedIdentifier); const parentMetadata = await this.accessor.getMetadata(associatedIdentifier);
if (this.isRootStorage(parentMetadata)) { if (this.isRootStorage(parentMetadata)) {

View File

@ -1,6 +1,6 @@
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
import fetch from 'cross-fetch'; import fetch from 'cross-fetch';
import { DataFactory, Parser } from 'n3'; import { DataFactory, Parser, Store } from 'n3';
import { joinFilePath, PIM, RDF } from '../../src/'; import { joinFilePath, PIM, RDF } from '../../src/';
import type { App } from '../../src/'; import type { App } from '../../src/';
import { LDP } from '../../src/util/Vocabularies'; import { LDP } from '../../src/util/Vocabularies';
@ -90,9 +90,11 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
const parser = new Parser({ baseIRI: baseUrl }); const parser = new Parser({ baseIRI: baseUrl });
const quads = parser.parse(await response.text()); const quads = parser.parse(await response.text());
expect(quads.some((entry): boolean => entry.equals( const store = new Store(quads);
quad(namedNode(baseUrl), RDF.terms.type, LDP.terms.Container), expect(store.countQuads(namedNode(baseUrl), RDF.terms.type, LDP.terms.Container, null)).toBe(1);
))).toBe(true); const contains = store.getObjects(namedNode(baseUrl), LDP.terms.contains, null);
expect(contains).toHaveLength(1);
expect(contains[0].value).toBe(`${baseUrl}index.html`);
}); });
it('can add a document to the store, read it and delete it.', async(): Promise<void> => { it('can add a document to the store, read it and delete it.', async(): Promise<void> => {

View File

@ -45,8 +45,8 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier); expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier);
}); });
it('returns the injected value for isRootRequired.', async(): Promise<void> => { it('returns the injected value for isRequiredInRoot.', async(): Promise<void> => {
expect(strategy.isRootRequired()).toBe(true); expect(strategy.isRequiredInRoot()).toBe(true);
}); });
it('adds metadata through the MetadataGenerator.', async(): Promise<void> => { it('adds metadata through the MetadataGenerator.', async(): Promise<void> => {
@ -63,9 +63,9 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
expect(validator.handleSafe).toHaveBeenLastCalledWith(representation); expect(validator.handleSafe).toHaveBeenLastCalledWith(representation);
}); });
it('defaults isRootRequired to false.', async(): Promise<void> => { it('defaults isRequiredInRoot to false.', async(): Promise<void> => {
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator); strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator);
expect(strategy.isRootRequired()).toBe(false); expect(strategy.isRequiredInRoot()).toBe(false);
}); });
it('does not add metadata or validate if the corresponding classes are not injected.', async(): Promise<void> => { it('does not add metadata or validate if the corresponding classes are not injected.', async(): Promise<void> => {

View File

@ -27,7 +27,7 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
return { path: identifier.path.slice(0, -this.suffix.length) }; return { path: identifier.path.slice(0, -this.suffix.length) };
} }
public isRootRequired(): boolean { public isRequiredInRoot(): boolean {
return true; return true;
} }
@ -77,13 +77,13 @@ describe('A RoutingAuxiliaryStrategy', (): void => {
expect(sources[1].addMetadata).toHaveBeenLastCalledWith(metadata); expect(sources[1].addMetadata).toHaveBeenLastCalledWith(metadata);
}); });
it('#isRootRequired returns the result of the correct source.', async(): Promise<void> => { it('#isRequiredInRoot returns the result of the correct source.', async(): Promise<void> => {
sources[0].isRootRequired = jest.fn(); sources[0].isRequiredInRoot = jest.fn();
sources[1].isRootRequired = jest.fn(); sources[1].isRequiredInRoot = jest.fn();
strategy.isRootRequired(dummy2Id); strategy.isRequiredInRoot(dummy2Id);
expect(sources[0].isRootRequired).toHaveBeenCalledTimes(0); expect(sources[0].isRequiredInRoot).toHaveBeenCalledTimes(0);
expect(sources[1].isRootRequired).toHaveBeenCalledTimes(1); expect(sources[1].isRequiredInRoot).toHaveBeenCalledTimes(1);
expect(sources[1].isRootRequired).toHaveBeenLastCalledWith(dummy2Id); expect(sources[1].isRequiredInRoot).toHaveBeenLastCalledWith(dummy2Id);
}); });
it('#validates using the correct validator.', async(): Promise<void> => { it('#validates using the correct validator.', async(): Promise<void> => {

View File

@ -107,7 +107,7 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
return { path: identifier.path.slice(0, -this.suffix.length) }; return { path: identifier.path.slice(0, -this.suffix.length) };
} }
public isRootRequired(): boolean { public isRequiredInRoot(): boolean {
return false; return false;
} }
@ -602,7 +602,7 @@ describe('A DataAccessorBasedStore', (): void => {
storageMetadata.add(RDF.type, PIM.terms.Storage); storageMetadata.add(RDF.type, PIM.terms.Storage);
accessor.data[`${root}container/`] = new BasicRepresentation(representation.data, storageMetadata); accessor.data[`${root}container/`] = new BasicRepresentation(representation.data, storageMetadata);
accessor.data[`${root}container/.dummy`] = representation; accessor.data[`${root}container/.dummy`] = representation;
auxiliaryStrategy.isRootRequired = jest.fn().mockReturnValue(true); auxiliaryStrategy.isRequiredInRoot = jest.fn().mockReturnValue(true);
const result = store.deleteResource({ path: `${root}container/.dummy` }); const result = store.deleteResource({ path: `${root}container/.dummy` });
await expect(result).rejects.toThrow(MethodNotAllowedHttpError); await expect(result).rejects.toThrow(MethodNotAllowedHttpError);
await expect(result).rejects.toThrow( await expect(result).rejects.toThrow(
@ -648,7 +648,7 @@ describe('A DataAccessorBasedStore', (): void => {
const storageMetadata = new RepresentationMetadata(representation.metadata); const storageMetadata = new RepresentationMetadata(representation.metadata);
accessor.data[`${root}container/`] = new BasicRepresentation(representation.data, storageMetadata); accessor.data[`${root}container/`] = new BasicRepresentation(representation.data, storageMetadata);
accessor.data[`${root}container/.dummy`] = representation; accessor.data[`${root}container/.dummy`] = representation;
auxiliaryStrategy.isRootRequired = jest.fn().mockReturnValue(true); auxiliaryStrategy.isRequiredInRoot = jest.fn().mockReturnValue(true);
await expect(store.deleteResource({ path: `${root}container/.dummy` })).resolves.toEqual([ await expect(store.deleteResource({ path: `${root}container/.dummy` })).resolves.toEqual([
{ path: `${root}container/.dummy` }, { path: `${root}container/.dummy` },
{ path: `${root}container/` }, { path: `${root}container/` },