feat: Generalize AclManager to AuxiliaryManager

This commit is contained in:
Joachim Van Herwegen 2021-01-25 17:13:48 +01:00
parent d6cdd7dbdf
commit 758f5ed083
22 changed files with 106 additions and 192 deletions

View File

@ -2,14 +2,34 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
"@graph": [ "@graph": [
{ {
"@id": "urn:solid-server:default:AclManager", "@id": "urn:solid-server:default:AclIdentifierStrategy",
"@type": "UrlBasedAclManager" "@type": "SuffixAuxiliaryIdentifierStrategy",
"SuffixAuxiliaryIdentifierStrategy:_suffix": ".acl"
}, },
{
"@id": "urn:solid-server:default:AclStrategy",
"@type": "ComposedAuxiliaryStrategy",
"ComposedAuxiliaryStrategy:_identifierStrategy": {
"@id": "urn:solid-server:default:AclIdentifierStrategy"
},
"ComposedAuxiliaryStrategy:_metadataGenerator": {
"@type": "LinkMetadataGenerator",
"LinkMetadataGenerator:_link": "http://www.w3.org/ns/auth/acl#accessControl",
"LinkMetadataGenerator:_identifierStrategy": {
"@id": "urn:solid-server:default:AclIdentifierStrategy"
}
},
"ComposedAuxiliaryStrategy:_validator": {
"@id": "urn:solid-server:default:RdfValidator"
},
"ComposedAuxiliaryStrategy:_isRootRequired": true
},
{ {
"@id": "urn:solid-server:default:AclAuthorizer", "@id": "urn:solid-server:default:AclAuthorizer",
"@type": "WebAclAuthorizer", "@type": "WebAclAuthorizer",
"WebAclAuthorizer:_aclManager": { "WebAclAuthorizer:_aclStrategy": {
"@id": "urn:solid-server:default:AclManager" "@id": "urn:solid-server:default:AclIdentifierStrategy"
}, },
"WebAclAuthorizer:_resourceStore": { "WebAclAuthorizer:_resourceStore": {
"@id": "urn:solid-server:default:ResourceStore" "@id": "urn:solid-server:default:ResourceStore"

View File

@ -28,8 +28,8 @@
"AclInitializer:_settings_store": { "AclInitializer:_settings_store": {
"@id": "urn:solid-server:default:ResourceStore" "@id": "urn:solid-server:default:ResourceStore"
}, },
"AclInitializer:_settings_aclManager": { "AclInitializer:_settings_aclStrategy": {
"@id": "urn:solid-server:default:AclManager" "@id": "urn:solid-server:default:AclIdentifierStrategy"
}, },
"AclInitializer:_settings_baseUrl": { "AclInitializer:_settings_baseUrl": {
"@id": "urn:solid-server:default:variable:baseUrl" "@id": "urn:solid-server:default:variable:baseUrl"

View File

@ -42,8 +42,8 @@
}, },
{ {
"@type": "AclLinkMetadataWriter", "@type": "AclLinkMetadataWriter",
"AclLinkMetadataWriter:_aclManager": { "AclLinkMetadataWriter:_aclStrategy": {
"@id": "urn:solid-server:default:AclManager" "@id": "urn:solid-server:default:AclIdentifierStrategy"
} }
} }
] ]

View File

@ -82,11 +82,11 @@
"@type": "WaterfallHandler", "@type": "WaterfallHandler",
"WaterfallHandler:_handlers": [ "WaterfallHandler:_handlers": [
{ {
"@id": "urn:solid-server:default:IndexConverter", "@id": "urn:solid-server:default:IndexConverter"
}, },
{ {
"@type": "IfNeededConverter", "@type": "IfNeededConverter",
"comment": "Only continue converting if the requester cannot accept the available content type", "comment": "Only continue converting if the requester cannot accept the available content type"
}, },
{ {
"@id": "urn:solid-server:default:ContentTypeReplacer" "@id": "urn:solid-server:default:ContentTypeReplacer"
@ -101,6 +101,14 @@
"@id": "urn:solid-server:default:RdfRepresentationConverter" "@id": "urn:solid-server:default:RdfRepresentationConverter"
} }
] ]
},
{
"@id": "urn:solid-server:default:RdfValidator",
"@type": "RdfValidator",
"RdfValidator:_converter": {
"@id": "urn:solid-server:default:RepresentationConverter"
}
} }
] ]
} }

