mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Change permission interface to store identifiers
This commit is contained in:
parent
b5d5071403
commit
23f0b37c28
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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> {}
|
||||
|
@ -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> {}
|
||||
|
@ -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>;
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
28
src/util/map/IdentifierMap.ts
Normal file
28
src/util/map/IdentifierMap.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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> => {
|
||||
|
@ -17,7 +17,6 @@ describe('A MethodFilterHandler', (): void => {
|
||||
operation = {
|
||||
method: 'PATCH',
|
||||
preferences: {},
|
||||
permissionSet: {},
|
||||
target: { path: 'http://example.com/foo' },
|
||||
body: new BasicRepresentation(),
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user