diff --git a/src/server/AuthorizingHttpHandler.ts b/src/server/AuthorizingHttpHandler.ts index 4b061fad6..956e0492e 100644 --- a/src/server/AuthorizingHttpHandler.ts +++ b/src/server/AuthorizingHttpHandler.ts @@ -1,13 +1,19 @@ +import { DataFactory } from 'n3'; import type { Credentials } from '../authentication/Credentials'; import type { CredentialsExtractor } from '../authentication/CredentialsExtractor'; import type { Authorizer } from '../authorization/Authorizer'; import type { PermissionReader } from '../authorization/PermissionReader'; import type { ModesExtractor } from '../authorization/permissions/ModesExtractor'; +import type { AccessMap } from '../authorization/permissions/Permissions'; import type { ResponseDescription } from '../http/output/response/ResponseDescription'; import { getLoggerFor } from '../logging/LogUtil'; +import { HttpError } from '../util/errors/HttpError'; +import { SOLID_META } from '../util/Vocabularies'; import type { OperationHttpHandlerInput } from './OperationHttpHandler'; import { OperationHttpHandler } from './OperationHttpHandler'; +const { blankNode, namedNode, literal } = DataFactory; + export interface AuthorizingHttpHandlerArgs { /** * Extracts the credentials from the incoming request. @@ -77,6 +83,9 @@ export class AuthorizingHttpHandler extends OperationHttpHandler { await this.authorizer.handleSafe({ credentials, requestedModes, availablePermissions }); } catch (error: unknown) { this.logger.verbose(`Authorization failed: ${(error as any).message}`); + if (HttpError.isInstance(error)) { + this.addAccessModesToError(error, requestedModes); + } throw error; } @@ -84,4 +93,15 @@ export class AuthorizingHttpHandler extends OperationHttpHandler { return this.operationHandler.handleSafe(input); } + + private addAccessModesToError(error: HttpError, requestedModes: AccessMap): void { + for (const [ identifier, modes ] of requestedModes.entrySets()) { + const bnode = blankNode(); + error.metadata.add(SOLID_META.terms.requestedAccess, bnode); + error.metadata.addQuad(bnode, SOLID_META.terms.accessTarget, namedNode(identifier.path)); + for (const mode of modes.values()) { + error.metadata.addQuad(bnode, SOLID_META.terms.accessMode, literal(mode)); + } + } + } } diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index 1afdfa665..e69104572 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -287,6 +287,10 @@ export const SOLID_META = createVocabulary('urn:npm:solid:community-server:meta: 'value', // This is used to indicate whether metadata should be preserved or not during a PUT operation 'preserve', + // These predicates are used to describe the requested access in case of an unauthorized request + 'requestedAccess', + 'accessTarget', + 'accessMode', ); export const VANN = createVocabulary('http://purl.org/vocab/vann/', diff --git a/test/unit/server/AuthorizingHttpHandler.test.ts b/test/unit/server/AuthorizingHttpHandler.test.ts index 2b3c91220..724b74792 100644 --- a/test/unit/server/AuthorizingHttpHandler.test.ts +++ b/test/unit/server/AuthorizingHttpHandler.test.ts @@ -1,3 +1,4 @@ +import type { BlankNode } from 'n3'; import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor'; import type { Authorizer } from '../../../src/authorization/Authorizer'; import type { PermissionReader } from '../../../src/authorization/PermissionReader'; @@ -11,14 +12,18 @@ import type { HttpRequest } from '../../../src/server/HttpRequest'; import type { HttpResponse } from '../../../src/server/HttpResponse'; import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler'; import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError'; +import { HttpError } from '../../../src/util/errors/HttpError'; import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/IdentifierMap'; +import { SOLID_META } from '../../../src/util/Vocabularies'; describe('An AuthorizingHttpHandler', (): void => { const credentials = { }; const target = { path: 'http://example.com/foo' }; - const requestedModes: AccessMap = new IdentifierSetMultiMap([[ target, AccessMode.read ]]); + const requestedModes: AccessMap = new IdentifierSetMultiMap( + [[ target, new Set([ AccessMode.read, AccessMode.write ]) ]], + ); const availablePermissions: PermissionMap = new IdentifierMap( - [[ target, { read: true }]], + [[ target, { read: true, write: true }]], ); const request: HttpRequest = {} as any; const response: HttpResponse = {} as any; @@ -73,10 +78,26 @@ describe('An AuthorizingHttpHandler', (): void => { expect(source.handleSafe).toHaveBeenLastCalledWith({ request, response, operation }); }); - it('errors if authorization fails.', async(): Promise => { + it('errors with added access modes if authorization fails.', async(): Promise => { const error = new ForbiddenHttpError(); authorizer.handleSafe.mockRejectedValueOnce(error); - await expect(handler.handle({ request, response, operation })).rejects.toThrow(error); + let handlerError: HttpError | undefined; + try { + await handler.handle({ request, response, operation }); + } catch (receivedError: unknown) { + if (receivedError instanceof HttpError) { + handlerError = receivedError; + } + } + expect(handlerError).toBe(error); + const [ bnode ] = handlerError?.metadata?.getAll(SOLID_META.terms.requestedAccess) ?? []; + expect(bnode?.termType).toBe('BlankNode'); + const [ targetQuad ] = handlerError?.metadata?.quads(bnode as BlankNode, SOLID_META.terms.accessTarget) ?? []; + expect(targetQuad.object.value).toBe(target.path); + const modeQuads = handlerError?.metadata?.quads(bnode as BlankNode, SOLID_META.terms.accessMode) ?? []; + const modes = modeQuads.map((quad): string => quad.object.value); + expect(modes).toContain(AccessMode.read); + expect(modes).toContain(AccessMode.write); expect(source.handleSafe).toHaveBeenCalledTimes(0); }); });