diff --git a/src/authorization/AclManager.ts b/src/authorization/AclManager.ts index 274a5c5cb..7947a2aac 100644 --- a/src/authorization/AclManager.ts +++ b/src/authorization/AclManager.ts @@ -2,6 +2,10 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifie /** * Handles where acl resources are stored. + * + * Solid, §4.3.3: "A given Solid resource MUST NOT be directly associated with more than one ACL auxiliary resource. + * A given ACL auxiliary resource MUST NOT be directly associated with more than one Solid resource." + * https://solid.github.io/specification/protocol#auxiliary-resources-reserved */ export interface AclManager { /** diff --git a/src/authorization/WebAclAuthorizer.ts b/src/authorization/WebAclAuthorizer.ts index 66565e2bd..f01fe04cf 100644 --- a/src/authorization/WebAclAuthorizer.ts +++ b/src/authorization/WebAclAuthorizer.ts @@ -8,7 +8,6 @@ import { getLoggerFor } from '../logging/LogUtil'; import type { ResourceStore } from '../storage/ResourceStore'; import { INTERNAL_QUADS } from '../util/ContentTypes'; import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError'; -import { InternalServerError } from '../util/errors/InternalServerError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError'; import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy'; @@ -44,6 +43,9 @@ export class WebAclAuthorizer extends Authorizer { public async handle(input: AuthorizerArgs): Promise { const store = await this.getAclRecursive(input.identifier); if (await this.aclManager.isAclDocument(input.identifier)) { + // Solid, §4.3.3: "To discover, read, create, or modify an ACL auxiliary resource, an acl:agent MUST + // have acl:Control privileges per the ACL inheritance algorithm on the resource directly associated with it." + // https://solid.github.io/specification/protocol#auxiliary-resources-reserved this.checkPermission(input.credentials, store, 'control'); } else { (Object.keys(input.permissions) as (keyof PermissionSet)[]).forEach((key): void => { @@ -77,6 +79,9 @@ export class WebAclAuthorizer extends Authorizer { this.logger.warn(`Agent ${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(); } @@ -154,7 +159,9 @@ export class WebAclAuthorizer extends Authorizer { this.logger.debug(`Traversing to the parent of ${id.path}`); if (this.identifierStrategy.isRootContainer(id)) { this.logger.error(`No ACL document found for root container ${id.path}`); - throw new InternalServerError('No ACL document found for root container'); + // Solid, §10.1: "In the event that a server can’t apply an ACL to a resource, it MUST deny access." + // https://solid.github.io/specification/protocol#web-access-control + throw new ForbiddenHttpError('No ACL document found for root container'); } const parent = this.identifierStrategy.getParentContainer(id); return this.getAclRecursive(parent, true); diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts index a9c2f7961..b310b1b30 100644 --- a/src/init/AclInitializer.ts +++ b/src/init/AclInitializer.ts @@ -51,6 +51,10 @@ export class AclInitializer extends Initializer { // Set up ACL so everything can still be done by default // Note that this will need to be adapted to go through all the correct channels later on + // + // Solid, §4.1: "The root container (pim:Storage) MUST have an ACL auxiliary resource directly associated to it. + // The associated ACL document MUST include an authorization policy with acl:Control access privilege." + // https://solid.github.io/specification/protocol#storage protected async setRootAclDocument(rootAcl: ResourceIdentifier): Promise { const acl = `@prefix acl: . @prefix foaf: . diff --git a/src/init/RootContainerInitializer.ts b/src/init/RootContainerInitializer.ts index ef83094cf..32e798a18 100644 --- a/src/init/RootContainerInitializer.ts +++ b/src/init/RootContainerInitializer.ts @@ -14,6 +14,10 @@ import namedNode = DataFactory.namedNode; /** * Initializes ResourceStores by creating a root container if it didn't exist yet. + * + * Solid, §4.1: "When a server supports a data pod, it MUST provide one or more storages (pim:Storage) – + * a space of URIs in which data can be accessed. A storage is the root container for all of its contained resources." + * https://solid.github.io/specification/protocol#storage */ export class RootContainerInitializer extends Initializer { protected readonly logger = getLoggerFor(this); @@ -59,6 +63,9 @@ export class RootContainerInitializer extends Initializer { // Make sure the root container is a pim:Storage // This prevents deletion of the root container as storage root containers can not be deleted + // Solid, §4.1: "Servers exposing the storage resource MUST advertise by including the HTTP Link header + // with rel="type" targeting http://www.w3.org/ns/pim/space#Storage when responding to storage’s request URI." + // https://solid.github.io/specification/protocol#storage metadata.add(RDF.type, PIM.terms.Storage); this.logger.debug(`Creating root container at ${this.baseId.path}`); diff --git a/src/server/middleware/CorsHandler.ts b/src/server/middleware/CorsHandler.ts index 1c66f6c57..23365a695 100644 --- a/src/server/middleware/CorsHandler.ts +++ b/src/server/middleware/CorsHandler.ts @@ -22,6 +22,11 @@ interface SimpleCorsOptions { /** * Handler that sets CORS options on the response. + * + * Solid, §7.1: "A data pod MUST implement the CORS protocol [FETCH] such that, to the extent possible, + * the browser allows Solid apps to send any request and combination of request headers to the data pod, + * and the Solid app can read any response and response headers received from the data pod." + * Full details: https://solid.github.io/specification/protocol#cors-server */ export class CorsHandler extends HttpHandler { private readonly corsHandler: RequestHandler; diff --git a/src/storage/accessors/DataAccessor.ts b/src/storage/accessors/DataAccessor.ts index ef793603c..616fb2482 100644 --- a/src/storage/accessors/DataAccessor.ts +++ b/src/storage/accessors/DataAccessor.ts @@ -57,6 +57,11 @@ export interface DataAccessor { /** * Deletes the resource and its corresponding metadata. + * + * Solid, §5.4: "When a contained resource is deleted, the server MUST also remove the corresponding containment + * triple, which has the effect of removing the deleted resource from the containing container." + * https://solid.github.io/specification/protocol#deleting-resources + * * @param identifier - Resource to delete. */ deleteResource: (identifier: ResourceIdentifier) => Promise; diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts index beb563493..d9c8529a3 100644 --- a/src/storage/patch/SparqlUpdatePatchHandler.ts +++ b/src/storage/patch/SparqlUpdatePatchHandler.ts @@ -92,7 +92,8 @@ export class SparqlUpdatePatchHandler extends PatchHandler { }); this.logger.debug(`${store.size} quads in ${identifier.path}.`); } catch (error: unknown) { - // In case the resource does not exist yet we want to create it + // Solid, §5.1: "Clients who want to assign a URI to a resource, MUST use PUT and PATCH requests." + // https://solid.github.io/specification/protocol#resource-type-heuristics if (!(error instanceof NotFoundHttpError)) { throw error; } diff --git a/src/util/PathUtil.ts b/src/util/PathUtil.ts index 41f182e19..4329505db 100644 --- a/src/util/PathUtil.ts +++ b/src/util/PathUtil.ts @@ -88,6 +88,8 @@ export function encodeUriPathComponents(path: string): string { * @param path - Path to check. */ export function isContainerPath(path: string): boolean { + // Solid, §3.1: "Paths ending with a slash denote a container resource." + // https://solid.github.io/specification/protocol#uri-slash-semantics return path.endsWith('/'); } diff --git a/test/unit/authorization/WebAclAuthorizer.test.ts b/test/unit/authorization/WebAclAuthorizer.test.ts index 79ee7460e..cbc6728ce 100644 --- a/test/unit/authorization/WebAclAuthorizer.test.ts +++ b/test/unit/authorization/WebAclAuthorizer.test.ts @@ -8,7 +8,6 @@ import type { Representation } from '../../../src/ldp/representation/Representat import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceStore } from '../../../src/storage/ResourceStore'; import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError'; -import { InternalServerError } from '../../../src/util/errors/InternalServerError'; import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError'; import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy'; @@ -153,7 +152,7 @@ describe('A WebAclAuthorizer', (): void => { }; const promise = authorizer.handle({ identifier, permissions, credentials }); await expect(promise).rejects.toThrow('No ACL document found for root container'); - await expect(promise).rejects.toThrow(InternalServerError); + await expect(promise).rejects.toThrow(ForbiddenHttpError); }); it('allows an agent to append if they have write access.', async(): Promise => {