import type { Credentials } from '../authentication/Credentials'; import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier'; import { getLoggerFor } from '../logging/LogUtil'; import type { ResourceSet } from '../storage/ResourceSet'; import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError'; import type { AuthorizerInput } from './Authorizer'; import { Authorizer } from './Authorizer'; import type { PermissionSet } from './permissions/Permissions'; import { AccessMode } from './permissions/Permissions'; /** * Authorizer that bases its decision on the output it gets from its PermissionReader. * For each permission it checks if the reader allows that for at least one credential type, * if yes authorization is granted. * `undefined` values for reader results are interpreted as `false`. */ export class PermissionBasedAuthorizer extends Authorizer { protected readonly logger = getLoggerFor(this); private readonly resourceSet: ResourceSet; /** * The existence of the target resource determines the output status code for certain situations. * The provided {@link ResourceSet} will be used for that. * @param resourceSet - {@link ResourceSet} that can verify the target resource existence. */ public constructor(resourceSet: ResourceSet) { super(); this.resourceSet = resourceSet; } public async handle(input: AuthorizerInput): Promise { const { credentials, requestedModes, availablePermissions } = input; // Ensure all required modes are within the agent's permissions. for (const [ identifier, modes ] of requestedModes.entrySets()) { const modeString = [ ...modes ].join(','); this.logger.debug(`Checking if ${credentials.agent?.webId} has ${modeString} permissions for ${identifier.path}`); const permissions = availablePermissions.get(identifier) ?? {}; for (const mode of modes) { try { this.requireModePermission(credentials, permissions, mode); } catch (error: unknown) { await this.reportAccessError(identifier, modes, permissions, error); } } this.logger.debug(`${JSON.stringify(credentials)} has ${modeString} permissions for ${identifier.path}`); } } /** * If we know the operation will return a 404 regardless (= resource does not exist and is not being created), * and the agent is allowed to know about its existence (= the agent has Read permissions), * then immediately send the 404 here, as it makes any other agent permissions irrelevant. * * Otherwise, deny access based on existing grounds. */ private async reportAccessError(identifier: ResourceIdentifier, modes: ReadonlySet, permissions: PermissionSet, cause: unknown): Promise { const exposeExistence = this.hasModePermission(permissions, AccessMode.read); if (exposeExistence && !modes.has(AccessMode.create) && !await this.resourceSet.hasResource(identifier)) { throw new NotFoundHttpError(); } throw cause; } /** * Ensures that at least one of the credentials provides permissions for the given mode. * Throws a {@link ForbiddenHttpError} or {@link UnauthorizedHttpError} depending on the credentials * if access is not allowed. * @param credentials - Credentials that require access. * @param permissionSet - PermissionSet describing the available permissions of the credentials. * @param mode - Which mode is requested. */ private requireModePermission(credentials: Credentials, permissionSet: PermissionSet, mode: AccessMode): void { if (!this.hasModePermission(permissionSet, mode)) { if (this.isAuthenticated(credentials)) { this.logger.warn(`Agent ${credentials.agent!.webId} has no ${mode} permissions`); throw new ForbiddenHttpError(); } else { // Solid, ยง2.1: "When a client does not provide valid credentials when requesting a resource that requires it, // the data pod MUST send a response with a 401 status code (unless 404 is preferred for security reasons)." // https://solid.github.io/specification/protocol#http-server this.logger.warn(`Unauthenticated agent has no ${mode} permissions`); throw new UnauthorizedHttpError(); } } } /** * Checks if one of the Permissions in the PermissionSet grants permission to use the given mode. */ private hasModePermission(permissionSet: PermissionSet, mode: AccessMode): boolean { for (const permissions of Object.values(permissionSet)) { if (permissions[mode]) { return true; } } return false; } /** * Checks whether the agent is authenticated (logged in) or not (public/anonymous). * @param credentials - Credentials to check. */ private isAuthenticated(credentials: Credentials): boolean { return typeof credentials.agent?.webId === 'string'; } }