diff --git a/config/ldp/authorization/allow-all.json b/config/ldp/authorization/allow-all.json index af59acab2..4002792a3 100644 --- a/config/ldp/authorization/allow-all.json +++ b/config/ldp/authorization/allow-all.json @@ -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": { - "@type": "AllStaticReader", - "allow": true - } + "@id": "urn:solid-server:default:PermissionReader", + "@type": "AllStaticReader", + "allow": true } ] } diff --git a/config/ldp/authorization/webacl.json b/config/ldp/authorization/webacl.json index 85a0a7a1f..8b25b8648 100644 --- a/config/ldp/authorization/webacl.json +++ b/config/ldp/authorization/webacl.json @@ -6,26 +6,23 @@ "@graph": [ { "comment": "Uses Web Access Control for authorization.", - "@id": "urn:solid-server:default:Authorizer", - "@type": "PermissionBasedAuthorizer", - "reader": { - "@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", - "@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", - "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:PermissionReader", + "@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", + "@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", + "resourceReader": { "@id": "urn:solid-server:default:WebAclReader" }, + "auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" } + }, + { "@id": "urn:solid-server:default:WebAclReader" } + ] } ] } diff --git a/config/ldp/handler/components/authorizer.json b/config/ldp/handler/components/authorizer.json new file mode 100644 index 000000000..ee130381d --- /dev/null +++ b/config/ldp/handler/components/authorizer.json @@ -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" + } + ] +} diff --git a/config/ldp/handler/components/operation-handler.json b/config/ldp/handler/components/operation-handler.json index 37fd10f51..befe45474 100644 --- a/config/ldp/handler/components/operation-handler.json +++ b/config/ldp/handler/components/operation-handler.json @@ -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", diff --git a/config/ldp/handler/components/operation-metadata.json b/config/ldp/handler/components/operation-metadata.json new file mode 100644 index 000000000..a907f9eae --- /dev/null +++ b/config/ldp/handler/components/operation-metadata.json @@ -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" + } + ] +} diff --git a/config/ldp/handler/default.json b/config/ldp/handler/default.json index b08820164..968da17c1 100644 --- a/config/ldp/handler/default.json +++ b/config/ldp/handler/default.json @@ -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" } } ] } diff --git a/src/authorization/Authorization.ts b/src/authorization/Authorization.ts deleted file mode 100644 index f0d93a772..000000000 --- a/src/authorization/Authorization.ts +++ /dev/null @@ -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; -} diff --git a/src/authorization/Authorizer.ts b/src/authorization/Authorizer.ts index e38a0bfb4..d47389795 100644 --- a/src/authorization/Authorizer.ts +++ b/src/authorization/Authorizer.ts @@ -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; + /** + * 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 {} +export abstract class Authorizer extends AsyncHandler {} diff --git a/src/authorization/PermissionBasedAuthorizer.ts b/src/authorization/PermissionBasedAuthorizer.ts index 819067202..e1adfd832 100644 --- a/src/authorization/PermissionBasedAuthorizer.ts +++ b/src/authorization/PermissionBasedAuthorizer.ts @@ -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 { - return this.reader.canHandle(input); - } - - public async handle(input: AuthorizerInput): Promise { - 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 { + 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; } /** diff --git a/src/authorization/WebAclAuthorization.ts b/src/authorization/WebAclAuthorization.ts deleted file mode 100644 index 29eeceb36..000000000 --- a/src/authorization/WebAclAuthorization.ts +++ /dev/null @@ -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]); - } - } - } -} diff --git a/src/index.ts b/src/index.ts index 9b2c9b17e..d425f2722 100644 --- a/src/index.ts +++ b/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'; diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts index dc39c509f..abd6ff8e3 100644 --- a/src/ldp/AuthenticatedLdpHandler.ts +++ b/src/ldp/AuthenticatedLdpHandler.ts @@ -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; } } diff --git a/src/ldp/operations/GetOperationHandler.ts b/src/ldp/operations/GetOperationHandler.ts index e2c1dfc7f..8c9f5642d 100644 --- a/src/ldp/operations/GetOperationHandler.ts +++ b/src/ldp/operations/GetOperationHandler.ts @@ -26,8 +26,6 @@ export class GetOperationHandler extends OperationHandler { public async handle(input: Operation): Promise { const body = await this.store.getRepresentation(input.target, input.preferences, input.conditions); - input.authorization?.addMetadata(body.metadata); - return new OkResponseDescription(body.metadata, body.data); } } diff --git a/src/ldp/operations/HeadOperationHandler.ts b/src/ldp/operations/HeadOperationHandler.ts index 3133fb2d9..3933f3b84 100644 --- a/src/ldp/operations/HeadOperationHandler.ts +++ b/src/ldp/operations/HeadOperationHandler.ts @@ -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); } } diff --git a/src/ldp/operations/Operation.ts b/src/ldp/operations/Operation.ts index d03091b51..613a91372 100644 --- a/src/ldp/operations/Operation.ts +++ b/src/ldp/operations/Operation.ts @@ -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. */ diff --git a/src/ldp/operations/metadata/OperationMetadataCollector.ts b/src/ldp/operations/metadata/OperationMetadataCollector.ts new file mode 100644 index 000000000..fc9efd0f1 --- /dev/null +++ b/src/ldp/operations/metadata/OperationMetadataCollector.ts @@ -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 {} diff --git a/src/ldp/operations/metadata/WebAclMetadataCollector.ts b/src/ldp/operations/metadata/WebAclMetadataCollector.ts new file mode 100644 index 000000000..ebb469ba7 --- /dev/null +++ b/src/ldp/operations/metadata/WebAclMetadataCollector.ts @@ -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 { + if (!operation.permissionSet || !VALID_METHODS.has(operation.method)) { + return; + } + const user = operation.permissionSet.agent ?? {}; + const everyone = operation.permissionSet.public ?? {}; + + const modes = new Set([ ...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]); + } + } + } +} diff --git a/test/unit/authorization/PermissionBasedAuthorizer.test.ts b/test/unit/authorization/PermissionBasedAuthorizer.test.ts index 9381daaf6..66671e0ce 100644 --- a/test/unit/authorization/PermissionBasedAuthorizer.test.ts +++ b/test/unit/authorization/PermissionBasedAuthorizer.test.ts @@ -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; let authorizer: PermissionBasedAuthorizer; beforeEach(async(): Promise => { input = { identifier: { path: 'http://test.com/foo' }, modes: new Set(), + 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 => { + it('can handle any input.', async(): Promise => { 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 => { 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 => { 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 => { 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 => { - await expect(authorizer.handle(input)).resolves.toEqual(authorization); + await expect(authorizer.handle(input)).resolves.toBeUndefined(); }); }); diff --git a/test/unit/authorization/WebAclAuthorization.test.ts b/test/unit/authorization/WebAclAuthorization.test.ts deleted file mode 100644 index 68eaf6058..000000000 --- a/test/unit/authorization/WebAclAuthorization.test.ts +++ /dev/null @@ -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 => { - 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 => { - expect(authorization.addMetadata(metadata)).toBeUndefined(); - expect(metadata.quads()).toHaveLength(0); - }); - - it('adds corresponding acl metadata for all permissions present.', async(): Promise => { - 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); - }); -}); diff --git a/test/unit/ldp/AuthenticatedLdpHandler.test.ts b/test/unit/ldp/AuthenticatedLdpHandler.test.ts index c4f57c5ec..d1488f3ec 100644 --- a/test/unit/ldp/AuthenticatedLdpHandler.test.ts +++ b/test/unit/ldp/AuthenticatedLdpHandler.test.ts @@ -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 = 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 => { + 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 => { const error = new Error('bad request!'); (args.requestParser.handleSafe as jest.Mock).mockRejectedValueOnce(new Error('bad request!')); diff --git a/test/unit/ldp/operations/GetOperationHandler.test.ts b/test/unit/ldp/operations/GetOperationHandler.test.ts index 8fe20a9bc..bb74da84b 100644 --- a/test/unit/ldp/operations/GetOperationHandler.test.ts +++ b/test/unit/ldp/operations/GetOperationHandler.test.ts @@ -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 => { - 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'); - }); }); diff --git a/test/unit/ldp/operations/HeadOperationHandler.test.ts b/test/unit/ldp/operations/HeadOperationHandler.test.ts index 63d4b9f8d..c8bcf8198 100644 --- a/test/unit/ldp/operations/HeadOperationHandler.test.ts +++ b/test/unit/ldp/operations/HeadOperationHandler.test.ts @@ -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 => ({ 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 => { - 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'); - }); }); diff --git a/test/unit/ldp/operations/metadata/WebAclMetadataCollector.test.ts b/test/unit/ldp/operations/metadata/WebAclMetadataCollector.test.ts new file mode 100644 index 000000000..87bc1a2e9 --- /dev/null +++ b/test/unit/ldp/operations/metadata/WebAclMetadataCollector.test.ts @@ -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 => { + operation = { + method: 'GET', + target: { path: 'http://test.com/foo' }, + preferences: {}, + }; + + metadata = new RepresentationMetadata(); + }); + + it('adds no metadata if there are no permissions.', async(): Promise => { + 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 => { + 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 => { + 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); + }); +});