mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create OperationMetadataCollector to handle operation metadata
This commit is contained in:
parent
bf28c83ffa
commit
5104cd56e8
@ -6,12 +6,9 @@
|
||||
"DO NOT USE IN PRODUCTION. ONLY FOR DEVELOPMENT, TESTING, OR DEBUGGING.",
|
||||
"Always allows all operations."
|
||||
],
|
||||
"@id": "urn:solid-server:default:Authorizer",
|
||||
"@type": "PermissionBasedAuthorizer",
|
||||
"reader": {
|
||||
"@id": "urn:solid-server:default:PermissionReader",
|
||||
"@type": "AllStaticReader",
|
||||
"allow": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -6,9 +6,7 @@
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Uses Web Access Control for authorization.",
|
||||
"@id": "urn:solid-server:default:Authorizer",
|
||||
"@type": "PermissionBasedAuthorizer",
|
||||
"reader": {
|
||||
"@id": "urn:solid-server:default:PermissionReader",
|
||||
"@type": "UnionPermissionReader",
|
||||
"readers": [
|
||||
{
|
||||
@ -26,6 +24,5 @@
|
||||
{ "@id": "urn:solid-server:default:WebAclReader" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
10
config/ldp/handler/components/authorizer.json
Normal file
10
config/ldp/handler/components/authorizer.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Matches requested permissions with those available.",
|
||||
"@id": "urn:solid-server:default:Authorizer",
|
||||
"@type": "PermissionBasedAuthorizer"
|
||||
}
|
||||
]
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
"handlers": [
|
||||
{
|
||||
"@type": "GetOperationHandler",
|
||||
"store": { "@id": "urn:solid-server:default:ResourceStore" }
|
||||
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||
},
|
||||
{
|
||||
"@type": "PostOperationHandler",
|
||||
@ -23,7 +23,7 @@
|
||||
},
|
||||
{
|
||||
"@type": "HeadOperationHandler",
|
||||
"store": { "@id": "urn:solid-server:default:ResourceStore" }
|
||||
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||
},
|
||||
{
|
||||
"@type": "PatchOperationHandler",
|
||||
|
9
config/ldp/handler/components/operation-metadata.json
Normal file
9
config/ldp/handler/components/operation-metadata.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "urn:solid-server:default:OperationMetadataCollector",
|
||||
"@type": "WebAclMetadataCollector"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"files-scs:config/ldp/handler/components/authorizer.json",
|
||||
"files-scs:config/ldp/handler/components/error-handler.json",
|
||||
"files-scs:config/ldp/handler/components/operation-handler.json",
|
||||
"files-scs:config/ldp/handler/components/operation-metadata.json",
|
||||
"files-scs:config/ldp/handler/components/request-parser.json",
|
||||
"files-scs:config/ldp/handler/components/response-writer.json"
|
||||
],
|
||||
@ -14,10 +16,12 @@
|
||||
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
||||
"args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
|
||||
"args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
|
||||
"args_permissionReader": { "@id": "urn:solid-server:default:PermissionReader" },
|
||||
"args_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
|
||||
"args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" },
|
||||
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
||||
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
|
||||
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
|
||||
"args_operationMetadataCollector": { "@id": "urn:solid-server:default:OperationMetadataCollector" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
||||
|
||||
/**
|
||||
* The output of an Authorizer
|
||||
*/
|
||||
export interface Authorization {
|
||||
/**
|
||||
* Add metadata relevant for this Authorization.
|
||||
* @param metadata - Metadata to update.
|
||||
*/
|
||||
addMetadata: (metadata: RepresentationMetadata) => void;
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
import type { CredentialSet } from '../authentication/Credentials';
|
||||
import type { AccessMode } from '../ldp/permissions/Permissions';
|
||||
import type { AccessMode, PermissionSet } from '../ldp/permissions/Permissions';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
||||
import type { Authorization } from './Authorization';
|
||||
|
||||
export interface AuthorizerInput {
|
||||
/**
|
||||
@ -17,10 +16,14 @@ export interface AuthorizerInput {
|
||||
* Modes that are requested on the resource.
|
||||
*/
|
||||
modes: Set<AccessMode>;
|
||||
/**
|
||||
* Permissions that are available for the request.
|
||||
*/
|
||||
permissionSet: PermissionSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the credentials provide access with the given permissions on the resource.
|
||||
* An {@link Error} with the necessary explanation will be thrown when permissions are not granted.
|
||||
*/
|
||||
export abstract class Authorizer extends AsyncHandler<AuthorizerInput, Authorization> {}
|
||||
export abstract class Authorizer extends AsyncHandler<AuthorizerInput> {}
|
||||
|
@ -1,14 +1,10 @@
|
||||
import type { CredentialSet } from '../authentication/Credentials';
|
||||
import type { AccessMode, PermissionSet } from '../ldp/permissions/Permissions';
|
||||
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
|
||||
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
|
||||
import type { Authorization } from './Authorization';
|
||||
import type { AuthorizerInput } from './Authorizer';
|
||||
import { Authorizer } from './Authorizer';
|
||||
import type { PermissionReader } from './PermissionReader';
|
||||
import { WebAclAuthorization } from './WebAclAuthorization';
|
||||
|
||||
/**
|
||||
* Authorizer that bases its decision on the output it gets from its PermissionReader.
|
||||
@ -19,32 +15,16 @@ import { WebAclAuthorization } from './WebAclAuthorization';
|
||||
export class PermissionBasedAuthorizer extends Authorizer {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly reader: PermissionReader;
|
||||
|
||||
public constructor(reader: PermissionReader) {
|
||||
super();
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public async canHandle(input: AuthorizerInput): Promise<void> {
|
||||
return this.reader.canHandle(input);
|
||||
}
|
||||
|
||||
public async handle(input: AuthorizerInput): Promise<Authorization> {
|
||||
const { credentials, modes, identifier } = input;
|
||||
|
||||
// Read out the permissions
|
||||
const permissions = await this.reader.handle(input);
|
||||
const authorization = new WebAclAuthorization(permissions.agent ?? {}, permissions.public ?? {});
|
||||
public async handle(input: AuthorizerInput): Promise<void> {
|
||||
const { credentials, modes, identifier, permissionSet } = input;
|
||||
|
||||
const modeString = [ ...modes ].join(',');
|
||||
this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modeString} permissions for ${identifier.path}`);
|
||||
|
||||
for (const mode of modes) {
|
||||
this.requireModePermission(credentials, permissions, mode);
|
||||
this.requireModePermission(credentials, permissionSet, mode);
|
||||
}
|
||||
this.logger.debug(`${JSON.stringify(credentials)} has ${modeString} permissions for ${identifier.path}`);
|
||||
return authorization;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,36 +0,0 @@
|
||||
import type { Permission } from '../ldp/permissions/Permissions';
|
||||
import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
||||
import { ACL, AUTH } from '../util/Vocabularies';
|
||||
import type { Authorization } from './Authorization';
|
||||
|
||||
/**
|
||||
* Indicates which permissions are available on the requested resource.
|
||||
*/
|
||||
export class WebAclAuthorization implements Authorization {
|
||||
/**
|
||||
* Permissions granted to the agent requesting the resource.
|
||||
*/
|
||||
public user: Permission;
|
||||
/**
|
||||
* Permissions granted to the public.
|
||||
*/
|
||||
public everyone: Permission;
|
||||
|
||||
public constructor(user: Permission, everyone: Permission) {
|
||||
this.user = user;
|
||||
this.everyone = everyone;
|
||||
}
|
||||
|
||||
public addMetadata(metadata: RepresentationMetadata): void {
|
||||
const modes = new Set([ ...Object.keys(this.user), ...Object.keys(this.everyone) ] as (keyof Permission)[]);
|
||||
for (const mode of modes) {
|
||||
const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control';
|
||||
if (this.user[mode]) {
|
||||
metadata.add(AUTH.terms.userMode, ACL.terms[capitalizedMode]);
|
||||
}
|
||||
if (this.everyone[mode]) {
|
||||
metadata.add(AUTH.terms.publicMode, ACL.terms[capitalizedMode]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/index.ts
18
src/index.ts
@ -8,24 +8,22 @@ export * from './authentication/UnionCredentialsExtractor';
|
||||
export * from './authentication/UnsecureConstantCredentialsExtractor';
|
||||
export * from './authentication/UnsecureWebIdExtractor';
|
||||
|
||||
// Authorization/Access-Checkers
|
||||
export * from './authorization/access-checkers/AccessChecker';
|
||||
export * from './authorization/access-checkers/AgentAccessChecker';
|
||||
export * from './authorization/access-checkers/AgentClassAccessChecker';
|
||||
export * from './authorization/access-checkers/AgentGroupAccessChecker';
|
||||
|
||||
// Authorization
|
||||
export * from './authorization/AllStaticReader';
|
||||
export * from './authorization/Authorization';
|
||||
export * from './authorization/Authorizer';
|
||||
export * from './authorization/AuxiliaryReader';
|
||||
export * from './authorization/PathBasedReader';
|
||||
export * from './authorization/PermissionBasedAuthorizer';
|
||||
export * from './authorization/PermissionReader';
|
||||
export * from './authorization/UnionPermissionReader';
|
||||
export * from './authorization/WebAclAuthorization';
|
||||
export * from './authorization/WebAclReader';
|
||||
|
||||
// Authorization/access-checkers
|
||||
export * from './authorization/access-checkers/AccessChecker';
|
||||
export * from './authorization/access-checkers/AgentAccessChecker';
|
||||
export * from './authorization/access-checkers/AgentClassAccessChecker';
|
||||
export * from './authorization/access-checkers/AgentGroupAccessChecker';
|
||||
|
||||
// Identity/Configuration
|
||||
export * from './identity/configuration/IdentityProviderFactory';
|
||||
export * from './identity/configuration/ProviderFactory';
|
||||
@ -141,6 +139,10 @@ export * from './ldp/http/SparqlUpdateBodyParser';
|
||||
export * from './ldp/http/SparqlUpdatePatch';
|
||||
export * from './ldp/http/TargetExtractor';
|
||||
|
||||
// LDP/Operations/Metadata
|
||||
export * from './ldp/operations/metadata/OperationMetadataCollector';
|
||||
export * from './ldp/operations/metadata/WebAclMetadataCollector';
|
||||
|
||||
// LDP/Operations
|
||||
export * from './ldp/operations/DeleteOperationHandler';
|
||||
export * from './ldp/operations/GetOperationHandler';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { CredentialSet } from '../authentication/Credentials';
|
||||
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
|
||||
import type { Authorizer } from '../authorization/Authorizer';
|
||||
import type { PermissionReader } from '../authorization/PermissionReader';
|
||||
import { BaseHttpHandler } from '../server/BaseHttpHandler';
|
||||
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
||||
@ -9,6 +10,7 @@ import type { ErrorHandler } from './http/ErrorHandler';
|
||||
import type { RequestParser } from './http/RequestParser';
|
||||
import type { ResponseDescription } from './http/response/ResponseDescription';
|
||||
import type { ResponseWriter } from './http/ResponseWriter';
|
||||
import type { OperationMetadataCollector } from './operations/metadata/OperationMetadataCollector';
|
||||
import type { Operation } from './operations/Operation';
|
||||
import type { OperationHandler } from './operations/OperationHandler';
|
||||
import type { ModesExtractor } from './permissions/ModesExtractor';
|
||||
@ -26,6 +28,10 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
* Extracts the required modes from the generated Operation.
|
||||
*/
|
||||
modesExtractor: ModesExtractor;
|
||||
/**
|
||||
* Reads the permissions available for the Operation.
|
||||
*/
|
||||
permissionReader: PermissionReader;
|
||||
/**
|
||||
* Verifies if the requested operation is allowed.
|
||||
*/
|
||||
@ -34,6 +40,10 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
* Executed the operation.
|
||||
*/
|
||||
operationHandler: OperationHandler;
|
||||
/**
|
||||
* Generates generic operation metadata that is required for a response.
|
||||
*/
|
||||
operationMetadataCollector: OperationMetadataCollector;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,8 +52,10 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
||||
private readonly credentialsExtractor: CredentialsExtractor;
|
||||
private readonly modesExtractor: ModesExtractor;
|
||||
private readonly permissionReader: PermissionReader;
|
||||
private readonly authorizer: Authorizer;
|
||||
private readonly operationHandler: OperationHandler;
|
||||
private readonly operationMetadataCollector: OperationMetadataCollector;
|
||||
|
||||
/**
|
||||
* Creates the handler.
|
||||
@ -53,8 +65,10 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
||||
super(args);
|
||||
this.credentialsExtractor = args.credentialsExtractor;
|
||||
this.modesExtractor = args.modesExtractor;
|
||||
this.permissionReader = args.permissionReader;
|
||||
this.authorizer = args.authorizer;
|
||||
this.operationHandler = args.operationHandler;
|
||||
this.operationMetadataCollector = args.operationMetadataCollector;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,16 +97,24 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
||||
const modes = await this.modesExtractor.handleSafe(operation);
|
||||
this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
|
||||
|
||||
const permissionSet = await this.permissionReader.handleSafe({ credentials, identifier: operation.target });
|
||||
this.logger.verbose(`Available permissions are ${JSON.stringify(permissionSet)}`);
|
||||
|
||||
try {
|
||||
const authorization = await this.authorizer
|
||||
.handleSafe({ credentials, identifier: operation.target, modes });
|
||||
operation.authorization = authorization;
|
||||
await this.authorizer.handleSafe({ credentials, identifier: operation.target, modes, permissionSet });
|
||||
operation.permissionSet = permissionSet;
|
||||
} catch (error: unknown) {
|
||||
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Authorization succeeded, performing operation`);
|
||||
return this.operationHandler.handleSafe(operation);
|
||||
const response = await this.operationHandler.handleSafe(operation);
|
||||
|
||||
if (response.metadata) {
|
||||
await this.operationMetadataCollector.handleSafe({ operation, metadata: response.metadata });
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,6 @@ export class GetOperationHandler extends OperationHandler {
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions);
|
||||
|
||||
input.authorization?.addMetadata(body.metadata);
|
||||
|
||||
return new OkResponseDescription(body.metadata, body.data);
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,6 @@ export class HeadOperationHandler extends OperationHandler {
|
||||
// Close the Readable as we will not return it.
|
||||
body.data.destroy();
|
||||
|
||||
input.authorization?.addMetadata(body.metadata);
|
||||
|
||||
return new OkResponseDescription(body.metadata);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Authorization } from '../../authorization/Authorization';
|
||||
import type { Conditions } from '../../storage/Conditions';
|
||||
import type { PermissionSet } from '../permissions/Permissions';
|
||||
import type { Representation } from '../representation/Representation';
|
||||
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
@ -25,9 +25,9 @@ export interface Operation {
|
||||
*/
|
||||
conditions?: Conditions;
|
||||
/**
|
||||
* This value will be set if the Operation was authorized by an Authorizer.
|
||||
* The permissions available for the current operation.
|
||||
*/
|
||||
authorization?: Authorization;
|
||||
permissionSet?: PermissionSet;
|
||||
/**
|
||||
* Optional representation of the body.
|
||||
*/
|
||||
|
19
src/ldp/operations/metadata/OperationMetadataCollector.ts
Normal file
19
src/ldp/operations/metadata/OperationMetadataCollector.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import type { Operation } from '../Operation';
|
||||
|
||||
export interface OperationMetadataCollectorInput {
|
||||
/**
|
||||
* Metadata to update with permission knowledge.
|
||||
*/
|
||||
metadata: RepresentationMetadata;
|
||||
/**
|
||||
* Operation corresponding to the request.
|
||||
*/
|
||||
operation: Operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds metadata about the operation to the provided metadata object.
|
||||
*/
|
||||
export abstract class OperationMetadataCollector extends AsyncHandler<OperationMetadataCollectorInput> {}
|
33
src/ldp/operations/metadata/WebAclMetadataCollector.ts
Normal file
33
src/ldp/operations/metadata/WebAclMetadataCollector.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { ACL, AUTH } from '../../../util/Vocabularies';
|
||||
import type { AccessMode } from '../../permissions/Permissions';
|
||||
|
||||
import type { OperationMetadataCollectorInput } from './OperationMetadataCollector';
|
||||
import { OperationMetadataCollector } from './OperationMetadataCollector';
|
||||
|
||||
const VALID_METHODS = new Set([ 'HEAD', 'GET' ]);
|
||||
|
||||
/**
|
||||
* Indicates which acl permissions are available on the requested resource.
|
||||
* Only adds public and agent permissions for HEAD/GET requests.
|
||||
*/
|
||||
export class WebAclMetadataCollector extends OperationMetadataCollector {
|
||||
public async handle({ metadata, operation }: OperationMetadataCollectorInput): Promise<void> {
|
||||
if (!operation.permissionSet || !VALID_METHODS.has(operation.method)) {
|
||||
return;
|
||||
}
|
||||
const user = operation.permissionSet.agent ?? {};
|
||||
const everyone = 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +1,56 @@
|
||||
import { CredentialGroup } from '../../../src/authentication/Credentials';
|
||||
import type { AuthorizerInput } from '../../../src/authorization/Authorizer';
|
||||
import { PermissionBasedAuthorizer } from '../../../src/authorization/PermissionBasedAuthorizer';
|
||||
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
|
||||
import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization';
|
||||
import { AccessMode } from '../../../src/ldp/permissions/Permissions';
|
||||
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
|
||||
import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError';
|
||||
|
||||
describe('A PermissionBasedAuthorizer', (): void => {
|
||||
let input: AuthorizerInput;
|
||||
let authorization: WebAclAuthorization;
|
||||
let reader: jest.Mocked<PermissionReader>;
|
||||
let authorizer: PermissionBasedAuthorizer;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
input = {
|
||||
identifier: { path: 'http://test.com/foo' },
|
||||
modes: new Set<AccessMode>(),
|
||||
permissionSet: {},
|
||||
credentials: {},
|
||||
};
|
||||
|
||||
authorization = new WebAclAuthorization({}, {});
|
||||
|
||||
reader = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn().mockResolvedValue({}),
|
||||
} as any;
|
||||
|
||||
authorizer = new PermissionBasedAuthorizer(reader);
|
||||
authorizer = new PermissionBasedAuthorizer();
|
||||
});
|
||||
|
||||
it('can handle any input supported by its reader.', async(): Promise<void> => {
|
||||
it('can handle any input.', async(): Promise<void> => {
|
||||
await expect(authorizer.canHandle(input)).resolves.toBeUndefined();
|
||||
|
||||
reader.canHandle.mockRejectedValue(new Error('bad request'));
|
||||
await expect(authorizer.canHandle(input)).rejects.toThrow('bad request');
|
||||
});
|
||||
|
||||
it('allows access if the permissions are matched by the reader output.', async(): Promise<void> => {
|
||||
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
||||
reader.handle.mockResolvedValueOnce({
|
||||
input.permissionSet = {
|
||||
[CredentialGroup.public]: { read: true, write: false },
|
||||
[CredentialGroup.agent]: { write: true },
|
||||
});
|
||||
Object.assign(authorization.everyone, { read: true, write: false });
|
||||
Object.assign(authorization.user, { write: true });
|
||||
await expect(authorizer.handle(input)).resolves.toEqual(authorization);
|
||||
};
|
||||
await expect(authorizer.handle(input)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws an UnauthorizedHttpError when an unauthenticated request has no access.', async(): Promise<void> => {
|
||||
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
||||
reader.handle.mockResolvedValueOnce({
|
||||
input.permissionSet = {
|
||||
[CredentialGroup.public]: { read: true, write: false },
|
||||
});
|
||||
};
|
||||
await expect(authorizer.handle(input)).rejects.toThrow(UnauthorizedHttpError);
|
||||
});
|
||||
|
||||
it('throws a ForbiddenHttpError when an authenticated request has no access.', async(): Promise<void> => {
|
||||
input.credentials = { agent: { webId: 'http://test.com/#me' }};
|
||||
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
||||
reader.handle.mockResolvedValueOnce({
|
||||
input.permissionSet = {
|
||||
[CredentialGroup.public]: { read: true, write: false },
|
||||
});
|
||||
};
|
||||
await expect(authorizer.handle(input)).rejects.toThrow(ForbiddenHttpError);
|
||||
});
|
||||
|
||||
it('defaults to empty permissions for the Authorization.', async(): Promise<void> => {
|
||||
await expect(authorizer.handle(input)).resolves.toEqual(authorization);
|
||||
await expect(authorizer.handle(input)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization';
|
||||
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
||||
import { ACL, AUTH } from '../../../src/util/Vocabularies';
|
||||
import 'jest-rdf';
|
||||
|
||||
describe('A WebAclAuthorization', (): void => {
|
||||
let authorization: WebAclAuthorization;
|
||||
let metadata: RepresentationMetadata;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
authorization = new WebAclAuthorization(
|
||||
{
|
||||
read: false,
|
||||
append: false,
|
||||
write: false,
|
||||
control: false,
|
||||
},
|
||||
{
|
||||
read: false,
|
||||
append: false,
|
||||
write: false,
|
||||
control: false,
|
||||
},
|
||||
);
|
||||
|
||||
metadata = new RepresentationMetadata();
|
||||
});
|
||||
|
||||
it('adds no metadata if there are no permissions.', async(): Promise<void> => {
|
||||
expect(authorization.addMetadata(metadata)).toBeUndefined();
|
||||
expect(metadata.quads()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('adds corresponding acl metadata for all permissions present.', async(): Promise<void> => {
|
||||
authorization.user.read = true;
|
||||
authorization.user.write = true;
|
||||
authorization.everyone.read = true;
|
||||
expect(authorization.addMetadata(metadata)).toBeUndefined();
|
||||
expect(metadata.quads()).toHaveLength(3);
|
||||
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
|
||||
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
|
||||
});
|
||||
});
|
@ -1,11 +1,14 @@
|
||||
import { CredentialGroup } from '../../../src/authentication/Credentials';
|
||||
import type { CredentialSet } from '../../../src/authentication/Credentials';
|
||||
import type { Authorization } from '../../../src/authorization/Authorization';
|
||||
import type { AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler';
|
||||
import { AuthenticatedLdpHandler } from '../../../src/ldp/AuthenticatedLdpHandler';
|
||||
import { OkResponseDescription } from '../../../src/ldp/http/response/OkResponseDescription';
|
||||
import { ResetResponseDescription } from '../../../src/ldp/http/response/ResetResponseDescription';
|
||||
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
||||
import type { Operation } from '../../../src/ldp/operations/Operation';
|
||||
import type { PermissionSet } from '../../../src/ldp/permissions/Permissions';
|
||||
import { AccessMode } from '../../../src/ldp/permissions/Permissions';
|
||||
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences';
|
||||
import * as LogUtil from '../../../src/logging/LogUtil';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
@ -18,7 +21,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
|
||||
let operation: Operation;
|
||||
const credentials: CredentialSet = {};
|
||||
const modes: Set<AccessMode> = new Set([ AccessMode.read ]);
|
||||
const authorization: Authorization = { addMetadata: jest.fn() };
|
||||
const permissionSet: PermissionSet = { [CredentialGroup.agent]: { read: true }};
|
||||
const result: ResponseDescription = new ResetResponseDescription();
|
||||
const errorResult: ResponseDescription = { statusCode: 500 };
|
||||
let args: AuthenticatedLdpHandlerArgs;
|
||||
@ -33,8 +36,10 @@ describe('An AuthenticatedLdpHandler', (): void => {
|
||||
} as any,
|
||||
credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
|
||||
modesExtractor: { handleSafe: jest.fn().mockResolvedValue(modes) } as any,
|
||||
authorizer: { handleSafe: jest.fn().mockResolvedValue(authorization) } as any,
|
||||
permissionReader: { handleSafe: jest.fn().mockResolvedValue(permissionSet) } as any,
|
||||
authorizer: { handleSafe: jest.fn() } as any,
|
||||
operationHandler: { handleSafe: jest.fn().mockResolvedValue(result) } as any,
|
||||
operationMetadataCollector: { handleSafe: jest.fn() } as any,
|
||||
errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any,
|
||||
responseWriter: { handleSafe: jest.fn() } as any,
|
||||
};
|
||||
@ -65,17 +70,28 @@ describe('An AuthenticatedLdpHandler', (): void => {
|
||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
|
||||
expect(args.modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||
expect(args.permissionReader.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.permissionReader.handleSafe).toHaveBeenLastCalledWith({ credentials, identifier: operation.target });
|
||||
expect(args.authorizer.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.authorizer.handleSafe)
|
||||
.toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, modes });
|
||||
expect(operation.authorization).toBe(authorization);
|
||||
.toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, modes, permissionSet });
|
||||
expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
|
||||
});
|
||||
|
||||
it('calls the operation metadata collector if there is response metadata.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata();
|
||||
const okResult = new OkResponseDescription(metadata);
|
||||
(args.operationHandler.handleSafe as jest.Mock).mockResolvedValueOnce(okResult);
|
||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(args.operationMetadataCollector.handleSafe).toHaveBeenLastCalledWith({ operation, metadata });
|
||||
});
|
||||
|
||||
it('sets preferences to text/plain in case of an error during request parsing.', async(): Promise<void> => {
|
||||
const error = new Error('bad request!');
|
||||
(args.requestParser.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!'));
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { Authorization } from '../../../../src/authorization/Authorization';
|
||||
import { GetOperationHandler } from '../../../../src/ldp/operations/GetOperationHandler';
|
||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
@ -34,16 +33,4 @@ describe('A GetOperationHandler', (): void => {
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
||||
});
|
||||
|
||||
it('adds authorization metadata in case the operation is an AuthorizedOperation.', async(): Promise<void> => {
|
||||
const authorization: Authorization = { addMetadata: jest.fn() };
|
||||
const result = await handler.handle(
|
||||
{ target: { path: 'url' }, preferences, conditions, authorization } as Operation,
|
||||
);
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
||||
expect(authorization.addMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(authorization.addMetadata).toHaveBeenLastCalledWith('metadata');
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Readable } from 'stream';
|
||||
import type { Authorization } from '../../../../src/authorization/Authorization';
|
||||
import { HeadOperationHandler } from '../../../../src/ldp/operations/HeadOperationHandler';
|
||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
@ -20,6 +19,7 @@ describe('A HeadOperationHandler', (): void => {
|
||||
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
||||
({ binary: false, data, metadata: 'metadata' } as any)),
|
||||
} as any;
|
||||
|
||||
handler = new HeadOperationHandler(store);
|
||||
});
|
||||
|
||||
@ -38,16 +38,4 @@ describe('A HeadOperationHandler', (): void => {
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
||||
});
|
||||
|
||||
it('adds authorization metadata in case the operation is an AuthorizedOperation.', async(): Promise<void> => {
|
||||
const authorization: Authorization = { addMetadata: jest.fn() };
|
||||
const result = await handler.handle(
|
||||
{ target: { path: 'url' }, preferences, conditions, authorization } as Operation,
|
||||
);
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
||||
expect(authorization.addMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(authorization.addMetadata).toHaveBeenLastCalledWith('metadata');
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,49 @@
|
||||
import 'jest-rdf';
|
||||
import { CredentialGroup } from '../../../../../src/authentication/Credentials';
|
||||
import { WebAclMetadataCollector } from '../../../../../src/ldp/operations/metadata/WebAclMetadataCollector';
|
||||
import type { Operation } from '../../../../../src/ldp/operations/Operation';
|
||||
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
|
||||
|
||||
describe('A WebAclMetadataCollector', (): void => {
|
||||
let operation: Operation;
|
||||
let metadata: RepresentationMetadata;
|
||||
const writer = new WebAclMetadataCollector();
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = {
|
||||
method: 'GET',
|
||||
target: { path: 'http://test.com/foo' },
|
||||
preferences: {},
|
||||
};
|
||||
|
||||
metadata = new RepresentationMetadata();
|
||||
});
|
||||
|
||||
it('adds no metadata if there are no permissions.', async(): Promise<void> => {
|
||||
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
|
||||
expect(metadata.quads()).toHaveLength(0);
|
||||
|
||||
operation.permissionSet = {};
|
||||
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
|
||||
expect(metadata.quads()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('adds no metadata if the method is wrong.', async(): Promise<void> => {
|
||||
operation.permissionSet = { [CredentialGroup.public]: { read: true, write: false }};
|
||||
operation.method = 'DELETE';
|
||||
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
|
||||
expect(metadata.quads()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('adds corresponding metadata for all permissions present.', async(): Promise<void> => {
|
||||
operation.permissionSet = {
|
||||
[CredentialGroup.agent]: { read: true, write: true, control: false },
|
||||
[CredentialGroup.public]: { read: true, write: false },
|
||||
};
|
||||
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
|
||||
expect(metadata.quads()).toHaveLength(3);
|
||||
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
|
||||
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user