feat: Change permission interface to store identifiers

This commit is contained in:
Joachim Van Herwegen 2022-06-29 10:54:04 +02:00
parent b5d5071403
commit 23f0b37c28
11 changed files with 84 additions and 45 deletions

View File

@ -1,4 +1,5 @@
[
"AccessMap",
"Adapter",
"BaseHttpError",
"BasicConditions",
@ -8,15 +9,16 @@
"Dict",
"Error",
"EventEmitter",
"HashMap",
"HttpErrorOptions",
"HttpResponse",
"IdentifierMap",
"IdentifierSetMultiMap",
"NodeJS.Dict",
"Permission",
"PermissionSet",
"PermissionMap",
"Promise",
"Readonly",
"RegExp",
"Set",
"Settings",
"Template",
"TemplateEngine",

View File

@ -1,7 +1,6 @@
import type { CredentialSet } from '../authentication/Credentials';
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
import { AsyncHandler } from '../util/handlers/AsyncHandler';
import type { AccessMode, PermissionSet } from './permissions/Permissions';
import type { AccessMap, PermissionMap } from './permissions/Permissions';
export interface AuthorizerInput {
/**
@ -9,17 +8,13 @@ export interface AuthorizerInput {
*/
credentials: CredentialSet;
/**
* Identifier of the resource that will be read/modified.
* Requested access modes per resource.
*/
identifier: ResourceIdentifier;
requestedModes: AccessMap;
/**
* Modes that are requested on the resource.
* Actual permissions available per resource and per credential group.
*/
modes: Set<AccessMode>;
/**
* Permissions that are available for the request.
*/
permissionSet: PermissionSet;
availablePermissions: PermissionMap;
}
/**

View File

@ -1,26 +1,22 @@
import type { CredentialSet } from '../authentication/Credentials';
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
import { AsyncHandler } from '../util/handlers/AsyncHandler';
import type { AccessMode, PermissionSet } from './permissions/Permissions';
import type { AccessMap, PermissionMap } from './permissions/Permissions';
export interface PermissionReaderInput {
/**
* Credentials of the entity that wants to use the resource.
* Credentials of the entity requesting access to resources.
*/
credentials: CredentialSet;
/**
* Identifier of the resource that will be read/modified.
* For each credential, the reader will check which of the given per-resource access modes are available.
* However, non-exhaustive information about other access modes and resources can still be returned.
*/
identifier: ResourceIdentifier;
/**
* This is the minimum set of access modes the output needs to contain,
* allowing the handler to limit its search space to this set.
* However, non-exhaustive information about other access modes can still be returned.
*/
modes: Set<AccessMode>;
requestedModes: AccessMap;
}
/**
* Discovers the permissions of the given credentials on the given identifier.
* In case the reader finds no permission for the requested identifiers and credentials
* it can return an empty or incomplete map.
*/
export abstract class PermissionReader extends AsyncHandler<PermissionReaderInput, PermissionSet> {}
export abstract class PermissionReader extends AsyncHandler<PermissionReaderInput, PermissionMap> {}

View File

@ -1,8 +1,8 @@
import type { Operation } from '../../http/Operation';
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import type { AccessMode } from './Permissions';
import type { AccessMap } from './Permissions';
/**
* Extracts all {@link AccessMode}s that are necessary to execute the given {@link Operation}.
*/
export abstract class ModesExtractor extends AsyncHandler<Operation, Set<AccessMode>> {}
export abstract class ModesExtractor extends AsyncHandler<Operation, AccessMap> {}

View File

@ -1,4 +1,5 @@
import type { CredentialGroup } from '../../authentication/Credentials';
import type { IdentifierMap, IdentifierSetMultiMap } from '../../util/map/IdentifierMap';
/**
* Different modes that require permission.
@ -11,9 +12,22 @@ export enum AccessMode {
delete = 'delete',
}
/**
* Access modes per identifier.
*/
export type AccessMap = IdentifierSetMultiMap<AccessMode>;
/**
* A data interface indicating which permissions are required (based on the context).
*/
export type Permission = Partial<Record<AccessMode, boolean>>;
/**
* Permission per CredentialGroup.
*/
export type PermissionSet = Partial<Record<CredentialGroup, Permission>>;
/**
* PermissionSet per identifier.
*/
export type PermissionMap = IdentifierMap<PermissionSet>;

View File