View File

@ -1,41 +0,0 @@
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
/**
* Handles where acl resources are stored.
*
* Solid, §4.3.3: "A given Solid resource MUST NOT be directly associated with more than one ACL auxiliary resource.
* A given ACL auxiliary resource MUST NOT be directly associated with more than one Solid resource."
* https://solid.github.io/specification/protocol#auxiliary-resources-reserved
*/
export interface AclManager {
/**
* Returns the identifier of the acl resource corresponding to the given resource.
* This does not guarantee that this acl resource exists.
* In the case the input is already an acl resource that will also be the response.
* @param id - The ResourceIdentifier of which we need the corresponding acl resource.
*
* @returns The ResourceIdentifier of the corresponding acl resource.
*/
getAclDocument: (id: ResourceIdentifier) => Promise<ResourceIdentifier>;
/**
* Checks if the input identifier corresponds to an acl resource.
* This does not check if that acl resource exists,
* only if the identifier indicates that there could be an acl resource there.
* @param id - Identifier to check.
*
* @returns true if the input identifier points to an acl resource.
*/
isAclDocument: (id: ResourceIdentifier) => Promise<boolean>;
/**
* Returns the identifier of the resource on which the acl constraints are placed.
* In general, this is the resource identifier when the input is a normal resource,
* or the non-acl version if the input is an acl resource.
* This does not guarantee that this resource exists.
* @param aclId - Identifier of the acl resource.
*
* @returns The ResourceIdentifier of the corresponding resource.
*/
getAclConstrainedResource: (id: ResourceIdentifier) => Promise<ResourceIdentifier>;
}

View File

@ -1,26 +0,0 @@
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import type { AclManager } from './AclManager';
/**
* Generates acl URIs by adding an .acl file extension.
*
* Needs to be updated according to issue #113.
*/
export class UrlBasedAclManager implements AclManager {
public async getAclDocument(id: ResourceIdentifier): Promise<ResourceIdentifier> {
return await this.isAclDocument(id) ? id : { path: `${id.path}.acl` };
}
public async isAclDocument(id: ResourceIdentifier): Promise<boolean> {
return /\.acl\/?/u.test(id.path);
}
public async getAclConstrainedResource(id: ResourceIdentifier): Promise<ResourceIdentifier> {
if (!await this.isAclDocument(id)) {
return id;
}
// Slice off `.acl`
return { path: id.path.slice(0, -4) };
}
}

View File

