mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add WAC-Allow header when required
This commit is contained in:
parent
f2f265c586
commit
139342470e
@ -46,6 +46,7 @@ export * from './ldp/http/metadata/MetadataExtractor';
|
||||
export * from './ldp/http/metadata/MetadataParser';
|
||||
export * from './ldp/http/metadata/MetadataWriter';
|
||||
export * from './ldp/http/metadata/SlugParser';
|
||||
export * from './ldp/http/metadata/WacAllowMetadataWriter';
|
||||
|
||||
// LDP/HTTP/Response
|
||||
export * from './ldp/http/response/CreatedResponseDescription';
|
||||
|
@ -127,7 +127,9 @@ export class AuthenticatedLdpHandler extends HttpHandler {
|
||||
this.logger.verbose(`Required permissions are read: ${read}, write: ${write}, append: ${append}`);
|
||||
|
||||
try {
|
||||
await this.authorizer.handleSafe({ credentials, identifier: operation.target, permissions });
|
||||
const authorization = await this.authorizer
|
||||
.handleSafe({ credentials, identifier: operation.target, permissions });
|
||||
operation.authorization = authorization;
|
||||
} catch (error: unknown) {
|
||||
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
|
||||
throw error;
|
||||
|
40
src/ldp/http/metadata/WacAllowMetadataWriter.ts
Normal file
40
src/ldp/http/metadata/WacAllowMetadataWriter.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { Term } from 'rdf-js';
|
||||
import type { HttpResponse } from '../../../server/HttpResponse';
|
||||
import { addHeader } from '../../../util/HeaderUtil';
|
||||
import { ACL, AUTH } from '../../../util/Vocabularies';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
/**
|
||||
* Add the necessary WAC-Allow header values.
|
||||
* Solid, §10.1: "Servers exposing client’s access privileges on a resource URL MUST advertise
|
||||
* by including the WAC-Allow HTTP header in the response of HTTP HEAD and GET requests."
|
||||
* https://solid.github.io/specification/protocol#web-access-control
|
||||
*/
|
||||
export class WacAllowMetadataWriter extends MetadataWriter {
|
||||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
||||
const userModes = input.metadata.getAll(AUTH.terms.userMode).map(this.aclToPermission);
|
||||
const publicModes = input.metadata.getAll(AUTH.terms.publicMode).map(this.aclToPermission);
|
||||
|
||||
const headerStrings: string[] = [];
|
||||
if (userModes.length > 0) {
|
||||
headerStrings.push(this.createAccessParam('user', userModes));
|
||||
}
|
||||
if (publicModes.length > 0) {
|
||||
headerStrings.push(this.createAccessParam('public', publicModes));
|
||||
}
|
||||
|
||||
// Only add the header if there are permissions to show
|
||||
if (headerStrings.length > 0) {
|
||||
addHeader(input.response, 'WAC-Allow', headerStrings.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
private aclToPermission(aclTerm: Term): string {
|
||||
return aclTerm.value.slice(ACL.namespace.length).toLowerCase();
|
||||
}
|
||||
|
||||
private createAccessParam(name: string, modes: string[]): string {
|
||||
return `${name}="${modes.join(' ')}"`;
|
||||
}
|
||||
}
|
@ -25,6 +25,9 @@ export class GetOperationHandler extends OperationHandler {
|
||||
|
||||
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||
const body = await this.store.getRepresentation(input.target, input.preferences);
|
||||
|
||||
input.authorization?.addMetadata(body.metadata);
|
||||
|
||||
return new OkResponseDescription(body.metadata, body.data);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ 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);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { Authorization } from '../../authorization/Authorization';
|
||||
import type { Representation } from '../representation/Representation';
|
||||
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
@ -18,6 +19,10 @@ export interface Operation {
|
||||
* Representation preferences of the response. Will be empty if there are none.
|
||||
*/
|
||||
preferences: RepresentationPreferences;
|
||||
/**
|
||||
* This value will be set if the Operation was authorized by an Authorizer.
|
||||
*/
|
||||
authorization?: Authorization;
|
||||
/**
|
||||
* Optional representation of the body.
|
||||
*/
|
||||
|
43
test/unit/ldp/http/metadata/WacAllowMetadataWriter.test.ts
Normal file
43
test/unit/ldp/http/metadata/WacAllowMetadataWriter.test.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { createResponse } from 'node-mocks-http';
|
||||
import { WacAllowMetadataWriter } from '../../../../../src/ldp/http/metadata/WacAllowMetadataWriter';
|
||||
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
||||
import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
|
||||
|
||||
describe('WacAllowMetadataWriter', (): void => {
|
||||
const writer = new WacAllowMetadataWriter();
|
||||
let response: HttpResponse;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
response = createResponse();
|
||||
});
|
||||
|
||||
it('adds no header if there is no relevant metadata.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata();
|
||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({ });
|
||||
});
|
||||
|
||||
it('adds a WAC-Allow header if there is relevant metadata.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata({
|
||||
[AUTH.userMode]: [ ACL.terms.Read, ACL.terms.Write ],
|
||||
[AUTH.publicMode]: [ ACL.terms.Read ],
|
||||
});
|
||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||
|
||||
expect(response.getHeaders()).toEqual({
|
||||
'wac-allow': 'user="read write",public="read"',
|
||||
});
|
||||
});
|
||||
|
||||
it('only adds a header value for entries with at least 1 permission.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata({
|
||||
[AUTH.userMode]: [ ACL.terms.Read, ACL.terms.Write ],
|
||||
});
|
||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||
|
||||
expect(response.getHeaders()).toEqual({
|
||||
'wac-allow': 'user="read write"',
|
||||
});
|
||||
});
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
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';
|
||||
@ -22,4 +23,12 @@ describe('A GetOperationHandler', (): void => {
|
||||
expect(result.metadata).toBe('metadata');
|
||||
expect(result.data).toBe('data');
|
||||
});
|
||||
|
||||
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' }, authorization } as Operation);
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(authorization.addMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(authorization.addMetadata).toHaveBeenLastCalledWith('metadata');
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
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';
|
||||
@ -32,4 +33,12 @@ describe('A HeadOperationHandler', (): void => {
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(data.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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' }, authorization } as Operation);
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(authorization.addMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(authorization.addMetadata).toHaveBeenLastCalledWith('metadata');
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user