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

@@ -16,7 +16,8 @@ export class AllStaticReader extends PermissionReader {
read: allow,
write: 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 { getLoggerFor } from '../logging/LogUtil';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
@@ -15,9 +15,9 @@ export class AuxiliaryReader extends PermissionReader {
protected readonly logger = getLoggerFor(this);
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();
this.resourceReader = resourceReader;
this.auxiliaryStrategy = auxiliaryStrategy;
@@ -44,6 +44,11 @@ export class AuxiliaryReader extends PermissionReader {
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(auxiliaryAuth.identifier)) {
throw new NotImplementedHttpError('AuxiliaryAuthorizer only supports auxiliary resources.');
}
if (this.auxiliaryStrategy.usesOwnAuthorization(auxiliaryAuth.identifier)) {
throw new NotImplementedHttpError('Auxiliary resource uses its own permissions.');
}
return {
...auxiliaryAuth,
identifier: this.auxiliaryStrategy.getAssociatedIdentifier(auxiliaryAuth.identifier),

View File

@@ -3,7 +3,9 @@ import { Store } from 'n3';
import { CredentialGroup } from '../authentication/Credentials';
import type { Credential, CredentialSet } from '../authentication/Credentials';
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 type { Representation } from '../ldp/representation/Representation';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
@@ -14,7 +16,6 @@ import { createErrorMessage } from '../util/errors/ErrorUtil';
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
import { InternalServerError } from '../util/errors/InternalServerError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { readableToQuads } from '../util/StreamUtil';
import { ACL, RDF } from '../util/Vocabularies';
@@ -22,11 +23,11 @@ import type { AccessChecker } from './access-checkers/AccessChecker';
import type { PermissionReaderInput } from './PermissionReader';
import { PermissionReader } from './PermissionReader';
const modesMap: Record<string, AccessMode> = {
const modesMap: Record<string, keyof AclPermission> = {
[ACL.Read]: AccessMode.read,
[ACL.Write]: AccessMode.write,
[ACL.Append]: AccessMode.append,
[ACL.Control]: AccessMode.control,
[ACL.Control]: AclMode.control,
} as const;
/**
@@ -50,12 +51,6 @@ export class WebAclReader extends PermissionReader {
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.
* Will throw an error if this is not the case.
@@ -66,24 +61,28 @@ export class WebAclReader extends PermissionReader {
// Determine the required access modes
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
const acl = await this.getAclRecursive(identifier);
return this.createPermissions(credentials, acl);
const acl = await this.getAclRecursive(mainIdentifier);
return this.createPermissions(credentials, acl, isAcl);
}
/**
* Creates an Authorization object based on the quads found in the ACL.
* @param credentials - Credentials to check permissions for.
* @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> {
const publicPermissions = await this.determinePermissions(acl, credentials.public);
const agentPermissions = await this.determinePermissions(acl, credentials.agent);
return {
[CredentialGroup.agent]: agentPermissions,
[CredentialGroup.public]: publicPermissions,
[CredentialGroup.agent]: this.updateAclPermissions(agentPermissions, isAcl),
[CredentialGroup.public]: this.updateAclPermissions(publicPermissions, isAcl),
};
}
@@ -93,10 +92,10 @@ export class WebAclReader extends PermissionReader {
* @param acl - Store containing all relevant authorization triples.
* @param credentials - Credentials to find the permissions for.
*/
private async determinePermissions(acl: Store, credentials?: Credential): Promise<Permission> {
const permissions: Permission = {};
private async determinePermissions(acl: Store, credentials?: Credential): Promise<AclPermission> {
const aclPermissions: AclPermission = {};
if (!credentials) {
return permissions;
return aclPermissions;
}
// Apply all ACL rules
@@ -108,18 +107,43 @@ export class WebAclReader extends PermissionReader {
const modes = acl.getObjects(rule, ACL.mode, null);
for (const { value: mode } of modes) {
if (mode in modesMap) {
permissions[modesMap[mode]] = true;
aclPermissions[modesMap[mode]] = true;
}
}
}
}
if (permissions.write) {
if (aclPermissions.write) {
// 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';
// LDP/Permissions
export * from './ldp/permissions/AclModesExtractor';
export * from './ldp/permissions/Permissions';
export * from './ldp/permissions/ModesExtractor';
export * from './ldp/permissions/MethodModesExtractor';

View File

@@ -9,6 +9,12 @@ import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'
* supported by this strategy.
*/
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.
* 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 metadataGenerator?: MetadataGenerator;
private readonly validator?: Validator;
private readonly ownAuthorization: boolean;
private readonly requiredInRoot: boolean;
public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator,
validator?: Validator, requiredInRoot = false) {
validator?: Validator, ownAuthorization = false, requiredInRoot = false) {
this.identifierStrategy = identifierStrategy;
this.metadataGenerator = metadataGenerator;
this.validator = validator;
this.ownAuthorization = ownAuthorization;
this.requiredInRoot = requiredInRoot;
}
@@ -40,6 +42,10 @@ export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
return this.identifierStrategy.getAssociatedIdentifier(identifier);
}
public usesOwnAuthorization(): boolean {
return this.ownAuthorization;
}
public isRequiredInRoot(): boolean {
return this.requiredInRoot;
}

View File

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

View File

@@ -1,10 +1,13 @@
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 { OperationMetadataCollector } from './OperationMetadataCollector';
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.
@@ -15,18 +18,20 @@ export class WebAclMetadataCollector extends OperationMetadataCollector {
if (!operation.permissionSet || !VALID_METHODS.has(operation.method)) {
return;
}
const user = operation.permissionSet.agent ?? {};
const everyone = operation.permissionSet.public ?? {};
const user: AclPermission = operation.permissionSet.agent ?? {};
const everyone: AclPermission = operation.permissionSet.public ?? {};
const modes = new Set<AccessMode>([ ...Object.keys(user), ...Object.keys(everyone) ] as AccessMode[]);
for (const mode of modes) {
const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control';
if (everyone[mode]) {
metadata.add(AUTH.terms.publicMode, ACL.terms[capitalizedMode]);
}
if (user[mode]) {
metadata.add(AUTH.terms.userMode, ACL.terms[capitalizedMode]);
if (VALID_ACL_MODES.has(mode)) {
const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control';
if (everyone[mode]) {
metadata.add(AUTH.terms.publicMode, 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)) {
result.add(AccessMode.write);
result.add(AccessMode.append);
result.add(AccessMode.create);
result.add(AccessMode.delete);
} else if (APPEND_METHODS.has(method)) {
result.add(AccessMode.append);
}

View File

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

View File

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