@ -1,6 +1,7 @@
import type { Quad, Term } from 'n3'; import type { Quad, Term } from 'n3';
import { Store } from 'n3'; import { Store } from 'n3';
import type { Credentials } from '../authentication/Credentials'; import type { Credentials } from '../authentication/Credentials';
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { PermissionSet } from '../ldp/permissions/PermissionSet'; import type { PermissionSet } from '../ldp/permissions/PermissionSet';
import type { Representation } from '../ldp/representation/Representation'; import type { Representation } from '../ldp/representation/Representation';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
@ -12,7 +13,6 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError'; import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy'; import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { ACL, FOAF } from '../util/Vocabularies'; import { ACL, FOAF } from '../util/Vocabularies';
import type { AclManager } from './AclManager';
import type { AuthorizerArgs } from './Authorizer'; import type { AuthorizerArgs } from './Authorizer';
import { Authorizer } from './Authorizer'; import { Authorizer } from './Authorizer';
@ -24,13 +24,14 @@ import { Authorizer } from './Authorizer';
export class WebAclAuthorizer extends Authorizer { export class WebAclAuthorizer extends Authorizer {
protected readonly logger = getLoggerFor(this); protected readonly logger = getLoggerFor(this);
private readonly aclManager: AclManager; private readonly aclStrategy: AuxiliaryIdentifierStrategy;
private readonly resourceStore: ResourceStore; private readonly resourceStore: ResourceStore;
private readonly identifierStrategy: IdentifierStrategy; private readonly identifierStrategy: IdentifierStrategy;
public constructor(aclManager: AclManager, resourceStore: ResourceStore, identifierStrategy: IdentifierStrategy) { public constructor(aclStrategy: AuxiliaryIdentifierStrategy, resourceStore: ResourceStore,
identifierStrategy: IdentifierStrategy) {
super(); super();
this.aclManager = aclManager; this.aclStrategy = aclStrategy;
this.resourceStore = resourceStore; this.resourceStore = resourceStore;
this.identifierStrategy = identifierStrategy; this.identifierStrategy = identifierStrategy;
} }
@ -44,7 +45,7 @@ export class WebAclAuthorizer extends Authorizer {
// Solid, §4.3.3: "To discover, read, create, or modify an ACL auxiliary resource, an acl:agent MUST // Solid, §4.3.3: "To discover, read, create, or modify an ACL auxiliary resource, an acl:agent MUST
// have acl:Control privileges per the ACL inheritance algorithm on the resource directly associated with it." // have acl:Control privileges per the ACL inheritance algorithm on the resource directly associated with it."
// https://solid.github.io/specification/protocol#auxiliary-resources-reserved // https://solid.github.io/specification/protocol#auxiliary-resources-reserved
const modes = await this.aclManager.isAclDocument(identifier) ? const modes = this.aclStrategy.isAuxiliaryIdentifier(identifier) ?
[ 'control' ] : [ 'control' ] :
(Object.keys(permissions) as (keyof PermissionSet)[]).filter((key): boolean => permissions[key]); (Object.keys(permissions) as (keyof PermissionSet)[]).filter((key): boolean => permissions[key]);
@ -141,12 +142,13 @@ export class WebAclAuthorizer extends Authorizer {
private async getAclRecursive(id: ResourceIdentifier, recurse?: boolean): Promise<Store> { private async getAclRecursive(id: ResourceIdentifier, recurse?: boolean): Promise<Store> {
this.logger.debug(`Trying to read the direct ACL document of ${id.path}`); this.logger.debug(`Trying to read the direct ACL document of ${id.path}`);
try { try {
const acl = await this.aclManager.getAclDocument(id); const isAcl = this.aclStrategy.isAuxiliaryIdentifier(id);
const acl = isAcl ? id : this.aclStrategy.getAuxiliaryIdentifier(id);
this.logger.debug(`Trying to read the ACL document ${acl.path}`); this.logger.debug(`Trying to read the ACL document ${acl.path}`);
const data = await this.resourceStore.getRepresentation(acl, { type: { [INTERNAL_QUADS]: 1 }}); const data = await this.resourceStore.getRepresentation(acl, { type: { [INTERNAL_QUADS]: 1 }});
this.logger.info(`Reading ACL statements from ${acl.path}`); this.logger.info(`Reading ACL statements from ${acl.path}`);
const resourceId = await this.aclManager.getAclConstrainedResource(id); const resourceId = isAcl ? this.aclStrategy.getAssociatedIdentifier(id) : id;
return this.filterData(data, recurse ? ACL.default : ACL.accessTo, resourceId.path); return this.filterData(data, recurse ? ACL.default : ACL.accessTo, resourceId.path);
} catch (error: unknown) { } catch (error: unknown) {
if (NotFoundHttpError.isInstance(error)) { if (NotFoundHttpError.isInstance(error)) {

View File

@ -9,9 +9,7 @@ export * from './authentication/UnsecureWebIdExtractor';
// Authorization // Authorization
export * from './authorization/AllowEverythingAuthorizer'; export * from './authorization/AllowEverythingAuthorizer';
export * from './authorization/AclManager';
export * from './authorization/Authorizer'; export * from './authorization/Authorizer';
export * from './authorization/UrlBasedAclManager';
export * from './authorization/WebAclAuthorizer'; export * from './authorization/WebAclAuthorizer';
// Init // Init

View File

@ -1,5 +1,5 @@
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
import type { AclManager } from '../authorization/AclManager'; import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation'; import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
@ -17,19 +17,19 @@ const DEFAULT_ACL_PATH = joinFilePath(__dirname, '../../templates/root/.acl');
export class AclInitializer extends Initializer { export class AclInitializer extends Initializer {
protected readonly logger = getLoggerFor(this); protected readonly logger = getLoggerFor(this);
private readonly store: ResourceStore; private readonly store: ResourceStore;
private readonly aclManager: AclManager; private readonly aclStrategy: AuxiliaryIdentifierStrategy;
private readonly root: ResourceIdentifier; private readonly root: ResourceIdentifier;
private readonly aclPath: string; private readonly aclPath: string;
public constructor(settings: { public constructor(settings: {
store: ResourceStore; store: ResourceStore;
aclManager: AclManager; aclStrategy: AuxiliaryIdentifierStrategy;
baseUrl: string; baseUrl: string;
aclPath?: string; aclPath?: string;
}) { }) {
super(); super();
this.store = settings.store; this.store = settings.store;
this.aclManager = settings.aclManager; this.aclStrategy = settings.aclStrategy;
this.root = { path: ensureTrailingSlash(settings.baseUrl) }; this.root = { path: ensureTrailingSlash(settings.baseUrl) };
this.aclPath = settings.aclPath ?? DEFAULT_ACL_PATH; this.aclPath = settings.aclPath ?? DEFAULT_ACL_PATH;
} }
@ -38,7 +38,7 @@ export class AclInitializer extends Initializer {
// The associated ACL document MUST include an authorization policy with acl:Control access privilege." // The associated ACL document MUST include an authorization policy with acl:Control access privilege."
// https://solid.github.io/specification/protocol#storage // https://solid.github.io/specification/protocol#storage
public async handle(): Promise<void> { public async handle(): Promise<void> {
const rootAcl = await this.aclManager.getAclDocument(this.root); const rootAcl = this.aclStrategy.getAuxiliaryIdentifier(this.root);
if (await containsResource(this.store, rootAcl)) { if (await containsResource(this.store, rootAcl)) {
this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`); this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`);
} else { } else {

View File

@ -10,10 +10,11 @@ import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'
*/ */
export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy { export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
/** /**
* Whether this auxiliary resource can be deleted when it's in a root storage container. * Whether this auxiliary resource in a root storage 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.
*/ */
requiresRootAuxiliary: (identifier: ResourceIdentifier) => boolean; isRootRequired: (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 canDelete: boolean; private readonly rootRequired: boolean;
public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator, public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator,
validator?: Validator, canDeleteRoot = false) { validator?: Validator, isRootRequired = false) {
this.identifierStrategy = identifierStrategy; this.identifierStrategy = identifierStrategy;
this.metadataGenerator = metadataGenerator; this.metadataGenerator = metadataGenerator;
this.validator = validator; this.validator = validator;
this.canDelete = canDeleteRoot; this.rootRequired = isRootRequired;
} }
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 requiresRootAuxiliary(): boolean { public isRootRequired(): boolean {
return this.canDelete; return this.rootRequired;
} }
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 requiresRootAuxiliary(identifier: ResourceIdentifier): boolean { public isRootRequired(identifier: ResourceIdentifier): boolean {
const source = this.getMatchingSource(identifier); const source = this.getMatchingSource(identifier);
return source.requiresRootAuxiliary(identifier); return source.isRootRequired(identifier);
} }
public async addMetadata(metadata: RepresentationMetadata): Promise<void> { public async addMetadata(metadata: RepresentationMetadata): Promise<void> {

View File

@ -34,8 +34,4 @@ export class SuffixAuxiliaryIdentifierStrategy implements AuxiliaryIdentifierStr
} }
return { path: identifier.path.slice(0, -this.suffix.length) }; return { path: identifier.path.slice(0, -this.suffix.length) };
} }
public canDeleteRoot(): boolean {
return true;
}
} }

View File

@ -1,6 +1,6 @@
import type { AclManager } from '../../../authorization/AclManager';
import type { HttpResponse } from '../../../server/HttpResponse'; import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/HeaderUtil'; import { addHeader } from '../../../util/HeaderUtil';
import type { AuxiliaryIdentifierStrategy } from '../../auxiliary/AuxiliaryIdentifierStrategy';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataWriter } from './MetadataWriter'; import { MetadataWriter } from './MetadataWriter';
@ -9,17 +9,17 @@ import { MetadataWriter } from './MetadataWriter';
* The `rel` parameter can be used if a different `rel` value is needed (such as http://www.w3.org/ns/solid/terms#acl). * 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 { export class AclLinkMetadataWriter extends MetadataWriter {
private readonly aclManager: AclManager; private readonly aclStrategy: AuxiliaryIdentifierStrategy;
private readonly rel: string; private readonly rel: string;
public constructor(aclManager: AclManager, rel = 'acl') { public constructor(aclStrategy: AuxiliaryIdentifierStrategy, rel = 'acl') {
super(); super();
this.aclManager = aclManager; this.aclStrategy = aclStrategy;
this.rel = rel; this.rel = rel;
} }
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> { public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
const identifier = await this.aclManager.getAclDocument({ path: input.metadata.identifier.value }); const identifier = this.aclStrategy.getAuxiliaryIdentifier({ path: input.metadata.identifier.value });
addHeader(input.response, 'Link', `<${identifier.path}>; rel="${this.rel}"`); addHeader(input.response, 'Link', `<${identifier.path}>; rel="${this.rel}"`);
} }
} }

View File

@ -1,6 +1,7 @@
{ {
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
"import": [ "import": [
"files-scs:config/presets/acl.json",
"files-scs:config/presets/http.json", "files-scs:config/presets/http.json",
"files-scs:config/presets/ldp/credentials-extractor.json", "files-scs:config/presets/ldp/credentials-extractor.json",
"files-scs:config/presets/ldp/metadata-handler.json", "files-scs:config/presets/ldp/metadata-handler.json",
@ -46,11 +47,6 @@
"PassthroughStore:_source": { "PassthroughStore:_source": {
"@id": "urn:solid-server:default:MemoryResourceStore" "@id": "urn:solid-server:default:MemoryResourceStore"
} }
},
{
"@id": "urn:solid-server:default:AclManager",
"@type": "UrlBasedAclManager",
"comment": "Needed for AclLinkMetadataWriter"
} }
] ]
} }

View File

@ -1,34 +0,0 @@
import { UrlBasedAclManager } from '../../../src/authorization/UrlBasedAclManager';
describe('An UrlBasedAclManager', (): void => {
const manager = new UrlBasedAclManager();
describe('#getAcl', (): void => {
it('generates acl URLs by adding an .acl extension.', async(): Promise<void> => {
await expect(manager.getAclDocument({ path: '/foo/bar' })).resolves.toEqual({ path: '/foo/bar.acl' });
});
it('returns the identifier if the input is already an acl resource.', async(): Promise<void> => {
await expect(manager.getAclDocument({ path: '/foo/bar.acl' })).resolves.toEqual({ path: '/foo/bar.acl' });
});
});
describe('#isAcl', (): void => {
it('checks if a resource is an acl resource by looking at the extension.', async(): Promise<void> => {
await expect(manager.isAclDocument({ path: '/foo/bar' })).resolves.toBeFalsy();
await expect(manager.isAclDocument({ path: '/foo/bar/' })).resolves.toBeFalsy();
await expect(manager.isAclDocument({ path: '/foo/bar.acl' })).resolves.toBeTruthy();
await expect(manager.isAclDocument({ path: '/foo/bar.acl/' })).resolves.toBeTruthy();
});
});
describe('#getResource', (): void => {
it('generates non-acl resource URLs by removing the .acl extension.', async(): Promise<void> => {
await expect(manager.getAclConstrainedResource({ path: '/foo/bar.acl' })).resolves.toEqual({ path: '/foo/bar' });
});
it('returns the identifier if the input is already a non-acl resource.', async(): Promise<void> => {
await expect(manager.getAclConstrainedResource({ path: '/foo/bar' })).resolves.toEqual({ path: '/foo/bar' });
});
});
});

View File

@ -1,8 +1,8 @@
import { namedNode, quad } from '@rdfjs/data-model'; import { namedNode, quad } from '@rdfjs/data-model';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import type { Credentials } from '../../../src/authentication/Credentials'; import type { Credentials } from '../../../src/authentication/Credentials';
import type { AclManager } from '../../../src/authorization/AclManager';
import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer'; import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer';
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet';
import type { Representation } from '../../../src/ldp/representation/Representation'; import type { Representation } from '../../../src/ldp/representation/Representation';
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
@ -18,13 +18,11 @@ const acl = 'http://www.w3.org/ns/auth/acl#';
describe('A WebAclAuthorizer', (): void => { describe('A WebAclAuthorizer', (): void => {
let authorizer: WebAclAuthorizer; let authorizer: WebAclAuthorizer;
const aclManager: AclManager = { const aclStrategy: AuxiliaryIdentifierStrategy = {
getAclDocument: async(id: ResourceIdentifier): Promise<ResourceIdentifier> => getAuxiliaryIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: `${id.path}.acl` }),
id.path.endsWith('.acl') ? id : { path: `${id.path}.acl` }, isAuxiliaryIdentifier: (id: ResourceIdentifier): boolean => id.path.endsWith('.acl'),
isAclDocument: async(id: ResourceIdentifier): Promise<boolean> => id.path.endsWith('.acl'), getAssociatedIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: id.path.slice(0, -4) }),
getAclConstrainedResource: async(id: ResourceIdentifier): Promise<ResourceIdentifier> => } as any;
!id.path.endsWith('.acl') ? id : { path: id.path.slice(0, -4) },
};
let store: ResourceStore; let store: ResourceStore;
const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/'); const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/');
let permissions: PermissionSet; let permissions: PermissionSet;
@ -44,11 +42,11 @@ describe('A WebAclAuthorizer', (): void => {
store = { store = {
getRepresentation: jest.fn(), getRepresentation: jest.fn(),
} as any; } as any;
authorizer = new WebAclAuthorizer(aclManager, store, identifierStrategy); authorizer = new WebAclAuthorizer(aclStrategy, store, identifierStrategy);
}); });
it('handles all inputs.', async(): Promise<void> => { it('handles all inputs.', async(): Promise<void> => {
authorizer = new WebAclAuthorizer(aclManager, null as any, identifierStrategy); authorizer = new WebAclAuthorizer(aclStrategy, null as any, identifierStrategy);
await expect(authorizer.canHandle({} as any)).resolves.toBeUndefined(); await expect(authorizer.canHandle({} as any)).resolves.toBeUndefined();
}); });
@ -130,7 +128,7 @@ describe('A WebAclAuthorizer', (): void => {
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)),
]) } as Representation); ]) } as Representation);
const aclIdentifier = await aclManager.getAclDocument(identifier); const aclIdentifier = aclStrategy.getAuxiliaryIdentifier(identifier);
await expect(authorizer.handle({ identifier: aclIdentifier, permissions, credentials })).resolves.toBeUndefined(); await expect(authorizer.handle({ identifier: aclIdentifier, permissions, credentials })).resolves.toBeUndefined();
}); });
@ -143,7 +141,7 @@ describe('A WebAclAuthorizer', (): void => {
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)),
]) } as Representation); ]) } as Representation);
identifier = await aclManager.getAclDocument(identifier); identifier = aclStrategy.getAuxiliaryIdentifier(identifier);
await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError); await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError);
}); });

