diff --git a/.componentsignore b/.componentsignore index 7c399b333..6c0ed1314 100644 --- a/.componentsignore +++ b/.componentsignore @@ -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", diff --git a/src/authorization/Authorizer.ts b/src/authorization/Authorizer.ts index eeb5c4f04..42bb4631c 100644 --- a/src/authorization/Authorizer.ts +++ b/src/authorization/Authorizer.ts @@ -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; - /** - * Permissions that are available for the request. - */ - permissionSet: PermissionSet; + availablePermissions: PermissionMap; } /** diff --git a/src/authorization/PermissionReader.ts b/src/authorization/PermissionReader.ts index c615b6513..ab1c3857e 100644 --- a/src/authorization/PermissionReader.ts +++ b/src/authorization/PermissionReader.ts @@ -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; + 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 {} +export abstract class PermissionReader extends AsyncHandler {} diff --git a/src/authorization/permissions/ModesExtractor.ts b/src/authorization/permissions/ModesExtractor.ts index 1ded7055f..1c918b278 100644 --- a/src/authorization/permissions/ModesExtractor.ts +++ b/src/authorization/permissions/ModesExtractor.ts @@ -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> {} +export abstract class ModesExtractor extends AsyncHandler {} diff --git a/src/authorization/permissions/Permissions.ts b/src/authorization/permissions/Permissions.ts index 5129456f2..e296142f7 100644 --- a/src/authorization/permissions/Permissions.ts +++ b/src/authorization/permissions/Permissions.ts @@ -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; + /** * A data interface indicating which permissions are required (based on the context). */ export type Permission = Partial>; +/** + * Permission per CredentialGroup. + */ export type PermissionSet = Partial>; + +/** + * PermissionSet per identifier. + */ +export type PermissionMap = IdentifierMap; diff --git a/src/http/Operation.ts b/src/http/Operation.ts index cde1388e8..06fb625f4 100644 --- a/src/http/Operation.ts +++ b/src/http/Operation.ts @@ -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. */ diff --git a/src/index.ts b/src/index.ts index 98550f716..bfb942bce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/server/AuthorizingHttpHandler.ts b/src/server/AuthorizingHttpHandler.ts index b23ea3584..6794ee7ca 100644 --- a/src/server/AuthorizingHttpHandler.ts +++ b/src/server/AuthorizingHttpHandler.ts @@ -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; diff --git a/src/util/map/IdentifierMap.ts b/src/util/map/IdentifierMap.ts new file mode 100644 index 000000000..5023b1646 --- /dev/null +++ b/src/util/map/IdentifierMap.ts @@ -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 extends HashMap { + public constructor(iterable?: Iterable) { + super(identifierHashFn, iterable); + } +} + +/** + * A specific implementation of {@link WrappedSetMultiMap} where the key type is {@link ResourceIdentifier}. + */ +export class IdentifierSetMultiMap extends WrappedSetMultiMap { + public constructor(iterable?: Iterable]>) { + super(IdentifierMap, iterable); + } +} diff --git a/test/unit/server/AuthorizingHttpHandler.test.ts b/test/unit/server/AuthorizingHttpHandler.test.ts index 0f2f91a25..01b3403e0 100644 --- a/test/unit/server/AuthorizingHttpHandler.test.ts +++ b/test/unit/server/AuthorizingHttpHandler.test.ts @@ -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([[ 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 => { 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 => { diff --git a/test/unit/util/handlers/MethodFilterHandler.test.ts b/test/unit/util/handlers/MethodFilterHandler.test.ts index 8f688144c..9025d1e38 100644 --- a/test/unit/util/handlers/MethodFilterHandler.test.ts +++ b/test/unit/util/handlers/MethodFilterHandler.test.ts @@ -17,7 +17,6 @@ describe('A MethodFilterHandler', (): void => { operation = { method: 'PATCH', preferences: {}, - permissionSet: {}, target: { path: 'http://example.com/foo' }, body: new BasicRepresentation(), };