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",
|
"Adapter",
|
||||||
"BaseHttpError",
|
"BaseHttpError",
|
||||||
"BasicConditions",
|
"BasicConditions",
|
||||||
@ -8,15 +9,16 @@
|
|||||||
"Dict",
|
"Dict",
|
||||||
"Error",
|
"Error",
|
||||||
"EventEmitter",
|
"EventEmitter",
|
||||||
|
"HashMap",
|
||||||
"HttpErrorOptions",
|
"HttpErrorOptions",
|
||||||
"HttpResponse",
|
"HttpResponse",
|
||||||
|
"IdentifierMap",
|
||||||
|
"IdentifierSetMultiMap",
|
||||||
"NodeJS.Dict",
|
"NodeJS.Dict",
|
||||||
"Permission",
|
"PermissionMap",
|
||||||
"PermissionSet",
|
|
||||||
"Promise",
|
"Promise",
|
||||||
"Readonly",
|
"Readonly",
|
||||||
"RegExp",
|
"RegExp",
|
||||||
"Set",
|
|
||||||
"Settings",
|
"Settings",
|
||||||
"Template",
|
"Template",
|
||||||
"TemplateEngine",
|
"TemplateEngine",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { CredentialSet } from '../authentication/Credentials';
|
import type { CredentialSet } from '../authentication/Credentials';
|
||||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
|
||||||
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
||||||
import type { AccessMode, PermissionSet } from './permissions/Permissions';
|
import type { AccessMap, PermissionMap } from './permissions/Permissions';
|
||||||
|
|
||||||
export interface AuthorizerInput {
|
export interface AuthorizerInput {
|
||||||
/**
|
/**
|
||||||
@ -9,17 +8,13 @@ export interface AuthorizerInput {
|
|||||||
*/
|
*/
|
||||||
credentials: CredentialSet;
|
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>;
|
availablePermissions: PermissionMap;
|
||||||
/**
|
|
||||||
* Permissions that are available for the request.
|
|
||||||
*/
|
|
||||||
permissionSet: PermissionSet;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,26 +1,22 @@
|
|||||||
import type { CredentialSet } from '../authentication/Credentials';
|
import type { CredentialSet } from '../authentication/Credentials';
|
||||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
|
||||||
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
||||||
import type { AccessMode, PermissionSet } from './permissions/Permissions';
|
import type { AccessMap, PermissionMap } from './permissions/Permissions';
|
||||||
|
|
||||||
export interface PermissionReaderInput {
|
export interface PermissionReaderInput {
|
||||||
/**
|
/**
|
||||||
* Credentials of the entity that wants to use the resource.
|
* Credentials of the entity requesting access to resources.
|
||||||
*/
|
*/
|
||||||
credentials: CredentialSet;
|
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;
|
requestedModes: AccessMap;
|
||||||
/**
|
|
||||||
* 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>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discovers the permissions of the given credentials on the given identifier.
|
* 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 type { Operation } from '../../http/Operation';
|
||||||
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
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}.
|
* 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 { CredentialGroup } from '../../authentication/Credentials';
|
||||||
|
import type { IdentifierMap, IdentifierSetMultiMap } from '../../util/map/IdentifierMap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Different modes that require permission.
|
* Different modes that require permission.
|
||||||
@ -11,9 +12,22 @@ export enum AccessMode {
|
|||||||
delete = 'delete',
|
delete = 'delete',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access modes per identifier.
|
||||||
|
*/
|
||||||
|
export type AccessMap = IdentifierSetMultiMap<AccessMode>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data interface indicating which permissions are required (based on the context).
|
* A data interface indicating which permissions are required (based on the context).
|
||||||
*/
|
*/
|
||||||
export type Permission = Partial<Record<AccessMode, boolean>>;
|
export type Permission = Partial<Record<AccessMode, boolean>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission per CredentialGroup.
|
||||||
|
*/
|
||||||
export type PermissionSet = Partial<Record<CredentialGroup, Permission>>;
|
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 { Conditions } from '../storage/Conditions';
|
||||||
import type { Representation } from './representation/Representation';
|
import type { Representation } from './representation/Representation';
|
||||||
import type { RepresentationPreferences } from './representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from './representation/RepresentationPreferences';
|
||||||
@ -27,7 +27,7 @@ export interface Operation {
|
|||||||
/**
|
/**
|
||||||
* The permissions available for the current operation.
|
* The permissions available for the current operation.
|
||||||
*/
|
*/
|
||||||
permissionSet?: PermissionSet;
|
availablePermissions?: PermissionMap;
|
||||||
/**
|
/**
|
||||||
* Representation of the body and metadata headers.
|
* Representation of the body and metadata headers.
|
||||||
*/
|
*/
|
||||||
|
@ -438,6 +438,7 @@ export * from './util/locking/VoidLocker';
|
|||||||
|
|
||||||
// Util/Map
|
// Util/Map
|
||||||
export * from './util/map/HashMap';
|
export * from './util/map/HashMap';
|
||||||
|
export * from './util/map/IdentifierMap';
|
||||||
export * from './util/map/SetMultiMap';
|
export * from './util/map/SetMultiMap';
|
||||||
export * from './util/map/WrappedSetMultiMap';
|
export * from './util/map/WrappedSetMultiMap';
|
||||||
|
|
||||||
|
@ -63,15 +63,15 @@ export class AuthorizingHttpHandler extends OperationHttpHandler {
|
|||||||
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
|
const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
|
||||||
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
|
this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
|
||||||
|
|
||||||
const modes = await this.modesExtractor.handleSafe(operation);
|
const requestedModes = await this.modesExtractor.handleSafe(operation);
|
||||||
this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
|
this.logger.verbose(`Retrieved required modes: ${[ ...requestedModes ].join(',')}`);
|
||||||
|
|
||||||
const permissionSet = await this.permissionReader.handleSafe({ credentials, identifier: operation.target, modes });
|
const availablePermissions = await this.permissionReader.handleSafe({ credentials, requestedModes });
|
||||||
this.logger.verbose(`Available permissions are ${JSON.stringify(permissionSet)}`);
|
this.logger.verbose(`Available permissions are ${JSON.stringify(availablePermissions)}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.authorizer.handleSafe({ credentials, identifier: operation.target, modes, permissionSet });
|
await this.authorizer.handleSafe({ credentials, requestedModes, availablePermissions });
|
||||||
operation.permissionSet = permissionSet;
|
operation.availablePermissions = availablePermissions;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
|
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
|
||||||
throw error;
|
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 { Authorizer } from '../../../src/authorization/Authorizer';
|
||||||
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
|
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
|
||||||
import type { ModesExtractor } from '../../../src/authorization/permissions/ModesExtractor';
|
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 { AccessMode } from '../../../src/authorization/permissions/Permissions';
|
||||||
import type { Operation } from '../../../src/http/Operation';
|
import type { Operation } from '../../../src/http/Operation';
|
||||||
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
|
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 { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
|
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
|
||||||
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
|
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
|
||||||
|
import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/IdentifierMap';
|
||||||
|
|
||||||
describe('An AuthorizingHttpHandler', (): void => {
|
describe('An AuthorizingHttpHandler', (): void => {
|
||||||
const credentials = { [CredentialGroup.public]: {}};
|
const credentials = { [CredentialGroup.public]: {}};
|
||||||
const modes = new Set([ AccessMode.read ]);
|
const target = { path: 'http://test.com/foo' };
|
||||||
const permissionSet = { [CredentialGroup.public]: { read: true }};
|
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 request: HttpRequest = {} as any;
|
||||||
const response: HttpResponse = {} as any;
|
const response: HttpResponse = {} as any;
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
@ -28,7 +33,7 @@ describe('An AuthorizingHttpHandler', (): void => {
|
|||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
operation = {
|
operation = {
|
||||||
target: { path: 'http://test.com/foo' },
|
target,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
preferences: {},
|
preferences: {},
|
||||||
body: new BasicRepresentation(),
|
body: new BasicRepresentation(),
|
||||||
@ -38,10 +43,10 @@ describe('An AuthorizingHttpHandler', (): void => {
|
|||||||
handleSafe: jest.fn().mockResolvedValue(credentials),
|
handleSafe: jest.fn().mockResolvedValue(credentials),
|
||||||
} as any;
|
} as any;
|
||||||
modesExtractor = {
|
modesExtractor = {
|
||||||
handleSafe: jest.fn().mockResolvedValue(modes),
|
handleSafe: jest.fn().mockResolvedValue(requestedModes),
|
||||||
} as any;
|
} as any;
|
||||||
permissionReader = {
|
permissionReader = {
|
||||||
handleSafe: jest.fn().mockResolvedValue(permissionSet),
|
handleSafe: jest.fn().mockResolvedValue(availablePermissions),
|
||||||
} as any;
|
} as any;
|
||||||
authorizer = {
|
authorizer = {
|
||||||
handleSafe: jest.fn(),
|
handleSafe: jest.fn(),
|
||||||
@ -62,13 +67,12 @@ describe('An AuthorizingHttpHandler', (): void => {
|
|||||||
expect(modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
expect(modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
|
expect(modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||||
expect(permissionReader.handleSafe).toHaveBeenCalledTimes(1);
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(authorizer.handleSafe)
|
expect(authorizer.handleSafe).toHaveBeenLastCalledWith({ credentials, requestedModes, availablePermissions });
|
||||||
.toHaveBeenLastCalledWith({ credentials, identifier: operation.target, modes, permissionSet });
|
|
||||||
expect(source.handleSafe).toHaveBeenCalledTimes(1);
|
expect(source.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(source.handleSafe).toHaveBeenLastCalledWith({ request, response, operation });
|
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> => {
|
it('errors if authorization fails.', async(): Promise<void> => {
|
||||||
|
@ -17,7 +17,6 @@ describe('A MethodFilterHandler', (): void => {
|
|||||||
operation = {
|
operation = {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
preferences: {},
|
preferences: {},
|
||||||
permissionSet: {},
|
|
||||||
target: { path: 'http://example.com/foo' },
|
target: { path: 'http://example.com/foo' },
|
||||||
body: new BasicRepresentation(),
|
body: new BasicRepresentation(),
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user