View File

@ -1,6 +1,6 @@
import fs from 'fs'; import fs from 'fs';
import type { AclManager } from '../../../src/authorization/AclManager';
import { AclInitializer } from '../../../src/init/AclInitializer'; import { AclInitializer } from '../../../src/init/AclInitializer';
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation'; import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
import type { ResourceStore } from '../../../src/storage/ResourceStore'; import type { ResourceStore } from '../../../src/storage/ResourceStore';
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
@ -19,8 +19,8 @@ describe('AclInitializer', (): void => {
setRepresentation: jest.fn(), setRepresentation: jest.fn(),
} as any; } as any;
const aclIdentifier = { path: 'http://test.com/.acl' }; const aclIdentifier = { path: 'http://test.com/.acl' };
const aclManager: jest.Mocked<AclManager> = { const aclStrategy: jest.Mocked<AuxiliaryIdentifierStrategy> = {
getAclDocument: jest.fn().mockResolvedValue(aclIdentifier), getAuxiliaryIdentifier: jest.fn().mockReturnValue(aclIdentifier),
} as any; } as any;
const baseUrl = 'http://localhost:3000/'; const baseUrl = 'http://localhost:3000/';
@ -29,10 +29,10 @@ describe('AclInitializer', (): void => {
}); });
it('sets the default ACL when none exists already.', async(): Promise<void> => { it('sets the default ACL when none exists already.', async(): Promise<void> => {
const initializer = new AclInitializer({ baseUrl, store, aclManager }); const initializer = new AclInitializer({ baseUrl, store, aclStrategy });
await initializer.handle(); await initializer.handle();
expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl }); expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
expect(store.getRepresentation).toHaveBeenCalledTimes(1); expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {}); expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {});
expect(store.setRepresentation).toHaveBeenCalledTimes(1); expect(store.setRepresentation).toHaveBeenCalledTimes(1);
@ -45,10 +45,10 @@ describe('AclInitializer', (): void => {
}); });
it('sets the specific ACL when one was specified.', async(): Promise<void> => { it('sets the specific ACL when one was specified.', async(): Promise<void> => {
const initializer = new AclInitializer({ baseUrl, store, aclManager, aclPath: '/path/doc.acl' }); const initializer = new AclInitializer({ baseUrl, store, aclStrategy, aclPath: '/path/doc.acl' });
await initializer.handle(); await initializer.handle();
expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl }); expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
expect(store.getRepresentation).toHaveBeenCalledTimes(1); expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {}); expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {});
expect(store.setRepresentation).toHaveBeenCalledTimes(1); expect(store.setRepresentation).toHaveBeenCalledTimes(1);
@ -65,10 +65,10 @@ describe('AclInitializer', (): void => {
data: { destroy: jest.fn() }, data: { destroy: jest.fn() },
} as any)); } as any));
const initializer = new AclInitializer({ baseUrl, store, aclManager }); const initializer = new AclInitializer({ baseUrl, store, aclStrategy });
await initializer.handle(); await initializer.handle();
expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl }); expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
expect(store.getRepresentation).toHaveBeenCalledTimes(1); expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {}); expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {});
expect(store.setRepresentation).toHaveBeenCalledTimes(0); expect(store.setRepresentation).toHaveBeenCalledTimes(0);
@ -77,7 +77,7 @@ describe('AclInitializer', (): void => {
it('errors when the root ACL check errors.', async(): Promise<void> => { it('errors when the root ACL check errors.', async(): Promise<void> => {
store.getRepresentation.mockRejectedValueOnce(new Error('Fatal')); store.getRepresentation.mockRejectedValueOnce(new Error('Fatal'));
const initializer = new AclInitializer({ baseUrl, store, aclManager }); const initializer = new AclInitializer({ baseUrl, store, aclStrategy });
await expect(initializer.handle()).rejects.toThrow('Fatal'); await expect(initializer.handle()).rejects.toThrow('Fatal');
}); });
}); });

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 canDeleteRoot.', async(): Promise<void> => { it('returns the injected value for isRootRequired.', async(): Promise<void> => {
expect(strategy.requiresRootAuxiliary()).toBe(true); expect(strategy.isRootRequired()).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 canDeleteRoot to false.', async(): Promise<void> => { it('defaults isRootRequired to false.', async(): Promise<void> => {
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator); strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator);
expect(strategy.requiresRootAuxiliary()).toBe(false); expect(strategy.isRootRequired()).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 requiresRootAuxiliary(): boolean { public isRootRequired(): 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('#canDeleteRoot returns the result of the correct source.', async(): Promise<void> => { it('#isRootRequired returns the result of the correct source.', async(): Promise<void> => {
sources[0].requiresRootAuxiliary = jest.fn(); sources[0].isRootRequired = jest.fn();
sources[1].requiresRootAuxiliary = jest.fn(); sources[1].isRootRequired = jest.fn();
strategy.requiresRootAuxiliary(dummy2Id); strategy.isRootRequired(dummy2Id);
expect(sources[0].requiresRootAuxiliary).toHaveBeenCalledTimes(0); expect(sources[0].isRootRequired).toHaveBeenCalledTimes(0);
expect(sources[1].requiresRootAuxiliary).toHaveBeenCalledTimes(1); expect(sources[1].isRootRequired).toHaveBeenCalledTimes(1);
expect(sources[1].requiresRootAuxiliary).toHaveBeenLastCalledWith(dummy2Id); expect(sources[1].isRootRequired).toHaveBeenLastCalledWith(dummy2Id);
}); });
it('#validates using the correct validator.', async(): Promise<void> => { it('#validates using the correct validator.', async(): Promise<void> => {

View File

@ -38,8 +38,4 @@ describe('A SuffixAuxiliaryManager', (): void => {
it('removes the suffix to create the associated identifier.', async(): Promise<void> => { it('removes the suffix to create the associated identifier.', async(): Promise<void> => {
expect(strategy.getAssociatedIdentifier(auxiliaryId)).toEqual(associatedId); expect(strategy.getAssociatedIdentifier(auxiliaryId)).toEqual(associatedId);
}); });
it('returns true on canDeleteRoot.', async(): Promise<void> => {
expect(strategy.canDeleteRoot()).toEqual(true);
});
}); });

View File

@ -1,17 +1,17 @@
import { createResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http';
import type { AclManager } from '../../../../../src/authorization/AclManager'; import type { AuxiliaryIdentifierStrategy } from '../../../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import { AclLinkMetadataWriter } from '../../../../../src/ldp/http/metadata/AclLinkMetadataWriter'; import { AclLinkMetadataWriter } from '../../../../../src/ldp/http/metadata/AclLinkMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../../../src/ldp/representation/ResourceIdentifier';
describe('An AclLinkMetadataWriter', (): void => { describe('An AclLinkMetadataWriter', (): void => {
const manager = { const strategy = {
getAclDocument: async(id: ResourceIdentifier): Promise<ResourceIdentifier> => ({ path: `${id.path}.acl` }), getAuxiliaryIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: `${id.path}.acl` }),
} as AclManager; } as AuxiliaryIdentifierStrategy;
const identifier = { path: 'http://test.com/foo' }; const identifier = { path: 'http://test.com/foo' };
it('adds the acl link header.', async(): Promise<void> => { it('adds the acl link header.', async(): Promise<void> => {
const writer = new AclLinkMetadataWriter(manager); const writer = new AclLinkMetadataWriter(strategy);
const response = createResponse(); const response = createResponse();
const metadata = new RepresentationMetadata(identifier); const metadata = new RepresentationMetadata(identifier);
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined(); await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
@ -19,7 +19,7 @@ describe('An AclLinkMetadataWriter', (): void => {
}); });
it('can use a custom rel attribute.', async(): Promise<void> => { it('can use a custom rel attribute.', async(): Promise<void> => {
const writer = new AclLinkMetadataWriter(manager, 'http://www.w3.org/ns/solid/terms#acl'); const writer = new AclLinkMetadataWriter(strategy, 'http://www.w3.org/ns/solid/terms#acl');
const response = createResponse(); const response = createResponse();
const metadata = new RepresentationMetadata(identifier); const metadata = new RepresentationMetadata(identifier);
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined(); await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();