feat: Replace acl specific permissions with generic permissions

This required AuxiliaryStrategy to have a new function
indicating if the auxiliary resource just used its associated resource authorization
or its own.
This commit is contained in:
Joachim Van Herwegen 2021-09-21 11:56:05 +02:00
parent 5104cd56e8
commit 7f8b923399
46 changed files with 221 additions and 152 deletions

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/dynamic.json", "files-scs:config/storage/backend/dynamic.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/file.json", "files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/file.json", "files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/file.json", "files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -30,5 +30,4 @@ Contains a list of metadata writers that will be run on outgoing responses.
## Modes ## Modes
Determines which modes are needed for requests, Determines which modes are needed for requests,
by default this is based on the used HTTP method. by default this is based on the used HTTP method.
* *acl*: The default setup with specific support for accessing .acl documents. * *default*: Bases required modes on HTTP method.
* *no-acl*: Same as above but interprets .acl documents as any other document.

View File

@ -1,18 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Makes sure acl files require control permissions.",
"@id": "urn:solid-server:default:ModesExtractor",
"@type": "WaterfallHandler",
"handlers": [
{
"@type": "AclModesExtractor",
"aclStrategy": { "@id": "urn:solid-server:default:AclStrategy" }
},
{ "@type": "MethodModesExtractor" },
{ "@type": "SparqlPatchModesExtractor" }
]
}
]
}

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [ "@graph": [
{ {
"comment": "Extracts the required permissions based on the HTTP method.", "comment": "Determines required modes based on HTTP methods.",
"@id": "urn:solid-server:default:ModesExtractor", "@id": "urn:solid-server:default:ModesExtractor",
"@type": "WaterfallHandler", "@type": "WaterfallHandler",
"handlers": [ "handlers": [

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/regex.json", "files-scs:config/storage/backend/regex.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/sparql.json", "files-scs:config/storage/backend/sparql.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/sparql.json", "files-scs:config/storage/backend/sparql.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

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

View File

@ -16,7 +16,8 @@ export class AllStaticReader extends PermissionReader {
read: allow, read: allow,
write: allow, write: allow,
append: allow, append: allow,
control: allow, create: allow,
delete: allow,
}); });
} }

View File

@ -1,4 +1,4 @@
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { AuxiliaryStrategy } from '../ldp/auxiliary/AuxiliaryStrategy';
import type { PermissionSet } from '../ldp/permissions/Permissions'; import type { PermissionSet } from '../ldp/permissions/Permissions';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
@ -15,9 +15,9 @@ export class AuxiliaryReader extends PermissionReader {
protected readonly logger = getLoggerFor(this); protected readonly logger = getLoggerFor(this);
private readonly resourceReader: PermissionReader; private readonly resourceReader: PermissionReader;
private readonly auxiliaryStrategy: AuxiliaryIdentifierStrategy; private readonly auxiliaryStrategy: AuxiliaryStrategy;
public constructor(resourceReader: PermissionReader, auxiliaryStrategy: AuxiliaryIdentifierStrategy) { public constructor(resourceReader: PermissionReader, auxiliaryStrategy: AuxiliaryStrategy) {
super(); super();
this.resourceReader = resourceReader; this.resourceReader = resourceReader;
this.auxiliaryStrategy = auxiliaryStrategy; this.auxiliaryStrategy = auxiliaryStrategy;
@ -44,6 +44,11 @@ export class AuxiliaryReader extends PermissionReader {
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(auxiliaryAuth.identifier)) { if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(auxiliaryAuth.identifier)) {
throw new NotImplementedHttpError('AuxiliaryAuthorizer only supports auxiliary resources.'); throw new NotImplementedHttpError('AuxiliaryAuthorizer only supports auxiliary resources.');
} }
if (this.auxiliaryStrategy.usesOwnAuthorization(auxiliaryAuth.identifier)) {
throw new NotImplementedHttpError('Auxiliary resource uses its own permissions.');
}
return { return {
...auxiliaryAuth, ...auxiliaryAuth,
identifier: this.auxiliaryStrategy.getAssociatedIdentifier(auxiliaryAuth.identifier), identifier: this.auxiliaryStrategy.getAssociatedIdentifier(auxiliaryAuth.identifier),

View File

@ -3,7 +3,9 @@ import { Store } from 'n3';
import { CredentialGroup } from '../authentication/Credentials'; import { CredentialGroup } from '../authentication/Credentials';
import type { Credential, CredentialSet } from '../authentication/Credentials'; import type { Credential, CredentialSet } from '../authentication/Credentials';
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { Permission, PermissionSet } from '../ldp/permissions/Permissions'; import { AclMode } from '../ldp/permissions/AclPermission';
import type { AclPermission } from '../ldp/permissions/AclPermission';
import type { PermissionSet } from '../ldp/permissions/Permissions';
import { AccessMode } from '../ldp/permissions/Permissions'; import { AccessMode } from '../ldp/permissions/Permissions';
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';
@ -14,7 +16,6 @@ import { createErrorMessage } from '../util/errors/ErrorUtil';
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError'; import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
import { InternalServerError } from '../util/errors/InternalServerError'; import { InternalServerError } from '../util/errors/InternalServerError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy'; import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { readableToQuads } from '../util/StreamUtil'; import { readableToQuads } from '../util/StreamUtil';
import { ACL, RDF } from '../util/Vocabularies'; import { ACL, RDF } from '../util/Vocabularies';
@ -22,11 +23,11 @@ import type { AccessChecker } from './access-checkers/AccessChecker';
import type { PermissionReaderInput } from './PermissionReader'; import type { PermissionReaderInput } from './PermissionReader';
import { PermissionReader } from './PermissionReader'; import { PermissionReader } from './PermissionReader';
const modesMap: Record<string, AccessMode> = { const modesMap: Record<string, keyof AclPermission> = {
[ACL.Read]: AccessMode.read, [ACL.Read]: AccessMode.read,
[ACL.Write]: AccessMode.write, [ACL.Write]: AccessMode.write,
[ACL.Append]: AccessMode.append, [ACL.Append]: AccessMode.append,
[ACL.Control]: AccessMode.control, [ACL.Control]: AclMode.control,
} as const; } as const;
/** /**
@ -50,12 +51,6 @@ export class WebAclReader extends PermissionReader {
this.accessChecker = accessChecker; this.accessChecker = accessChecker;
} }
public async canHandle({ identifier }: PermissionReaderInput): Promise<void> {
if (this.aclStrategy.isAuxiliaryIdentifier(identifier)) {
throw new NotImplementedHttpError('WebAclAuthorizer does not support permissions on auxiliary resources.');
}
}
/** /**
* Checks if an agent is allowed to execute the requested actions. * Checks if an agent is allowed to execute the requested actions.
* Will throw an error if this is not the case. * Will throw an error if this is not the case.
@ -66,24 +61,28 @@ export class WebAclReader extends PermissionReader {
// Determine the required access modes // Determine the required access modes
this.logger.debug(`Retrieving permissions of ${credentials.agent?.webId} for ${identifier.path}`); this.logger.debug(`Retrieving permissions of ${credentials.agent?.webId} for ${identifier.path}`);
const isAcl = this.aclStrategy.isAuxiliaryIdentifier(identifier);
const mainIdentifier = isAcl ? this.aclStrategy.getAssociatedIdentifier(identifier) : identifier;
// Determine the full authorization for the agent granted by the applicable ACL // Determine the full authorization for the agent granted by the applicable ACL
const acl = await this.getAclRecursive(identifier); const acl = await this.getAclRecursive(mainIdentifier);
return this.createPermissions(credentials, acl); return this.createPermissions(credentials, acl, isAcl);
} }
/** /**
* Creates an Authorization object based on the quads found in the ACL. * Creates an Authorization object based on the quads found in the ACL.
* @param credentials - Credentials to check permissions for. * @param credentials - Credentials to check permissions for.
* @param acl - Store containing all relevant authorization triples. * @param acl - Store containing all relevant authorization triples.
* @param isAcl - If the target resource is an acl document.
*/ */
private async createPermissions(credentials: CredentialSet, acl: Store): private async createPermissions(credentials: CredentialSet, acl: Store, isAcl: boolean):
Promise<PermissionSet> { Promise<PermissionSet> {
const publicPermissions = await this.determinePermissions(acl, credentials.public); const publicPermissions = await this.determinePermissions(acl, credentials.public);
const agentPermissions = await this.determinePermissions(acl, credentials.agent); const agentPermissions = await this.determinePermissions(acl, credentials.agent);
return { return {
[CredentialGroup.agent]: agentPermissions, [CredentialGroup.agent]: this.updateAclPermissions(agentPermissions, isAcl),
[CredentialGroup.public]: publicPermissions, [CredentialGroup.public]: this.updateAclPermissions(publicPermissions, isAcl),
}; };
} }
@ -93,10 +92,10 @@ export class WebAclReader extends PermissionReader {
* @param acl - Store containing all relevant authorization triples. * @param acl - Store containing all relevant authorization triples.
* @param credentials - Credentials to find the permissions for. * @param credentials - Credentials to find the permissions for.
*/ */
private async determinePermissions(acl: Store, credentials?: Credential): Promise<Permission> { private async determinePermissions(acl: Store, credentials?: Credential): Promise<AclPermission> {
const permissions: Permission = {}; const aclPermissions: AclPermission = {};
if (!credentials) { if (!credentials) {
return permissions; return aclPermissions;
} }
// Apply all ACL rules // Apply all ACL rules
@ -108,18 +107,43 @@ export class WebAclReader extends PermissionReader {
const modes = acl.getObjects(rule, ACL.mode, null); const modes = acl.getObjects(rule, ACL.mode, null);
for (const { value: mode } of modes) { for (const { value: mode } of modes) {
if (mode in modesMap) { if (mode in modesMap) {
permissions[modesMap[mode]] = true; aclPermissions[modesMap[mode]] = true;
} }
} }
} }
} }
if (permissions.write) { if (aclPermissions.write) {
// Write permission implies Append permission // Write permission implies Append permission
permissions.append = true; aclPermissions.append = true;
} }
return permissions; return aclPermissions;
}
/**
* Sets the correct values for non-acl permissions such as create and delete.
* Also adds the correct values to indicate that having control permission
* implies having read/write/etc. on the acl resource.
*
* The main reason for keeping the control value is so we can correctly set the WAC-Allow header later.
*/
private updateAclPermissions(aclPermissions: AclPermission, isAcl: boolean): AclPermission {
if (isAcl) {
return {
read: aclPermissions.control,
append: aclPermissions.control,
write: aclPermissions.control,
create: aclPermissions.control,
delete: aclPermissions.control,
control: aclPermissions.control,
};
}
return {
...aclPermissions,
create: aclPermissions.write,
delete: aclPermissions.write,
};
} }
/** /**

View File

@ -154,7 +154,6 @@ export * from './ldp/operations/PostOperationHandler';
export * from './ldp/operations/PutOperationHandler'; export * from './ldp/operations/PutOperationHandler';
// LDP/Permissions // LDP/Permissions
export * from './ldp/permissions/AclModesExtractor';
export * from './ldp/permissions/Permissions'; export * from './ldp/permissions/Permissions';
export * from './ldp/permissions/ModesExtractor'; export * from './ldp/permissions/ModesExtractor';
export * from './ldp/permissions/MethodModesExtractor'; export * from './ldp/permissions/MethodModesExtractor';

View File

@ -9,6 +9,12 @@ import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'
* supported by this strategy. * supported by this strategy.
*/ */
export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy { export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
/**
* Whether this auxiliary resources uses its own authorization instead of the associated resource authorization.
* @param identifier - Identifier of the auxiliary resource.
*/
usesOwnAuthorization: (identifier: ResourceIdentifier) => boolean;
/** /**
* Whether the root storage container requires this auxiliary resource to be present. * 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.

View File

@ -14,13 +14,15 @@ 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 ownAuthorization: boolean;
private readonly requiredInRoot: boolean; private readonly requiredInRoot: boolean;
public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator, public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator,
validator?: Validator, requiredInRoot = false) { validator?: Validator, ownAuthorization = false, requiredInRoot = false) {
this.identifierStrategy = identifierStrategy; this.identifierStrategy = identifierStrategy;
this.metadataGenerator = metadataGenerator; this.metadataGenerator = metadataGenerator;
this.validator = validator; this.validator = validator;
this.ownAuthorization = ownAuthorization;
this.requiredInRoot = requiredInRoot; this.requiredInRoot = requiredInRoot;
} }
@ -40,6 +42,10 @@ export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
return this.identifierStrategy.getAssociatedIdentifier(identifier); return this.identifierStrategy.getAssociatedIdentifier(identifier);
} }
public usesOwnAuthorization(): boolean {
return this.ownAuthorization;
}
public isRequiredInRoot(): boolean { public isRequiredInRoot(): boolean {
return this.requiredInRoot; return this.requiredInRoot;
} }

View File

@ -18,6 +18,11 @@ export class RoutingAuxiliaryStrategy extends RoutingAuxiliaryIdentifierStrategy
super(sources); super(sources);
} }
public usesOwnAuthorization(identifier: ResourceIdentifier): boolean {
const source = this.getMatchingSource(identifier);
return source.usesOwnAuthorization(identifier);
}
public isRequiredInRoot(identifier: ResourceIdentifier): boolean { public isRequiredInRoot(identifier: ResourceIdentifier): boolean {
const source = this.getMatchingSource(identifier); const source = this.getMatchingSource(identifier);
return source.isRequiredInRoot(identifier); return source.isRequiredInRoot(identifier);

View File

@ -1,10 +1,13 @@
import { ACL, AUTH } from '../../../util/Vocabularies'; import { ACL, AUTH } from '../../../util/Vocabularies';
import type { AccessMode } from '../../permissions/Permissions'; import { AclMode } from '../../permissions/AclPermission';
import type { AclPermission } from '../../permissions/AclPermission';
import { AccessMode } from '../../permissions/Permissions';
import type { OperationMetadataCollectorInput } from './OperationMetadataCollector'; import type { OperationMetadataCollectorInput } from './OperationMetadataCollector';
import { OperationMetadataCollector } from './OperationMetadataCollector'; import { OperationMetadataCollector } from './OperationMetadataCollector';
const VALID_METHODS = new Set([ 'HEAD', 'GET' ]); const VALID_METHODS = new Set([ 'HEAD', 'GET' ]);
const VALID_ACL_MODES = new Set([ AccessMode.read, AccessMode.write, AccessMode.append, AclMode.control ]);
/** /**
* Indicates which acl permissions are available on the requested resource. * Indicates which acl permissions are available on the requested resource.
@ -15,18 +18,20 @@ export class WebAclMetadataCollector extends OperationMetadataCollector {
if (!operation.permissionSet || !VALID_METHODS.has(operation.method)) { if (!operation.permissionSet || !VALID_METHODS.has(operation.method)) {
return; return;
} }
const user = operation.permissionSet.agent ?? {}; const user: AclPermission = operation.permissionSet.agent ?? {};
const everyone = operation.permissionSet.public ?? {}; const everyone: AclPermission = operation.permissionSet.public ?? {};
const modes = new Set<AccessMode>([ ...Object.keys(user), ...Object.keys(everyone) ] as AccessMode[]); const modes = new Set<AccessMode>([ ...Object.keys(user), ...Object.keys(everyone) ] as AccessMode[]);
for (const mode of modes) { for (const mode of modes) {
const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control'; if (VALID_ACL_MODES.has(mode)) {
if (everyone[mode]) { const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control';
metadata.add(AUTH.terms.publicMode, ACL.terms[capitalizedMode]); if (everyone[mode]) {
} metadata.add(AUTH.terms.publicMode, ACL.terms[capitalizedMode]);
if (user[mode]) { }
metadata.add(AUTH.terms.userMode, ACL.terms[capitalizedMode]); if (user[mode]) {
metadata.add(AUTH.terms.userMode, ACL.terms[capitalizedMode]);
}
} }
} }
} }

View File

@ -1,24 +0,0 @@
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import type { AuxiliaryIdentifierStrategy } from '../auxiliary/AuxiliaryIdentifierStrategy';
import type { Operation } from '../operations/Operation';
import { ModesExtractor } from './ModesExtractor';
import { AccessMode } from './Permissions';
export class AclModesExtractor extends ModesExtractor {
private readonly aclStrategy: AuxiliaryIdentifierStrategy;
public constructor(aclStrategy: AuxiliaryIdentifierStrategy) {
super();
this.aclStrategy = aclStrategy;
}
public async canHandle({ target }: Operation): Promise<void> {
if (!this.aclStrategy.isAuxiliaryIdentifier(target)) {
throw new NotImplementedHttpError('Can only determine permissions of acl resources');
}
}
public async handle(): Promise<Set<AccessMode>> {
return new Set([ AccessMode.control ]);
}
}

View File

@ -0,0 +1,10 @@
import type { Permission } from './Permissions';
export enum AclMode {
control = 'control',
}
// Adds a control field to the permissions to specify this WAC-specific value
export type AclPermission = Permission & {
[mode in AclMode]?: boolean;
};

View File

@ -27,6 +27,8 @@ export class MethodModesExtractor extends ModesExtractor {
if (WRITE_METHODS.has(method)) { if (WRITE_METHODS.has(method)) {
result.add(AccessMode.write); result.add(AccessMode.write);
result.add(AccessMode.append); result.add(AccessMode.append);
result.add(AccessMode.create);
result.add(AccessMode.delete);
} else if (APPEND_METHODS.has(method)) { } else if (APPEND_METHODS.has(method)) {
result.add(AccessMode.append); result.add(AccessMode.append);
} }

View File

@ -7,7 +7,8 @@ export enum AccessMode {
read = 'read', read = 'read',
append = 'append', append = 'append',
write = 'write', write = 'write',
control = 'control', create = 'create',
delete = 'delete',
} }
/** /**

View File

@ -31,6 +31,8 @@ export class SparqlPatchModesExtractor extends ModesExtractor {
if (this.needsWrite(update)) { if (this.needsWrite(update)) {
result.add(AccessMode.write); result.add(AccessMode.write);
result.add(AccessMode.append); result.add(AccessMode.append);
result.add(AccessMode.create);
result.add(AccessMode.delete);
} else if (this.needsAppend(update)) { } else if (this.needsAppend(update)) {
result.add(AccessMode.append); result.add(AccessMode.append);
} }

View File

@ -177,7 +177,8 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeConfig,
const response = await fetch(`${baseUrl}.acl`); const response = await fetch(`${baseUrl}.acl`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.headers.get('wac-allow')).toBe('user="control",public="control"'); expect(response.headers.get('wac-allow'))
.toBe('user="append control read write",public="append control read write"');
// Close response // Close response
await response.text(); await response.text();

View File

@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json", "files-scs:config/util/auxiliary/acl.json",

View File

@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/dynamic.json", "files-scs:config/storage/backend/dynamic.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -17,7 +17,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json", "files-scs:config/util/auxiliary/acl.json",

View File

@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/memory.json", "files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json", "files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json", "files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json", "files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json", "files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json", "files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json", "files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json", "files-scs:config/storage/middleware/default.json",

View File

@ -7,7 +7,8 @@ function getPermissions(allow: boolean): Permission {
read: allow, read: allow,
write: allow, write: allow,
append: allow, append: allow,
control: allow, create: allow,
delete: allow,
}; };
} }

View File

@ -1,7 +1,7 @@
import { CredentialGroup } from '../../../src/authentication/Credentials'; import { CredentialGroup } from '../../../src/authentication/Credentials';
import { AuxiliaryReader } from '../../../src/authorization/AuxiliaryReader'; import { AuxiliaryReader } from '../../../src/authorization/AuxiliaryReader';
import type { PermissionReader } from '../../../src/authorization/PermissionReader'; import type { PermissionReader } from '../../../src/authorization/PermissionReader';
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { AuxiliaryStrategy } from '../../../src/ldp/auxiliary/AuxiliaryStrategy';
import type { PermissionSet } from '../../../src/ldp/permissions/Permissions'; import type { PermissionSet } from '../../../src/ldp/permissions/Permissions';
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
@ -12,8 +12,8 @@ describe('An AuxiliaryReader', (): void => {
const associatedIdentifier = { path: 'http://test.com/foo' }; const associatedIdentifier = { path: 'http://test.com/foo' };
const auxiliaryIdentifier = { path: 'http://test.com/foo.dummy' }; const auxiliaryIdentifier = { path: 'http://test.com/foo.dummy' };
const permissionSet: PermissionSet = { [CredentialGroup.agent]: { read: true }}; const permissionSet: PermissionSet = { [CredentialGroup.agent]: { read: true }};
let source: PermissionReader; let source: jest.Mocked<PermissionReader>;
let strategy: AuxiliaryIdentifierStrategy; let strategy: jest.Mocked<AuxiliaryStrategy>;
let reader: AuxiliaryReader; let reader: AuxiliaryReader;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
@ -27,6 +27,7 @@ describe('An AuxiliaryReader', (): void => {
isAuxiliaryIdentifier: jest.fn((identifier: ResourceIdentifier): boolean => identifier.path.endsWith(suffix)), isAuxiliaryIdentifier: jest.fn((identifier: ResourceIdentifier): boolean => identifier.path.endsWith(suffix)),
getAssociatedIdentifier: jest.fn((identifier: ResourceIdentifier): ResourceIdentifier => getAssociatedIdentifier: jest.fn((identifier: ResourceIdentifier): ResourceIdentifier =>
({ path: identifier.path.slice(0, -suffix.length) })), ({ path: identifier.path.slice(0, -suffix.length) })),
usesOwnAuthorization: jest.fn().mockReturnValue(false),
} as any; } as any;
reader = new AuxiliaryReader(source, strategy); reader = new AuxiliaryReader(source, strategy);
}); });
@ -39,7 +40,12 @@ describe('An AuxiliaryReader', (): void => {
); );
await expect(reader.canHandle({ identifier: associatedIdentifier, credentials })) await expect(reader.canHandle({ identifier: associatedIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError); .rejects.toThrow(NotImplementedHttpError);
source.canHandle = jest.fn().mockRejectedValue(new Error('no source support'));
strategy.usesOwnAuthorization.mockReturnValueOnce(true);
await expect(reader.canHandle({ identifier: auxiliaryIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
source.canHandle.mockRejectedValue(new Error('no source support'));
await expect(reader.canHandle({ identifier: auxiliaryIdentifier, credentials })) await expect(reader.canHandle({ identifier: auxiliaryIdentifier, credentials }))
.rejects.toThrow('no source support'); .rejects.toThrow('no source support');
}); });
@ -61,9 +67,15 @@ describe('An AuxiliaryReader', (): void => {
expect(source.handleSafe).toHaveBeenLastCalledWith( expect(source.handleSafe).toHaveBeenLastCalledWith(
{ identifier: associatedIdentifier, credentials }, { identifier: associatedIdentifier, credentials },
); );
await expect(reader.handleSafe({ identifier: associatedIdentifier, credentials })) await expect(reader.handleSafe({ identifier: associatedIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError); .rejects.toThrow(NotImplementedHttpError);
source.handleSafe = jest.fn().mockRejectedValue(new Error('no source support'));
strategy.usesOwnAuthorization.mockReturnValueOnce(true);
await expect(reader.canHandle({ identifier: auxiliaryIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
source.handleSafe.mockRejectedValue(new Error('no source support'));
await expect(reader.handleSafe({ identifier: auxiliaryIdentifier, credentials })) await expect(reader.handleSafe({ identifier: auxiliaryIdentifier, credentials }))
.rejects.toThrow('no source support'); .rejects.toThrow('no source support');
}); });

View File

@ -44,13 +44,13 @@ describe('A UnionPermissionReader', (): void => {
it('merges same fields using false > true > undefined.', async(): Promise<void> => { it('merges same fields using false > true > undefined.', async(): Promise<void> => {
readers[0].handle.mockResolvedValue( readers[0].handle.mockResolvedValue(
{ [CredentialGroup.agent]: { read: true, write: false, append: undefined, control: true }}, { [CredentialGroup.agent]: { read: true, write: false, append: undefined, create: true, delete: undefined }},
); );
readers[1].handle.mockResolvedValue( readers[1].handle.mockResolvedValue(
{ [CredentialGroup.agent]: { read: false, write: true, append: true, control: true }}, { [CredentialGroup.agent]: { read: false, write: true, append: true, create: true, delete: undefined }},
); );
await expect(unionReader.handle(input)).resolves.toEqual({ await expect(unionReader.handle(input)).resolves.toEqual({
[CredentialGroup.agent]: { read: false, write: false, append: true, control: true }, [CredentialGroup.agent]: { read: false, write: false, append: true, create: true },
}); });
}); });
}); });

View File

@ -12,7 +12,6 @@ import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError'; import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
import { InternalServerError } from '../../../src/util/errors/InternalServerError'; import { InternalServerError } from '../../../src/util/errors/InternalServerError';
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy'; import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
import { guardedStreamFrom } from '../../../src/util/StreamUtil'; import { guardedStreamFrom } from '../../../src/util/StreamUtil';
@ -51,10 +50,8 @@ describe('A WebAclReader', (): void => {
reader = new WebAclReader(aclStrategy, store, identifierStrategy, accessChecker); reader = new WebAclReader(aclStrategy, store, identifierStrategy, accessChecker);
}); });
it('handles all non-acl inputs.', async(): Promise<void> => { it('handles all input.', async(): Promise<void> => {
await expect(reader.canHandle({ identifier, credentials })).resolves.toBeUndefined(); await expect(reader.canHandle({ } as any)).resolves.toBeUndefined();
await expect(reader.canHandle({ identifier: aclStrategy.getAuxiliaryIdentifier(identifier) } as any))
.rejects.toThrow(NotImplementedHttpError);
}); });
it('returns undefined permissions for undefined credentials.', async(): Promise<void> => { it('returns undefined permissions for undefined credentials.', async(): Promise<void> => {
@ -137,15 +134,39 @@ describe('A WebAclReader', (): void => {
await expect(promise).rejects.toThrow(ForbiddenHttpError); await expect(promise).rejects.toThrow(ForbiddenHttpError);
}); });
it('allows an agent to append if they have write access.', async(): Promise<void> => { it('allows an agent to append/create/delete if they have write access.', async(): Promise<void> => {
store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([ store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([
quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)), quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
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}Write`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)),
]) } as Representation); ]) } as Representation);
await expect(reader.handle({ identifier, credentials })).resolves.toEqual({ await expect(reader.handle({ identifier, credentials })).resolves.toEqual({
[CredentialGroup.public]: { write: true, append: true }, [CredentialGroup.public]: { write: true, append: true, create: true, delete: true },
[CredentialGroup.agent]: { write: true, append: true }, [CredentialGroup.agent]: { write: true, append: true, create: true, delete: true },
});
});
it('allows everything on an acl resource if control permissions are granted.', async(): Promise<void> => {
store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([
quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)),
]) } as Representation);
await expect(reader.handle({ identifier: { path: `${identifier.path}.acl` }, credentials })).resolves.toEqual({
[CredentialGroup.public]: { read: true, write: true, append: true, create: true, delete: true, control: true },
[CredentialGroup.agent]: { read: true, write: true, append: true, create: true, delete: true, control: true },
});
});
it('rejects everything on an acl resource if there are no control permissions.', async(): Promise<void> => {
store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([
quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation);
await expect(reader.handle({ identifier: { path: `${identifier.path}.acl` }, credentials })).resolves.toEqual({
[CredentialGroup.public]: {},
[CredentialGroup.agent]: {},
}); });
}); });

View File

@ -24,7 +24,7 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
validator = { validator = {
handleSafe: jest.fn(), handleSafe: jest.fn(),
} as any; } as any;
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator, true); strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator, false, true);
}); });
it('calls the AuxiliaryIdentifierStrategy for related calls.', async(): Promise<void> => { it('calls the AuxiliaryIdentifierStrategy for related calls.', async(): Promise<void> => {
@ -45,6 +45,10 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier); expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier);
}); });
it('returns the injected value for usesOwnAuthorization.', async(): Promise<void> => {
expect(strategy.usesOwnAuthorization()).toBe(false);
});
it('returns the injected value for isRequiredInRoot.', async(): Promise<void> => { it('returns the injected value for isRequiredInRoot.', async(): Promise<void> => {
expect(strategy.isRequiredInRoot()).toBe(true); expect(strategy.isRequiredInRoot()).toBe(true);
}); });

View File

@ -11,6 +11,10 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
this.suffix = suffix; this.suffix = suffix;
} }
public usesOwnAuthorization(): boolean {
return true;
}
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier { public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: `${identifier.path}${this.suffix}` }; return { path: `${identifier.path}${this.suffix}` };
} }
@ -77,6 +81,15 @@ describe('A RoutingAuxiliaryStrategy', (): void => {
expect(sources[1].addMetadata).toHaveBeenLastCalledWith(metadata); expect(sources[1].addMetadata).toHaveBeenLastCalledWith(metadata);
}); });
it('#usesOwnAuthorization returns the result of the correct source.', async(): Promise<void> => {
sources[0].usesOwnAuthorization = jest.fn();
sources[1].usesOwnAuthorization = jest.fn();
strategy.usesOwnAuthorization(dummy2Id);
expect(sources[0].usesOwnAuthorization).toHaveBeenCalledTimes(0);
expect(sources[1].usesOwnAuthorization).toHaveBeenCalledTimes(1);
expect(sources[1].usesOwnAuthorization).toHaveBeenLastCalledWith(dummy2Id);
});
it('#isRequiredInRoot returns the result of the correct source.', async(): Promise<void> => { it('#isRequiredInRoot returns the result of the correct source.', async(): Promise<void> => {
sources[0].isRequiredInRoot = jest.fn(); sources[0].isRequiredInRoot = jest.fn();
sources[1].isRequiredInRoot = jest.fn(); sources[1].isRequiredInRoot = jest.fn();

View File

@ -2,6 +2,7 @@ import 'jest-rdf';
import { CredentialGroup } from '../../../../../src/authentication/Credentials'; import { CredentialGroup } from '../../../../../src/authentication/Credentials';
import { WebAclMetadataCollector } from '../../../../../src/ldp/operations/metadata/WebAclMetadataCollector'; import { WebAclMetadataCollector } from '../../../../../src/ldp/operations/metadata/WebAclMetadataCollector';
import type { Operation } from '../../../../../src/ldp/operations/Operation'; import type { Operation } from '../../../../../src/ldp/operations/Operation';
import type { AclPermission } from '../../../../../src/ldp/permissions/AclPermission';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import { ACL, AUTH } from '../../../../../src/util/Vocabularies'; import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
@ -38,7 +39,7 @@ describe('A WebAclMetadataCollector', (): void => {
it('adds corresponding metadata for all permissions present.', async(): Promise<void> => { it('adds corresponding metadata for all permissions present.', async(): Promise<void> => {
operation.permissionSet = { operation.permissionSet = {
[CredentialGroup.agent]: { read: true, write: true, control: false }, [CredentialGroup.agent]: { read: true, write: true, control: false } as AclPermission,
[CredentialGroup.public]: { read: true, write: false }, [CredentialGroup.public]: { read: true, write: false },
}; };
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined(); await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
@ -46,4 +47,15 @@ describe('A WebAclMetadataCollector', (): void => {
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]); expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read); expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
}); });
it('ignores unknown modes.', async(): Promise<void> => {
operation.permissionSet = {
[CredentialGroup.agent]: { read: true, create: true },
[CredentialGroup.public]: { read: true },
};
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(2);
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read ]);
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
});
}); });

View File

@ -1,26 +0,0 @@
import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import { AclModesExtractor } from '../../../../src/ldp/permissions/AclModesExtractor';
import { AccessMode } from '../../../../src/ldp/permissions/Permissions';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('An AclModesExtractor', (): void => {
let extractor: AclModesExtractor;
beforeEach(async(): Promise<void> => {
const aclStrategy = {
isAuxiliaryIdentifier: (id): boolean => id.path.endsWith('.acl'),
} as AuxiliaryIdentifierStrategy;
extractor = new AclModesExtractor(aclStrategy);
});
it('can only handle acl files.', async(): Promise<void> => {
await expect(extractor.canHandle({ target: { path: 'http://test.com/foo' }} as any))
.rejects.toThrow(NotImplementedHttpError);
await expect(extractor.canHandle({ target: { path: 'http://test.com/foo.acl' }} as any))
.resolves.toBeUndefined();
});
it('returns control permissions.', async(): Promise<void> => {
await expect(extractor.handle()).resolves.toEqual(new Set([ AccessMode.control ]));
});
});

View File

@ -29,11 +29,11 @@ describe('A MethodModesExtractor', (): void => {
it('requires write for PUT operations.', async(): Promise<void> => { it('requires write for PUT operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'PUT' } as Operation)) await expect(extractor.handle({ method: 'PUT' } as Operation))
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ])); .resolves.toEqual(new Set([ AccessMode.append, AccessMode.write, AccessMode.create, AccessMode.delete ]));
}); });
it('requires write for DELETE operations.', async(): Promise<void> => { it('requires write for DELETE operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'DELETE' } as Operation)) await expect(extractor.handle({ method: 'DELETE' } as Operation))
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ])); .resolves.toEqual(new Set([ AccessMode.append, AccessMode.write, AccessMode.create, AccessMode.delete ]));
}); });
}); });

View File

@ -58,7 +58,8 @@ describe('A SparqlPatchModesExtractor', (): void => {
factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')), factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')),
]) }, ]) },
} as unknown as Operation; } as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ])); await expect(extractor.handle(operation))
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write, AccessMode.create, AccessMode.delete ]));
}); });
it('requires append for composite operations with an insert.', async(): Promise<void> => { it('requires append for composite operations with an insert.', async(): Promise<void> => {
@ -81,6 +82,7 @@ describe('A SparqlPatchModesExtractor', (): void => {
factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')), factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')),
]) ]) }, ]) ]) },
} as unknown as Operation; } as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ])); await expect(extractor.handle(operation))
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write, AccessMode.create, AccessMode.delete ]));
}); });
}); });

View File

@ -91,6 +91,10 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
this.suffix = suffix; this.suffix = suffix;
} }
public usesOwnAuthorization(): boolean {
return true;
}
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier { public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: `${identifier.path}${this.suffix}` }; return { path: `${identifier.path}${this.suffix}` };
} }

View File

@ -1,5 +1,6 @@
import type { ResourceStore, Permission } from '../../src/'; import type { ResourceStore } from '../../src/';
import { BasicRepresentation } from '../../src/'; import { BasicRepresentation } from '../../src/';
import type { AclPermission } from '../../src/ldp/permissions/AclPermission';
export class AclHelper { export class AclHelper {
public readonly store: ResourceStore; public readonly store: ResourceStore;
@ -11,7 +12,7 @@ export class AclHelper {
public async setSimpleAcl( public async setSimpleAcl(
resource: string, resource: string,
options: { options: {
permissions: Partial<Permission>; permissions: AclPermission;
agentClass?: 'agent' | 'authenticated'; agentClass?: 'agent' | 'authenticated';
agent?: string; agent?: string;
accessTo?: boolean; accessTo?: boolean;
@ -32,7 +33,7 @@ export class AclHelper {
]; ];
for (const perm of [ 'Read', 'Append', 'Write', 'Control' ]) { for (const perm of [ 'Read', 'Append', 'Write', 'Control' ]) {
if (options.permissions[perm.toLowerCase() as keyof Permission]) { if (options.permissions[perm.toLowerCase() as keyof AclPermission]) {
acl.push(`;\n acl:mode acl:${perm}`); acl.push(`;\n acl:mode acl:${perm}`);
} }
} }