Skip to content

Commit 1afed65

Browse files
committedJan 21, 2022
feat: Return correct status codes for invalid requests
1 parent bc6203f commit 1afed65

File tree

12 files changed

+179
-17
lines changed

12 files changed

+179
-17
lines changed
 

‎config/ldp/handler/components/operation-handler.json

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
{
2929
"@type": "PatchOperationHandler",
3030
"store": { "@id": "urn:solid-server:default:ResourceStore" }
31+
},
32+
{
33+
"@type": "StaticThrowHandler",
34+
"error": { "@type": "MethodNotAllowedHttpError" }
3135
}
3236
]
3337
}

‎config/ldp/modes/default.json

+27-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,34 @@
66
"@id": "urn:solid-server:default:ModesExtractor",
77
"@type": "WaterfallHandler",
88
"handlers": [
9-
{ "@type": "MethodModesExtractor" },
10-
{ "@type": "SparqlPatchModesExtractor" }
9+
{
10+
"comment": "Extract access modes for PATCH requests based on the request body.",
11+
"@id": "urn:solid-server:default:PatchModesExtractor"
12+
},
13+
{
14+
"comment": "Extract access modes based on the HTTP method.",
15+
"@type": "MethodModesExtractor"
16+
},
17+
{
18+
"@type": "StaticThrowHandler",
19+
"error": { "@type": "MethodNotAllowedHttpError" }
20+
}
1121
]
22+
},
23+
{
24+
"@id": "urn:solid-server:default:PatchModesExtractor",
25+
"@type": "MethodFilterHandler",
26+
"methods": [ "PATCH" ],
27+
"source": {
28+
"@type": "WaterfallHandler",
29+
"handlers": [
30+
{ "@type": "SparqlUpdateModesExtractor" },
31+
{
32+
"@type": "StaticThrowHandler",
33+
"error": { "@type": "UnsupportedMediaTypeHttpError" }
34+
}
35+
]
36+
}
1237
}
1338
]
1439
}

‎config/storage/middleware/stores/patching.json

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
2323
"intermediateType": "internal/quads",
2424
"defaultType": "text/turtle"
25+
},
26+
{
27+
"@type": "StaticThrowHandler",
28+
"error": { "@type": "UnsupportedMediaTypeHttpError" }
2529
}
2630
]
2731
}

‎src/authorization/permissions/ModesExtractor.ts

+3
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ import type { Operation } from '../../http/Operation';
22
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
33
import type { AccessMode } from './Permissions';
44

5+
/**
6+
* Extracts all {@link AccessMode}s that are necessary to execute the given {@link Operation}.
7+
*/
58
export abstract class ModesExtractor extends AsyncHandler<Operation, Set<AccessMode>> {}

‎src/authorization/permissions/SparqlPatchModesExtractor.ts ‎src/authorization/permissions/SparqlUpdateModesExtractor.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpErr
66
import { ModesExtractor } from './ModesExtractor';
77
import { AccessMode } from './Permissions';
88

