Skip to content

Commit

Permalink
feat: Add WAC-Allow header when required
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Feb 18, 2021
1 parent f2f265c commit 1393424
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 3 additions & 1 deletion src/ldp/AuthenticatedLdpHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
40 changes: 40 additions & 0 deletions src/ldp/http/metadata/WacAllowMetadataWriter.ts
Original file line number Diff line number Diff line change
@@ -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(' ')}"`;
}
}
3 changes: 3 additions & 0 deletions src/ldp/operations/GetOperationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions src/ldp/operations/HeadOperationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
5 changes: 5 additions & 0 deletions src/ldp/operations/Operation.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.
*/
Expand Down
43 changes: 43 additions & 0 deletions test/unit/ldp/http/metadata/WacAllowMetadataWriter.test.ts
Original file line number Diff line number Diff line change
@@ -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"',
});
});
});
9 changes: 9 additions & 0 deletions test/unit/ldp/operations/GetOperationHandler.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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');
});
});
9 changes: 9 additions & 0 deletions test/unit/ldp/operations/HeadOperationHandler.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
});
});

0 comments on commit 1393424

Please sign in to comment.