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.",
|
"DO NOT USE IN PRODUCTION. ONLY FOR DEVELOPMENT, TESTING, OR DEBUGGING.",
|
||||||
"Always allows all operations."
|
"Always allows all operations."
|
||||||
],
|
],
|
||||||
"@id": "urn:solid-server:default:Authorizer",
|
"@id": "urn:solid-server:default:PermissionReader",
|
||||||
"@type": "PermissionBasedAuthorizer",
|
"@type": "AllStaticReader",
|
||||||
"reader": {
|
"allow": true
|
||||||
"@type": "AllStaticReader",
|
|
||||||
"allow": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -6,26 +6,23 @@
|
|||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
"comment": "Uses Web Access Control for authorization.",
|
"comment": "Uses Web Access Control for authorization.",
|
||||||
"@id": "urn:solid-server:default:Authorizer",
|
"@id": "urn:solid-server:default:PermissionReader",
|
||||||
"@type": "PermissionBasedAuthorizer",
|
"@type": "UnionPermissionReader",
|
||||||
"reader": {
|
"readers": [
|
||||||
"@type": "UnionPermissionReader",
|
{
|
||||||
"readers": [
|
"comment": "This PermissionReader will be used to prevent external access to containers used for internal storage.",
|
||||||
{
|
"@id": "urn:solid-server:default:PathBasedReader",
|
||||||
"comment": "This PermissionReader will be used to prevent external access to containers used for internal storage.",
|
"@type": "PathBasedReader",
|
||||||
"@id": "urn:solid-server:default:PathBasedReader",
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
"@type": "PathBasedReader",
|
},
|
||||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
{
|
||||||
},
|
"comment": "This PermissionReader makes sure that for auxiliary resources, the main reader gets called with the associated identifier.",
|
||||||
{
|
"@type": "AuxiliaryReader",
|
||||||
"comment": "This PermissionReader makes sure that for auxiliary resources, the main reader gets called with the associated identifier.",
|
"resourceReader": { "@id": "urn:solid-server:default:WebAclReader" },
|
||||||
"@type": "AuxiliaryReader",
|
"auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" }
|
||||||
"resourceReader": { "@id": "urn:solid-server:default:WebAclReader" },
|
},
|
||||||
"auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" }
|
{ "@id": "urn:solid-server:default:WebAclReader" }
|
||||||
},
|
]
|
||||||
{ "@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": [
|
"handlers": [
|
||||||
{
|
{
|
||||||
"@type": "GetOperationHandler",
|
"@type": "GetOperationHandler",
|
||||||
"store": { "@id": "urn:solid-server:default:ResourceStore" }
|
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "PostOperationHandler",
|
"@type": "PostOperationHandler",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "HeadOperationHandler",
|
"@type": "HeadOperationHandler",
|
||||||
"store": { "@id": "urn:solid-server:default:ResourceStore" }
|
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "PatchOperationHandler",
|
"@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",
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
"import": [
|
"import": [
|
||||||
|
"files-scs:config/ldp/handler/components/authorizer.json",
|
||||||
"files-scs:config/ldp/handler/components/error-handler.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-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/request-parser.json",
|
||||||
"files-scs:config/ldp/handler/components/response-writer.json"
|
"files-scs:config/ldp/handler/components/response-writer.json"
|
||||||
],
|
],
|
||||||
@ -14,10 +16,12 @@
|
|||||||
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
|
||||||
"args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
|
"args_credentialsExtractor": { "@id": "urn:solid-server:default:CredentialsExtractor" },
|
||||||
"args_modesExtractor": { "@id": "urn:solid-server:default:ModesExtractor" },
|
"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_authorizer": { "@id": "urn:solid-server:default:Authorizer" },
|
||||||
"args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" },
|
"args_operationHandler": { "@id": "urn:solid-server:default:OperationHandler" },
|
||||||
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
|
"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 { 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 type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||||
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
||||||
import type { Authorization } from './Authorization';
|
|
||||||
|
|
||||||
export interface AuthorizerInput {
|
export interface AuthorizerInput {
|
||||||
/**
|
/**
|
||||||
@ -17,10 +16,14 @@ export interface AuthorizerInput {
|
|||||||
* Modes that are requested on the resource.
|
* Modes that are requested on the resource.
|
||||||
*/
|
*/
|
||||||
modes: Set<AccessMode>;
|
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.
|
* 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.
|
* 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 { CredentialSet } from '../authentication/Credentials';
|
||||||
import type { AccessMode, PermissionSet } from '../ldp/permissions/Permissions';
|
import type { AccessMode, PermissionSet } from '../ldp/permissions/Permissions';
|
||||||
|
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
|
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
|
||||||
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
|
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
|
||||||
import type { Authorization } from './Authorization';
|
|
||||||
import type { AuthorizerInput } from './Authorizer';
|
import type { AuthorizerInput } from './Authorizer';
|
||||||
import { Authorizer } 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.
|
* 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 {
|
export class PermissionBasedAuthorizer extends Authorizer {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly reader: PermissionReader;
|
public async handle(input: AuthorizerInput): Promise<void> {
|
||||||
|
const { credentials, modes, identifier, permissionSet } = input;
|
||||||
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 ?? {});
|
|
||||||
|
|
||||||
const modeString = [ ...modes ].join(',');
|
const modeString = [ ...modes ].join(',');
|
||||||
this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modeString} permissions for ${identifier.path}`);
|
this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modeString} permissions for ${identifier.path}`);
|
||||||
|
|
||||||
for (const mode of modes) {
|
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}`);
|
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/UnsecureConstantCredentialsExtractor';
|
||||||
export * from './authentication/UnsecureWebIdExtractor';
|
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
|
// Authorization
|
||||||
export * from './authorization/AllStaticReader';
|
export * from './authorization/AllStaticReader';
|
||||||
export * from './authorization/Authorization';
|
|
||||||
export * from './authorization/Authorizer';
|
export * from './authorization/Authorizer';
|
||||||
export * from './authorization/AuxiliaryReader';
|
export * from './authorization/AuxiliaryReader';
|
||||||
export * from './authorization/PathBasedReader';
|
export * from './authorization/PathBasedReader';
|
||||||
export * from './authorization/PermissionBasedAuthorizer';
|
export * from './authorization/PermissionBasedAuthorizer';
|
||||||
export * from './authorization/PermissionReader';
|
export * from './authorization/PermissionReader';
|
||||||
export * from './authorization/UnionPermissionReader';
|
export * from './authorization/UnionPermissionReader';
|
||||||
export * from './authorization/WebAclAuthorization';
|
|
||||||
export * from './authorization/WebAclReader';
|
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
|
// Identity/Configuration
|
||||||
export * from './identity/configuration/IdentityProviderFactory';
|
export * from './identity/configuration/IdentityProviderFactory';
|
||||||
export * from './identity/configuration/ProviderFactory';
|
export * from './identity/configuration/ProviderFactory';
|
||||||
@ -141,6 +139,10 @@ export * from './ldp/http/SparqlUpdateBodyParser';
|
|||||||
export * from './ldp/http/SparqlUpdatePatch';
|
export * from './ldp/http/SparqlUpdatePatch';
|
||||||
export * from './ldp/http/TargetExtractor';
|
export * from './ldp/http/TargetExtractor';
|
||||||
|
|
||||||
|
// LDP/Operations/Metadata
|
||||||
|
export * from './ldp/operations/metadata/OperationMetadataCollector';
|
||||||
|
export * from './ldp/operations/metadata/WebAclMetadataCollector';
|
||||||
|
|
||||||
// LDP/Operations
|
// LDP/Operations
|
||||||
export * from './ldp/operations/DeleteOperationHandler';
|
export * from './ldp/operations/DeleteOperationHandler';
|
||||||
export * from './ldp/operations/GetOperationHandler';
|
export * from './ldp/operations/GetOperationHandler';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { CredentialSet } from '../authentication/Credentials';
|
import type { CredentialSet } from '../authentication/Credentials';
|
||||||
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
|
import type { CredentialsExtractor } from '../authentication/CredentialsExtractor';
|
||||||
import type { Authorizer } from '../authorization/Authorizer';
|
import type { Authorizer } from '../authorization/Authorizer';
|
||||||
|
import type { PermissionReader } from '../authorization/PermissionReader';
|
||||||
import { BaseHttpHandler } from '../server/BaseHttpHandler';
|
import { BaseHttpHandler } from '../server/BaseHttpHandler';
|
||||||
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler';
|
||||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
import type { HttpHandlerInput } from '../server/HttpHandler';
|
||||||
@ -9,6 +10,7 @@ import type { ErrorHandler } from './http/ErrorHandler';
|
|||||||
import type { RequestParser } from './http/RequestParser';
|
import type { RequestParser } from './http/RequestParser';
|
||||||
import type { ResponseDescription } from './http/response/ResponseDescription';
|
import type { ResponseDescription } from './http/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from './http/ResponseWriter';
|
import type { ResponseWriter } from './http/ResponseWriter';
|
||||||
|
import type { OperationMetadataCollector } from './operations/metadata/OperationMetadataCollector';
|
||||||
import type { Operation } from './operations/Operation';
|
import type { Operation } from './operations/Operation';
|
||||||
import type { OperationHandler } from './operations/OperationHandler';
|
import type { OperationHandler } from './operations/OperationHandler';
|
||||||
import type { ModesExtractor } from './permissions/ModesExtractor';
|
import type { ModesExtractor } from './permissions/ModesExtractor';
|
||||||
@ -26,6 +28,10 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
|
|||||||
* Extracts the required modes from the generated Operation.
|
* Extracts the required modes from the generated Operation.
|
||||||
*/
|
*/
|
||||||
modesExtractor: ModesExtractor;
|
modesExtractor: ModesExtractor;
|
||||||
|
/**
|
||||||
|
* Reads the permissions available for the Operation.
|
||||||
|
*/
|
||||||
|
permissionReader: PermissionReader;
|
||||||
/**
|
/**
|
||||||
* Verifies if the requested operation is allowed.
|
* Verifies if the requested operation is allowed.
|
||||||
*/
|
*/
|
||||||
@ -34,6 +40,10 @@ export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs {
|
|||||||
* Executed the operation.
|
* Executed the operation.
|
||||||
*/
|
*/
|
||||||
operationHandler: OperationHandler;
|
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 {
|
export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
||||||
private readonly credentialsExtractor: CredentialsExtractor;
|
private readonly credentialsExtractor: CredentialsExtractor;
|
||||||
private readonly modesExtractor: ModesExtractor;
|
private readonly modesExtractor: ModesExtractor;
|
||||||
|
private readonly permissionReader: PermissionReader;
|
||||||
private readonly authorizer: Authorizer;
|
private readonly authorizer: Authorizer;
|
||||||
private readonly operationHandler: OperationHandler;
|
private readonly operationHandler: OperationHandler;
|
||||||
|
private readonly operationMetadataCollector: OperationMetadataCollector;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the handler.
|
* Creates the handler.
|
||||||
@ -53,8 +65,10 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
|||||||
super(args);
|
super(args);
|
||||||
this.credentialsExtractor = args.credentialsExtractor;
|
this.credentialsExtractor = args.credentialsExtractor;
|
||||||
this.modesExtractor = args.modesExtractor;
|
this.modesExtractor = args.modesExtractor;
|
||||||
|
this.permissionReader = args.permissionReader;
|
||||||
this.authorizer = args.authorizer;
|
this.authorizer = args.authorizer;
|
||||||
this.operationHandler = args.operationHandler;
|
this.operationHandler = args.operationHandler;
|
||||||
|
this.operationMetadataCollector = args.operationMetadataCollector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,16 +97,24 @@ export class AuthenticatedLdpHandler extends BaseHttpHandler {
|
|||||||
const modes = await this.modesExtractor.handleSafe(operation);
|
const modes = await this.modesExtractor.handleSafe(operation);
|
||||||
this.logger.verbose(`Required modes are read: ${[ ...modes ].join(',')}`);
|
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 {
|
try {
|
||||||
const authorization = await this.authorizer
|
await this.authorizer.handleSafe({ credentials, identifier: operation.target, modes, permissionSet });
|
||||||
.handleSafe({ credentials, identifier: operation.target, modes });
|
operation.permissionSet = permissionSet;
|
||||||
operation.authorization = authorization;
|
|
||||||
} 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.verbose(`Authorization succeeded, performing operation`);
|
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> {
|
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||||
const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions);
|
const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions);
|
||||||
|
|
||||||
input.authorization?.addMetadata(body.metadata);
|
|
||||||
|
|
||||||
return new OkResponseDescription(body.metadata, body.data);
|
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.
|
// Close the Readable as we will not return it.
|
||||||
body.data.destroy();
|
body.data.destroy();
|
||||||
|
|
||||||
input.authorization?.addMetadata(body.metadata);
|
|
||||||
|
|
||||||
return new OkResponseDescription(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 { Conditions } from '../../storage/Conditions';
|
||||||
|
import type { PermissionSet } from '../permissions/Permissions';
|
||||||
import type { Representation } from '../representation/Representation';
|
import type { Representation } from '../representation/Representation';
|
||||||
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
||||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||||
@ -25,9 +25,9 @@ export interface Operation {
|
|||||||
*/
|
*/
|
||||||
conditions?: Conditions;
|
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.
|
* 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 { CredentialGroup } from '../../../src/authentication/Credentials';
|
||||||
import type { AuthorizerInput } from '../../../src/authorization/Authorizer';
|
import type { AuthorizerInput } from '../../../src/authorization/Authorizer';
|
||||||
import { PermissionBasedAuthorizer } from '../../../src/authorization/PermissionBasedAuthorizer';
|
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 { AccessMode } from '../../../src/ldp/permissions/Permissions';
|
||||||
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
|
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
|
||||||
import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError';
|
import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError';
|
||||||
|
|
||||||
describe('A PermissionBasedAuthorizer', (): void => {
|
describe('A PermissionBasedAuthorizer', (): void => {
|
||||||
let input: AuthorizerInput;
|
let input: AuthorizerInput;
|
||||||
let authorization: WebAclAuthorization;
|
|
||||||
let reader: jest.Mocked<PermissionReader>;
|
|
||||||
let authorizer: PermissionBasedAuthorizer;
|
let authorizer: PermissionBasedAuthorizer;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
input = {
|
input = {
|
||||||
identifier: { path: 'http://test.com/foo' },
|
identifier: { path: 'http://test.com/foo' },
|
||||||
modes: new Set<AccessMode>(),
|
modes: new Set<AccessMode>(),
|
||||||
|
permissionSet: {},
|
||||||
credentials: {},
|
credentials: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
authorization = new WebAclAuthorization({}, {});
|
authorizer = new PermissionBasedAuthorizer();
|
||||||
|
|
||||||
reader = {
|
|
||||||
canHandle: jest.fn(),
|
|
||||||
handle: jest.fn().mockResolvedValue({}),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
authorizer = new PermissionBasedAuthorizer(reader);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
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> => {
|
it('allows access if the permissions are matched by the reader output.', async(): Promise<void> => {
|
||||||
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
||||||
reader.handle.mockResolvedValueOnce({
|
input.permissionSet = {
|
||||||
[CredentialGroup.public]: { read: true, write: false },
|
[CredentialGroup.public]: { read: true, write: false },
|
||||||
[CredentialGroup.agent]: { write: true },
|
[CredentialGroup.agent]: { write: true },
|
||||||
});
|
};
|
||||||
Object.assign(authorization.everyone, { read: true, write: false });
|
await expect(authorizer.handle(input)).resolves.toBeUndefined();
|
||||||
Object.assign(authorization.user, { write: true });
|
|
||||||
await expect(authorizer.handle(input)).resolves.toEqual(authorization);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an UnauthorizedHttpError when an unauthenticated request has no access.', async(): Promise<void> => {
|
it('throws an UnauthorizedHttpError when an unauthenticated request has no access.', async(): Promise<void> => {
|
||||||
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
||||||
reader.handle.mockResolvedValueOnce({
|
input.permissionSet = {
|
||||||
[CredentialGroup.public]: { read: true, write: false },
|
[CredentialGroup.public]: { read: true, write: false },
|
||||||
});
|
};
|
||||||
await expect(authorizer.handle(input)).rejects.toThrow(UnauthorizedHttpError);
|
await expect(authorizer.handle(input)).rejects.toThrow(UnauthorizedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a ForbiddenHttpError when an authenticated request has no access.', async(): Promise<void> => {
|
it('throws a ForbiddenHttpError when an authenticated request has no access.', async(): Promise<void> => {
|
||||||
input.credentials = { agent: { webId: 'http://test.com/#me' }};
|
input.credentials = { agent: { webId: 'http://test.com/#me' }};
|
||||||
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
input.modes = new Set([ AccessMode.read, AccessMode.write ]);
|
||||||
reader.handle.mockResolvedValueOnce({
|
input.permissionSet = {
|
||||||
[CredentialGroup.public]: { read: true, write: false },
|
[CredentialGroup.public]: { read: true, write: false },
|
||||||
});
|
};
|
||||||
await expect(authorizer.handle(input)).rejects.toThrow(ForbiddenHttpError);
|
await expect(authorizer.handle(input)).rejects.toThrow(ForbiddenHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defaults to empty permissions for the Authorization.', async(): Promise<void> => {
|
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 { CredentialSet } from '../../../src/authentication/Credentials';
|
||||||
import type { Authorization } from '../../../src/authorization/Authorization';
|
|
||||||
import type { AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler';
|
import type { AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler';
|
||||||
import { AuthenticatedLdpHandler } 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 { ResetResponseDescription } from '../../../src/ldp/http/response/ResetResponseDescription';
|
||||||
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
import type { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription';
|
||||||
import type { Operation } from '../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../src/ldp/operations/Operation';
|
||||||
|
import type { PermissionSet } from '../../../src/ldp/permissions/Permissions';
|
||||||
import { AccessMode } 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 type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences';
|
||||||
import * as LogUtil from '../../../src/logging/LogUtil';
|
import * as LogUtil from '../../../src/logging/LogUtil';
|
||||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||||
@ -18,7 +21,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
|
|||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
const credentials: CredentialSet = {};
|
const credentials: CredentialSet = {};
|
||||||
const modes: Set<AccessMode> = new Set([ AccessMode.read ]);
|
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 result: ResponseDescription = new ResetResponseDescription();
|
||||||
const errorResult: ResponseDescription = { statusCode: 500 };
|
const errorResult: ResponseDescription = { statusCode: 500 };
|
||||||
let args: AuthenticatedLdpHandlerArgs;
|
let args: AuthenticatedLdpHandlerArgs;
|
||||||
@ -33,8 +36,10 @@ describe('An AuthenticatedLdpHandler', (): void => {
|
|||||||
} as any,
|
} as any,
|
||||||
credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
|
credentialsExtractor: { handleSafe: jest.fn().mockResolvedValue(credentials) } as any,
|
||||||
modesExtractor: { handleSafe: jest.fn().mockResolvedValue(modes) } 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,
|
operationHandler: { handleSafe: jest.fn().mockResolvedValue(result) } as any,
|
||||||
|
operationMetadataCollector: { handleSafe: jest.fn() } as any,
|
||||||
errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any,
|
errorHandler: { handleSafe: jest.fn().mockResolvedValue(errorResult) } as any,
|
||||||
responseWriter: { handleSafe: jest.fn() } as any,
|
responseWriter: { handleSafe: jest.fn() } as any,
|
||||||
};
|
};
|
||||||
@ -65,17 +70,28 @@ describe('An AuthenticatedLdpHandler', (): void => {
|
|||||||
expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
|
expect(args.credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
|
||||||
expect(args.modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
expect(args.modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(args.modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(args.authorizer.handleSafe)
|
expect(args.authorizer.handleSafe)
|
||||||
.toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, modes });
|
.toHaveBeenLastCalledWith({ credentials, identifier: { path: 'identifier' }, modes, permissionSet });
|
||||||
expect(operation.authorization).toBe(authorization);
|
|
||||||
expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(args.operationHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation);
|
expect(args.operationHandler.handleSafe).toHaveBeenLastCalledWith(operation);
|
||||||
|
expect(args.operationMetadataCollector.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
expect(args.errorHandler.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(args.responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(args.responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result });
|
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> => {
|
it('sets preferences to text/plain in case of an error during request parsing.', async(): Promise<void> => {
|
||||||
const error = new Error('bad request!');
|
const error = new Error('bad request!');
|
||||||
(args.requestParser.handleSafe as jest.Mock).mockRejectedValueOnce(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 { GetOperationHandler } from '../../../../src/ldp/operations/GetOperationHandler';
|
||||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
@ -34,16 +33,4 @@ describe('A GetOperationHandler', (): void => {
|
|||||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
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 { Readable } from 'stream';
|
||||||
import type { Authorization } from '../../../../src/authorization/Authorization';
|
|
||||||
import { HeadOperationHandler } from '../../../../src/ldp/operations/HeadOperationHandler';
|
import { HeadOperationHandler } from '../../../../src/ldp/operations/HeadOperationHandler';
|
||||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
@ -20,6 +19,7 @@ describe('A HeadOperationHandler', (): void => {
|
|||||||
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
||||||
({ binary: false, data, metadata: 'metadata' } as any)),
|
({ binary: false, data, metadata: 'metadata' } as any)),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
handler = new HeadOperationHandler(store);
|
handler = new HeadOperationHandler(store);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -38,16 +38,4 @@ describe('A HeadOperationHandler', (): void => {
|
|||||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||||
expect(store.getRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, preferences, conditions);
|
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