9-
export class SparqlPatchModesExtractor extends ModesExtractor {
10-
public async canHandle({ method, body }: Operation): Promise<void> {
11-
if (method !== 'PATCH') {
12-
throw new NotImplementedHttpError(`Cannot determine permissions of ${method}, only PATCH.`);
13-
}
9+
/**
10+
* Generates permissions for a SPARQL DELETE/INSERT body.
11+
* Updates with only an INSERT can be done with just append permissions,
12+
* while DELETEs require write permissions as well.
13+
*/
14+
export class SparqlUpdateModesExtractor extends ModesExtractor {
15+
public async canHandle({ body }: Operation): Promise<void> {
1416
if (!this.isSparql(body)) {
1517
throw new NotImplementedHttpError('Cannot determine permissions of non-SPARQL patches.');
1618
}

‎src/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export * from './authorization/access/AgentGroupAccessChecker';
1818
export * from './authorization/permissions/Permissions';
1919
export * from './authorization/permissions/ModesExtractor';
2020
export * from './authorization/permissions/MethodModesExtractor';
21-
export * from './authorization/permissions/SparqlPatchModesExtractor';
21+
export * from './authorization/permissions/SparqlUpdateModesExtractor';
2222

2323
// Authorization
2424
export * from './authorization/AllStaticReader';
@@ -359,9 +359,12 @@ export * from './util/errors/UnsupportedMediaTypeHttpError';
359359
export * from './util/handlers/AsyncHandler';
360360
export * from './util/handlers/BooleanHandler';
361361
export * from './util/handlers/ConditionalHandler';
362+
export * from './util/handlers/HandlerUtil';
363+
export * from './util/handlers/MethodFilterHandler';
362364
export * from './util/handlers/ParallelHandler';
363365
export * from './util/handlers/SequenceHandler';
364366
export * from './util/handlers/StaticHandler';
367+
export * from './util/handlers/StaticThrowHandler';
365368
export * from './util/handlers/UnionHandler';
366369
export * from './util/handlers/UnsupportedAsyncHandler';
367370
export * from './util/handlers/WaterfallHandler';
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NotImplementedHttpError } from '../errors/NotImplementedHttpError';
2+
import { AsyncHandler } from './AsyncHandler';
3+
4+
/**
5+
* Only accepts requests where the input has a `method` field that matches any one of the given methods.
6+
* In case of a match, the input will be sent to the source handler.
7+
*/
8+
export class MethodFilterHandler<TIn extends { method: string }, TOut> extends AsyncHandler<TIn, TOut> {
9+
private readonly methods: string[];
10+
private readonly source: AsyncHandler<TIn, TOut>;
11+
12+
public constructor(methods: string[], source: AsyncHandler<TIn, TOut>) {
13+
super();
14+
this.methods = methods;
15+
this.source = source;
16+
}
17+
18+
public async canHandle(input: TIn): Promise<void> {
19+
if (!this.methods.includes(input.method)) {
20+
throw new NotImplementedHttpError(
21+
`Cannot determine permissions of ${input.method}, only ${this.methods.join(',')}.`,
22+
);
23+
}
24+
await this.source.canHandle(input);
25+
}
26+
27+
public async handle(input: TIn): Promise<TOut> {
28+
return this.source.handle(input);
29+
}
30+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { HttpError } from '../errors/HttpError';
2+
import { AsyncHandler } from './AsyncHandler';
3+
4+
/**
5+
* Utility handler that can handle all input and always throws the given error.
6+
*/
7+
export class StaticThrowHandler extends AsyncHandler<any, never> {
8+
private readonly error: HttpError;
9+
10+
public constructor(error: HttpError) {
11+
super();
12+
this.error = error;
13+
}
14+
15+
public async handle(): Promise<never> {
16+
throw this.error;
17+
}
18+
}

‎test/integration/LdpHandlerWithoutAuth.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -386,4 +386,14 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
386386
// DELETE
387387
expect(await deleteResource(documentUrl)).toBeUndefined();
388388
});
389+
390+
it('returns 405 for unsupported methods.', async(): Promise<void> => {
391+
const response = await fetch(baseUrl, { method: 'TRACE' });
392+
expect(response.status).toBe(405);
393+
});
394+
395+
it('returns 415 for unsupported PATCH types.', async(): Promise<void> => {
396+
const response = await fetch(baseUrl, { method: 'PATCH', headers: { 'content-type': 'text/plain' }, body: 'abc' });
397+
expect(response.status).toBe(415);
398+
});
389399
});

‎test/unit/authorization/permissions/SparqlPatchModesExtractor.test.ts ‎test/unit/authorization/permissions/SparqlUpdateModesExtractor.test.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
import { Factory } from 'sparqlalgebrajs';
22
import { AccessMode } from '../../../../src/authorization/permissions/Permissions';
3-
import { SparqlPatchModesExtractor } from '../../../../src/authorization/permissions/SparqlPatchModesExtractor';
3+
import { SparqlUpdateModesExtractor } from '../../../../src/authorization/permissions/SparqlUpdateModesExtractor';
44
import type { Operation } from '../../../../src/http/Operation';
55
import type { SparqlUpdatePatch } from '../../../../src/http/representation/SparqlUpdatePatch';
66
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
77