@ -1,4 +1,4 @@
import type { PermissionSet } from '../authorization/permissions/Permissions';
import type { PermissionMap } from '../authorization/permissions/Permissions';
import type { Conditions } from '../storage/Conditions';
import type { Representation } from './representation/Representation';
import type { RepresentationPreferences } from './representation/RepresentationPreferences';
@ -27,7 +27,7 @@ export interface Operation {
/**
* The permissions available for the current operation.
*/
permissionSet?: PermissionSet;
availablePermissions?: PermissionMap;
/**
* Representation of the body and metadata headers.
*/

View File

@ -438,6 +438,7 @@ export * from './util/locking/VoidLocker';
// Util/Map
export * from './util/map/HashMap';
export * from './util/map/IdentifierMap';
export * from './util/map/SetMultiMap';
export * from './util/map/WrappedSetMultiMap';

View File

@ -63,15 +63,15 @@ export class AuthorizingHttpHandler extends OperationHttpHandler {
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
const modes = await this.modesExtractor.handleSafe(operation);
this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
const requestedModes = await this.modesExtractor.handleSafe(operation);
this.logger.verbose(`Retrieved required modes: ${[ ...requestedModes ].join(',')}`);
const permissionSet = await this.permissionReader.handleSafe({ credentials, identifier: operation.target, modes });
this.logger.verbose(`Available permissions are ${JSON.stringify(permissionSet)}`);
const availablePermissions = await this.permissionReader.handleSafe({ credentials, requestedModes });
this.logger.verbose(`Available permissions are ${JSON.stringify(availablePermissions)}`);
try {
await this.authorizer.handleSafe({ credentials, identifier: operation.target, modes, permissionSet });
operation.permissionSet = permissionSet;
await this.authorizer.handleSafe({ credentials, requestedModes, availablePermissions });
operation.availablePermissions = availablePermissions;
} catch (error: unknown) {
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
throw error;

View File

@ -0,0 +1,28 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { HashMap } from './HashMap';
import { WrappedSetMultiMap } from './WrappedSetMultiMap';
/**
* Converts a {@link ResourceIdentifier} into a string unique to that identifier.
*/
export function identifierHashFn(identifier: ResourceIdentifier): string {
return identifier.path;
}
/**
* A specific implementation of {@link HashMap} where the key type is {@link ResourceIdentifier}.
*/
export class IdentifierMap<T> extends HashMap<ResourceIdentifier, T> {
public constructor(iterable?: Iterable<readonly [ResourceIdentifier, T]>) {
super(identifierHashFn, iterable);
}
}
/**
* A specific implementation of {@link WrappedSetMultiMap} where the key type is {@link ResourceIdentifier}.
*/
export class IdentifierSetMultiMap<T> extends WrappedSetMultiMap<ResourceIdentifier, T> {
public constructor(iterable?: Iterable<readonly [ResourceIdentifier, T | ReadonlySet<T>]>) {
super(IdentifierMap, iterable);
}
}

View File

@ -3,6 +3,7 @@ import type { CredentialsExtractor } from '../../../src/authentication/Credentia
import type { Authorizer } from '../../../src/authorization/Authorizer';
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
import type { ModesExtractor } from '../../../src/authorization/permissions/ModesExtractor';
import type { AccessMap, PermissionMap } from '../../../src/authorization/permissions/Permissions';
import { AccessMode } from '../../../src/authorization/permissions/Permissions';
import type { Operation } from '../../../src/http/Operation';
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
@ -11,11 +12,15 @@ import type { HttpRequest } from '../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../src/server/HttpResponse';
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/IdentifierMap';
describe('An AuthorizingHttpHandler', (): void => {
const credentials = { [CredentialGroup.public]: {}};
const modes = new Set([ AccessMode.read ]);
const permissionSet = { [CredentialGroup.public]: { read: true }};
const target = { path: 'http://test.com/foo' };
const requestedModes: AccessMap = new IdentifierSetMultiMap<AccessMode>([[ target, AccessMode.read ]]);
const availablePermissions: PermissionMap = new IdentifierMap(
[[ target, { [CredentialGroup.public]: { read: true }}]],
);
const request: HttpRequest = {} as any;
const response: HttpResponse = {} as any;
let operation: Operation;
@ -28,7 +33,7 @@ describe('An AuthorizingHttpHandler', (): void => {
beforeEach(async(): Promise<void> => {
operation = {
target: { path: 'http://test.com/foo' },
target,
method: 'GET',
preferences: {},
body: new BasicRepresentation(),
@ -38,10 +43,10 @@ describe('An AuthorizingHttpHandler', (): void => {
handleSafe: jest.fn().mockResolvedValue(credentials),
} as any;
modesExtractor = {
handleSafe: jest.fn().mockResolvedValue(modes),
handleSafe: jest.fn().mockResolvedValue(requestedModes),
} as any;
permissionReader = {
handleSafe: jest.fn().mockResolvedValue(permissionSet),
handleSafe: jest.fn().mockResolvedValue(availablePermissions),
} as any;
authorizer = {
handleSafe: jest.fn(),
@ -62,13 +67,12 @@ describe('An AuthorizingHttpHandler', (): void => {
expect(modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
expect(permissionReader.handleSafe).toHaveBeenCalledTimes(1);
expect(permissionReader.handleSafe).toHaveBeenLastCalledWith({ credentials, identifier: operation.target, modes });
expect(permissionReader.handleSafe).toHaveBeenLastCalledWith({ credentials, requestedModes });
expect(authorizer.handleSafe).toHaveBeenCalledTimes(1);
expect(authorizer.handleSafe)
.toHaveBeenLastCalledWith({ credentials, identifier: operation.target, modes, permissionSet });
expect(authorizer.handleSafe).toHaveBeenLastCalledWith({ credentials, requestedModes, availablePermissions });
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(source.handleSafe).toHaveBeenLastCalledWith({ request, response, operation });
expect(operation.permissionSet).toBe(permissionSet);
expect(operation.availablePermissions).toBe(availablePermissions);
});
it('errors if authorization fails.', async(): Promise<void> => {

View File

@ -17,7 +17,6 @@ describe('A MethodFilterHandler', (): void => {
operation = {
method: 'PATCH',
preferences: {},
permissionSet: {},
target: { path: 'http://example.com/foo' },
body: new BasicRepresentation(),
};