mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Generalize AclManager to AuxiliaryManager
This commit is contained in:
parent
d6cdd7dbdf
commit
758f5ed083
@ -2,14 +2,34 @@
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "urn:solid-server:default:AclManager",
|
||||
"@type": "UrlBasedAclManager"
|
||||
"@id": "urn:solid-server:default:AclIdentifierStrategy",
|
||||
"@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",
|
||||
"@type": "WebAclAuthorizer",
|
||||
"WebAclAuthorizer:_aclManager": {
|
||||
"@id": "urn:solid-server:default:AclManager"
|
||||
"WebAclAuthorizer:_aclStrategy": {
|
||||
"@id": "urn:solid-server:default:AclIdentifierStrategy"
|
||||
},
|
||||
"WebAclAuthorizer:_resourceStore": {
|
||||
"@id": "urn:solid-server:default:ResourceStore"
|
||||
|
@ -28,8 +28,8 @@
|
||||
"AclInitializer:_settings_store": {
|
||||
"@id": "urn:solid-server:default:ResourceStore"
|
||||
},
|
||||
"AclInitializer:_settings_aclManager": {
|
||||
"@id": "urn:solid-server:default:AclManager"
|
||||
"AclInitializer:_settings_aclStrategy": {
|
||||
"@id": "urn:solid-server:default:AclIdentifierStrategy"
|
||||
},
|
||||
"AclInitializer:_settings_baseUrl": {
|
||||
"@id": "urn:solid-server:default:variable:baseUrl"
|
||||
|
@ -42,8 +42,8 @@
|
||||
},
|
||||
{
|
||||
"@type": "AclLinkMetadataWriter",
|
||||
"AclLinkMetadataWriter:_aclManager": {
|
||||
"@id": "urn:solid-server:default:AclManager"
|
||||
"AclLinkMetadataWriter:_aclStrategy": {
|
||||
"@id": "urn:solid-server:default:AclIdentifierStrategy"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -82,11 +82,11 @@
|
||||
"@type": "WaterfallHandler",
|
||||
"WaterfallHandler:_handlers": [
|
||||
{
|
||||
"@id": "urn:solid-server:default:IndexConverter",
|
||||
"@id": "urn:solid-server:default:IndexConverter"
|
||||
},
|
||||
{
|
||||
"@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"
|
||||
@ -101,6 +101,14 @@
|
||||
"@id": "urn:solid-server:default:RdfRepresentationConverter"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"@id": "urn:solid-server:default:RdfValidator",
|
||||
"@type": "RdfValidator",
|
||||
"RdfValidator:_converter": {
|
||||
"@id": "urn:solid-server:default:RepresentationConverter"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
@ -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) };
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import type { Quad, Term } from 'n3';
|
||||
import { Store } from 'n3';
|
||||
import type { Credentials } from '../authentication/Credentials';
|
||||
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import type { PermissionSet } from '../ldp/permissions/PermissionSet';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
@ -12,7 +13,6 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
|
||||
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
|
||||
import { ACL, FOAF } from '../util/Vocabularies';
|
||||
import type { AclManager } from './AclManager';
|
||||
import type { AuthorizerArgs } from './Authorizer';
|
||||
import { Authorizer } from './Authorizer';
|
||||
|
||||
@ -24,13 +24,14 @@ import { Authorizer } from './Authorizer';
|
||||
export class WebAclAuthorizer extends Authorizer {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly aclManager: AclManager;
|
||||
private readonly aclStrategy: AuxiliaryIdentifierStrategy;
|
||||
private readonly resourceStore: ResourceStore;
|
||||
private readonly identifierStrategy: IdentifierStrategy;
|
||||
|
||||
public constructor(aclManager: AclManager, resourceStore: ResourceStore, identifierStrategy: IdentifierStrategy) {
|
||||
public constructor(aclStrategy: AuxiliaryIdentifierStrategy, resourceStore: ResourceStore,
|
||||
identifierStrategy: IdentifierStrategy) {
|
||||
super();
|
||||
this.aclManager = aclManager;
|
||||
this.aclStrategy = aclStrategy;
|
||||
this.resourceStore = resourceStore;
|
||||
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
|
||||
// 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
|
||||
const modes = await this.aclManager.isAclDocument(identifier) ?
|
||||
const modes = this.aclStrategy.isAuxiliaryIdentifier(identifier) ?
|
||||
[ 'control' ] :
|
||||
(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> {
|
||||
this.logger.debug(`Trying to read the direct ACL document of ${id.path}`);
|
||||
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}`);
|
||||
const data = await this.resourceStore.getRepresentation(acl, { type: { [INTERNAL_QUADS]: 1 }});
|
||||
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);
|
||||
} catch (error: unknown) {
|
||||
if (NotFoundHttpError.isInstance(error)) {
|
||||
|
@ -9,9 +9,7 @@ export * from './authentication/UnsecureWebIdExtractor';
|
||||
|
||||
// Authorization
|
||||
export * from './authorization/AllowEverythingAuthorizer';
|
||||
export * from './authorization/AclManager';
|
||||
export * from './authorization/Authorizer';
|
||||
export * from './authorization/UrlBasedAclManager';
|
||||
export * from './authorization/WebAclAuthorizer';
|
||||
|
||||
// Init
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createReadStream } from 'fs';
|
||||
import type { AclManager } from '../authorization/AclManager';
|
||||
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
@ -17,19 +17,19 @@ const DEFAULT_ACL_PATH = joinFilePath(__dirname, '../../templates/root/.acl');
|
||||
export class AclInitializer extends Initializer {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
private readonly store: ResourceStore;
|
||||
private readonly aclManager: AclManager;
|
||||
private readonly aclStrategy: AuxiliaryIdentifierStrategy;
|
||||
private readonly root: ResourceIdentifier;
|
||||
private readonly aclPath: string;
|
||||
|
||||
public constructor(settings: {
|
||||
store: ResourceStore;
|
||||
aclManager: AclManager;
|
||||
aclStrategy: AuxiliaryIdentifierStrategy;
|
||||
baseUrl: string;
|
||||
aclPath?: string;
|
||||
}) {
|
||||
super();
|
||||
this.store = settings.store;
|
||||
this.aclManager = settings.aclManager;
|
||||
this.aclStrategy = settings.aclStrategy;
|
||||
this.root = { path: ensureTrailingSlash(settings.baseUrl) };
|
||||
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."
|
||||
// https://solid.github.io/specification/protocol#storage
|
||||
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)) {
|
||||
this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`);
|
||||
} else {
|
||||
|
@ -10,10 +10,11 @@ import type { AuxiliaryIdentifierStrategy } from './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.
|
||||
*/
|
||||
requiresRootAuxiliary: (identifier: ResourceIdentifier) => boolean;
|
||||
isRootRequired: (identifier: ResourceIdentifier) => boolean;
|
||||
|
||||
/**
|
||||
* Adds metadata related to this auxiliary resource,
|
||||
|
@ -14,14 +14,14 @@ export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
|
||||
private readonly identifierStrategy: AuxiliaryIdentifierStrategy;
|
||||
private readonly metadataGenerator?: MetadataGenerator;
|
||||
private readonly validator?: Validator;
|
||||
private readonly canDelete: boolean;
|
||||
private readonly rootRequired: boolean;
|
||||
|
||||
public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator,
|
||||
validator?: Validator, canDeleteRoot = false) {
|
||||
validator?: Validator, isRootRequired = false) {
|
||||
this.identifierStrategy = identifierStrategy;
|
||||
this.metadataGenerator = metadataGenerator;
|
||||
this.validator = validator;
|
||||
this.canDelete = canDeleteRoot;
|
||||
this.rootRequired = isRootRequired;
|
||||
}
|
||||
|
||||
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
@ -40,8 +40,8 @@ export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
|
||||
return this.identifierStrategy.getAssociatedIdentifier(identifier);
|
||||
}
|
||||
|
||||
public requiresRootAuxiliary(): boolean {
|
||||
return this.canDelete;
|
||||
public isRootRequired(): boolean {
|
||||
return this.rootRequired;
|
||||
}
|
||||
|
||||
public async addMetadata(metadata: RepresentationMetadata): Promise<void> {
|
||||
|
@ -18,9 +18,9 @@ export class RoutingAuxiliaryStrategy extends RoutingAuxiliaryIdentifierStrategy
|
||||
super(sources);
|
||||
}
|
||||
|
||||
public requiresRootAuxiliary(identifier: ResourceIdentifier): boolean {
|
||||
public isRootRequired(identifier: ResourceIdentifier): boolean {
|
||||
const source = this.getMatchingSource(identifier);
|
||||
return source.requiresRootAuxiliary(identifier);
|
||||
return source.isRootRequired(identifier);
|
||||
}
|
||||
|
||||
public async addMetadata(metadata: RepresentationMetadata): Promise<void> {
|
||||
|
@ -34,8 +34,4 @@ export class SuffixAuxiliaryIdentifierStrategy implements AuxiliaryIdentifierStr
|
||||
}
|
||||
return { path: identifier.path.slice(0, -this.suffix.length) };
|
||||
}
|
||||
|
||||
public canDeleteRoot(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { AclManager } from '../../../authorization/AclManager';
|
||||
import type { HttpResponse } from '../../../server/HttpResponse';
|
||||
import { addHeader } from '../../../util/HeaderUtil';
|
||||
import type { AuxiliaryIdentifierStrategy } from '../../auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
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).
|
||||
*/
|
||||
export class AclLinkMetadataWriter extends MetadataWriter {
|
||||
private readonly aclManager: AclManager;
|
||||
private readonly aclStrategy: AuxiliaryIdentifierStrategy;
|
||||
private readonly rel: string;
|
||||
|
||||
public constructor(aclManager: AclManager, rel = 'acl') {
|
||||
public constructor(aclStrategy: AuxiliaryIdentifierStrategy, rel = 'acl') {
|
||||
super();
|
||||
this.aclManager = aclManager;
|
||||
this.aclStrategy = aclStrategy;
|
||||
this.rel = rel;
|
||||
}
|
||||
|
||||
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}"`);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"files-scs:config/presets/acl.json",
|
||||
"files-scs:config/presets/http.json",
|
||||
"files-scs:config/presets/ldp/credentials-extractor.json",
|
||||
"files-scs:config/presets/ldp/metadata-handler.json",
|
||||
@ -46,11 +47,6 @@
|
||||
"PassthroughStore:_source": {
|
||||
"@id": "urn:solid-server:default:MemoryResourceStore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@id": "urn:solid-server:default:AclManager",
|
||||
"@type": "UrlBasedAclManager",
|
||||
"comment": "Needed for AclLinkMetadataWriter"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
@ -1,8 +1,8 @@
|
||||
import { namedNode, quad } from '@rdfjs/data-model';
|
||||
import streamifyArray from 'streamify-array';
|
||||
import type { Credentials } from '../../../src/authentication/Credentials';
|
||||
import type { AclManager } from '../../../src/authorization/AclManager';
|
||||
import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer';
|
||||
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet';
|
||||
import type { Representation } from '../../../src/ldp/representation/Representation';
|
||||
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 => {
|
||||
let authorizer: WebAclAuthorizer;
|
||||
const aclManager: AclManager = {
|
||||
getAclDocument: async(id: ResourceIdentifier): Promise<ResourceIdentifier> =>
|
||||
id.path.endsWith('.acl') ? id : { path: `${id.path}.acl` },
|
||||
isAclDocument: async(id: ResourceIdentifier): Promise<boolean> => id.path.endsWith('.acl'),
|
||||
getAclConstrainedResource: async(id: ResourceIdentifier): Promise<ResourceIdentifier> =>
|
||||
!id.path.endsWith('.acl') ? id : { path: id.path.slice(0, -4) },
|
||||
};
|
||||
const aclStrategy: AuxiliaryIdentifierStrategy = {
|
||||
getAuxiliaryIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: `${id.path}.acl` }),
|
||||
isAuxiliaryIdentifier: (id: ResourceIdentifier): boolean => id.path.endsWith('.acl'),
|
||||
getAssociatedIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: id.path.slice(0, -4) }),
|
||||
} as any;
|
||||
let store: ResourceStore;
|
||||
const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/');
|
||||
let permissions: PermissionSet;
|
||||
@ -44,11 +42,11 @@ describe('A WebAclAuthorizer', (): void => {
|
||||
store = {
|
||||
getRepresentation: jest.fn(),
|
||||
} as any;
|
||||
authorizer = new WebAclAuthorizer(aclManager, store, identifierStrategy);
|
||||
authorizer = new WebAclAuthorizer(aclStrategy, store, identifierStrategy);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
@ -130,7 +128,7 @@ describe('A WebAclAuthorizer', (): void => {
|
||||
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);
|
||||
const aclIdentifier = aclStrategy.getAuxiliaryIdentifier(identifier);
|
||||
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}Write`)),
|
||||
]) } as Representation);
|
||||
identifier = await aclManager.getAclDocument(identifier);
|
||||
identifier = aclStrategy.getAuxiliaryIdentifier(identifier);
|
||||
await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError);
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import type { AclManager } from '../../../src/authorization/AclManager';
|
||||
import { AclInitializer } from '../../../src/init/AclInitializer';
|
||||
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
||||
@ -19,8 +19,8 @@ describe('AclInitializer', (): void => {
|
||||
setRepresentation: jest.fn(),
|
||||
} as any;
|
||||
const aclIdentifier = { path: 'http://test.com/.acl' };
|
||||
const aclManager: jest.Mocked<AclManager> = {
|
||||
getAclDocument: jest.fn().mockResolvedValue(aclIdentifier),
|
||||
const aclStrategy: jest.Mocked<AuxiliaryIdentifierStrategy> = {
|
||||
getAuxiliaryIdentifier: jest.fn().mockReturnValue(aclIdentifier),
|
||||
} as any;
|
||||
const baseUrl = 'http://localhost:3000/';
|
||||
|
||||
@ -29,10 +29,10 @@ describe('AclInitializer', (): 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();
|
||||
|
||||
expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl });
|
||||
expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {});
|
||||
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
@ -45,10 +45,10 @@ describe('AclInitializer', (): 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();
|
||||
|
||||
expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl });
|
||||
expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {});
|
||||
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
@ -65,10 +65,10 @@ describe('AclInitializer', (): void => {
|
||||
data: { destroy: jest.fn() },
|
||||
} as any));
|
||||
|
||||
const initializer = new AclInitializer({ baseUrl, store, aclManager });
|
||||
const initializer = new AclInitializer({ baseUrl, store, aclStrategy });
|
||||
await initializer.handle();
|
||||
|
||||
expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: baseUrl });
|
||||
expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenCalledWith(aclIdentifier, {});
|
||||
expect(store.setRepresentation).toHaveBeenCalledTimes(0);
|
||||
@ -77,7 +77,7 @@ describe('AclInitializer', (): void => {
|
||||
it('errors when the root ACL check errors.', async(): Promise<void> => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@ -45,8 +45,8 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
|
||||
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier);
|
||||
});
|
||||
|
||||
it('returns the injected value for canDeleteRoot.', async(): Promise<void> => {
|
||||
expect(strategy.requiresRootAuxiliary()).toBe(true);
|
||||
it('returns the injected value for isRootRequired.', async(): Promise<void> => {
|
||||
expect(strategy.isRootRequired()).toBe(true);
|
||||
});
|
||||
|
||||
it('adds metadata through the MetadataGenerator.', async(): Promise<void> => {
|
||||
@ -63,9 +63,9 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
|
||||
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);
|
||||
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> => {
|
||||
|
@ -27,7 +27,7 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
|
||||
return { path: identifier.path.slice(0, -this.suffix.length) };
|
||||
}
|
||||
|
||||
public requiresRootAuxiliary(): boolean {
|
||||
public isRootRequired(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -77,13 +77,13 @@ describe('A RoutingAuxiliaryStrategy', (): void => {
|
||||
expect(sources[1].addMetadata).toHaveBeenLastCalledWith(metadata);
|
||||
});
|
||||
|
||||
it('#canDeleteRoot returns the result of the correct source.', async(): Promise<void> => {
|
||||
sources[0].requiresRootAuxiliary = jest.fn();
|
||||
sources[1].requiresRootAuxiliary = jest.fn();
|
||||
strategy.requiresRootAuxiliary(dummy2Id);
|
||||
expect(sources[0].requiresRootAuxiliary).toHaveBeenCalledTimes(0);
|
||||
expect(sources[1].requiresRootAuxiliary).toHaveBeenCalledTimes(1);
|
||||
expect(sources[1].requiresRootAuxiliary).toHaveBeenLastCalledWith(dummy2Id);
|
||||
it('#isRootRequired returns the result of the correct source.', async(): Promise<void> => {
|
||||
sources[0].isRootRequired = jest.fn();
|
||||
sources[1].isRootRequired = jest.fn();
|
||||
strategy.isRootRequired(dummy2Id);
|
||||
expect(sources[0].isRootRequired).toHaveBeenCalledTimes(0);
|
||||
expect(sources[1].isRootRequired).toHaveBeenCalledTimes(1);
|
||||
expect(sources[1].isRootRequired).toHaveBeenLastCalledWith(dummy2Id);
|
||||
});
|
||||
|
||||
it('#validates using the correct validator.', async(): Promise<void> => {
|
||||
|
@ -38,8 +38,4 @@ describe('A SuffixAuxiliaryManager', (): void => {
|
||||
it('removes the suffix to create the associated identifier.', async(): Promise<void> => {
|
||||
expect(strategy.getAssociatedIdentifier(auxiliaryId)).toEqual(associatedId);
|
||||
});
|
||||
|
||||
it('returns true on canDeleteRoot.', async(): Promise<void> => {
|
||||
expect(strategy.canDeleteRoot()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
@ -1,17 +1,17 @@
|
||||
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 { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../../../../src/ldp/representation/ResourceIdentifier';
|
||||
|
||||
describe('An AclLinkMetadataWriter', (): void => {
|
||||
const manager = {
|
||||
getAclDocument: async(id: ResourceIdentifier): Promise<ResourceIdentifier> => ({ path: `${id.path}.acl` }),
|
||||
} as AclManager;
|
||||
const strategy = {
|
||||
getAuxiliaryIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: `${id.path}.acl` }),
|
||||
} as AuxiliaryIdentifierStrategy;
|
||||
const identifier = { path: 'http://test.com/foo' };
|
||||
|
||||
it('adds the acl link header.', async(): Promise<void> => {
|
||||
const writer = new AclLinkMetadataWriter(manager);
|
||||
const writer = new AclLinkMetadataWriter(strategy);
|
||||
const response = createResponse();
|
||||
const metadata = new RepresentationMetadata(identifier);
|
||||
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> => {
|
||||
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 metadata = new RepresentationMetadata(identifier);
|
||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||
|
Loading…
x
Reference in New Issue
Block a user