8-
describe('A SparqlPatchModesExtractor', (): void => {
9-
const extractor = new SparqlPatchModesExtractor();
8+
describe('A SparqlUpdateModesExtractor', (): void => {
9+
const extractor = new SparqlUpdateModesExtractor();
1010
const factory = new Factory();
1111

12-
it('can only handle (composite) SPARQL DELETE/INSERT PATCH operations.', async(): Promise<void> => {
12+
it('can only handle (composite) SPARQL DELETE/INSERT operations.', async(): Promise<void> => {
1313
const operation = { method: 'PATCH', body: { algebra: factory.createDeleteInsert() }} as unknown as Operation;
1414
await expect(extractor.canHandle(operation)).resolves.toBeUndefined();
1515
(operation.body as SparqlUpdatePatch).algebra = factory.createCompositeUpdate([ factory.createDeleteInsert() ]);
1616
await expect(extractor.canHandle(operation)).resolves.toBeUndefined();
1717

18-
let result = extractor.canHandle({ ...operation, method: 'GET' });
19-
await expect(result).rejects.toThrow(NotImplementedHttpError);
20-
await expect(result).rejects.toThrow('Cannot determine permissions of GET, only PATCH.');
21-
22-
result = extractor.canHandle({ ...operation, body: {} as SparqlUpdatePatch });
18+
let result = extractor.canHandle({ ...operation, body: {} as SparqlUpdatePatch });
2319
await expect(result).rejects.toThrow(NotImplementedHttpError);
2420
await expect(result).rejects.toThrow('Cannot determine permissions of non-SPARQL patches.');
2521

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Operation } from '../../../../src/http/Operation';
2+
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
3+
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
4+
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
5+
import {
6+
MethodFilterHandler,
7+
} from '../../../../src/util/handlers/MethodFilterHandler';
8+
9+
describe('A MethodFilterHandler', (): void => {
10+
const modes = [ 'PATCH', 'POST' ];
11+
const result = 'RESULT';
12+
let operation: Operation;
13+
let source: jest.Mocked<AsyncHandler<Operation, string>>;
14+
let handler: MethodFilterHandler<Operation, string>;
15+
16+
beforeEach(async(): Promise<void> => {
17+
operation = {
18+
method: 'PATCH',
19+
preferences: {},
20+
permissionSet: {},
21+
target: { path: 'http://example.com/foo' },
22+
body: new BasicRepresentation(),
23+
};
24+
25+
source = {
26+
canHandle: jest.fn(),
27+
handle: jest.fn().mockResolvedValue(result),
28+
} as any;
29+
30+
handler = new MethodFilterHandler(modes, source);
31+
});
32+
33+
it('rejects unknown methods.', async(): Promise<void> => {
34+
operation.method = 'GET';
35+
await expect(handler.canHandle(operation)).rejects.toThrow(NotImplementedHttpError);
36+
});
37+
38+
it('checks if the source handle supports the request.', async(): Promise<void> => {
39+
operation.method = 'PATCH';
40+
await expect(handler.canHandle(operation)).resolves.toBeUndefined();
41+
operation.method = 'POST';
42+
await expect(handler.canHandle(operation)).resolves.toBeUndefined();
43+
source.canHandle.mockRejectedValueOnce(new Error('not supported'));
44+
await expect(handler.canHandle(operation)).rejects.toThrow('not supported');
45+
expect(source.canHandle).toHaveBeenLastCalledWith(operation);
46+
});
47+
48+
it('calls the source extractor.', async(): Promise<void> => {
49+
await expect(handler.handle(operation)).resolves.toBe(result);
50+
expect(source.handle).toHaveBeenLastCalledWith(operation);
51+
});
52+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
2+
import { StaticThrowHandler } from '../../../../src/util/handlers/StaticThrowHandler';
3+
4+
describe('A StaticThrowHandler', (): void => {
5+
const error = new BadRequestHttpError();
6+
const handler = new StaticThrowHandler(error);
7+
8+
it('can handle all requests.', async(): Promise<void> => {
9+
await expect(handler.canHandle({})).resolves.toBeUndefined();
10+
});
11+
12+
it('always throws the given error.', async(): Promise<void> => {
13+
await expect(handler.handle()).rejects.toThrow(error);
14+
});
15+
});

0 commit comments

Comments
 (0)
Please sign in to comment.