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/MetadataParser';
|
||||||
export * from './ldp/http/metadata/MetadataWriter';
|
export * from './ldp/http/metadata/MetadataWriter';
|
||||||
export * from './ldp/http/metadata/SlugParser';
|
export * from './ldp/http/metadata/SlugParser';
|
||||||
|
export * from './ldp/http/metadata/WacAllowMetadataWriter';
|
||||||
|
|
||||||
// LDP/HTTP/Response
|
// LDP/HTTP/Response
|
||||||
export * from './ldp/http/response/CreatedResponseDescription';
|
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}`);
|
this.logger.verbose(`Required permissions are read: ${read}, write: ${write}, append: ${append}`);
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (error: unknown) {
|
||||||
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
|
this.logger.verbose(`Authorization failed: ${(error as any).message}`);
|
||||||
throw error;
|
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> {
|
public async handle(input: Operation): Promise<ResponseDescription> {
|
||||||
const body = await this.store.getRepresentation(input.target, input.preferences);
|
const body = await this.store.getRepresentation(input.target, input.preferences);
|
||||||
|
|
||||||
|
input.authorization?.addMetadata(body.metadata);
|
||||||
|
|
||||||
return new OkResponseDescription(body.metadata, body.data);
|
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.
|
// Close the Readable as we will not return it.
|
||||||
body.data.destroy();
|
body.data.destroy();
|
||||||
|
|
||||||
|
input.authorization?.addMetadata(body.metadata);
|
||||||
|
|
||||||
return new OkResponseDescription(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 { Representation } from '../representation/Representation';
|
||||||
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
||||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
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.
|
* Representation preferences of the response. Will be empty if there are none.
|
||||||
*/
|
*/
|
||||||
preferences: RepresentationPreferences;
|
preferences: RepresentationPreferences;
|
||||||
|
/**
|
||||||
|
* This value will be set if the Operation was authorized by an Authorizer.
|
||||||
|
*/
|
||||||
|
authorization?: Authorization;
|
||||||
/**
|
/**
|
||||||
* Optional representation of the body.
|
* 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 { GetOperationHandler } from '../../../../src/ldp/operations/GetOperationHandler';
|
||||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
@ -22,4 +23,12 @@ describe('A GetOperationHandler', (): void => {
|
|||||||
expect(result.metadata).toBe('metadata');
|
expect(result.metadata).toBe('metadata');
|
||||||
expect(result.data).toBe('data');
|
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 { Readable } from 'stream';
|
||||||
|
import type { Authorization } from '../../../../src/authorization/Authorization';
|
||||||
import { HeadOperationHandler } from '../../../../src/ldp/operations/HeadOperationHandler';
|
import { HeadOperationHandler } from '../../../../src/ldp/operations/HeadOperationHandler';
|
||||||
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
import type { Operation } from '../../../../src/ldp/operations/Operation';
|
||||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
@ -32,4 +33,12 @@ describe('A HeadOperationHandler', (): void => {
|
|||||||
expect(result.data).toBeUndefined();
|
expect(result.data).toBeUndefined();
|
||||||
expect(data.destroy).toHaveBeenCalledTimes(1);
|
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