mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Make allow headers more accurate
This commit is contained in:
parent
d7078ad692
commit
5e60000681
@ -8,8 +8,11 @@ import { LDP, PIM, RDF, SOLID_ERROR } from '../../../util/Vocabularies';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
// Only PUT and PATCH can be used to create a new resource
|
||||
const NEW_RESOURCE_ALLOWED_METHODS = new Set([ 'PUT', 'PATCH' ]);
|
||||
enum ResourceType {
|
||||
document,
|
||||
container,
|
||||
unknown,
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Allow, Accept-Patch, Accept-Post, and Accept-Put headers.
|
||||
@ -30,8 +33,20 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
||||
const { response, metadata } = input;
|
||||
|
||||
let resourceType: ResourceType;
|
||||
if (metadata.has(RDF.terms.type, LDP.terms.Resource)) {
|
||||
resourceType = isContainerPath(metadata.identifier.value) ? ResourceType.container : ResourceType.document;
|
||||
} else {
|
||||
const target = metadata.get(SOLID_ERROR.terms.target)?.value;
|
||||
if (target) {
|
||||
resourceType = isContainerPath(target) ? ResourceType.container : ResourceType.document;
|
||||
} else {
|
||||
resourceType = ResourceType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out methods which are not allowed
|
||||
const allowedMethods = this.filterAllowedMethods(metadata);
|
||||
const allowedMethods = this.filterAllowedMethods(metadata, resourceType);
|
||||
|
||||
// Generate the Allow headers (if required)
|
||||
const generateAllow = this.generateAllow(allowedMethods, response, metadata);
|
||||
@ -43,29 +58,34 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
/**
|
||||
* Starts from the stored set of methods and removes all those that are not allowed based on the metadata.
|
||||
*/
|
||||
private filterAllowedMethods(metadata: RepresentationMetadata): Set<string> {
|
||||
private filterAllowedMethods(metadata: RepresentationMetadata, resourceType: ResourceType): Set<string> {
|
||||
const disallowedMethods = new Set(metadata.getAll(SOLID_ERROR.terms.disallowedMethod)
|
||||
.map((term): string => term.value));
|
||||
const allowedMethods = new Set(this.supportedMethods.filter((method): boolean => !disallowedMethods.has(method)));
|
||||
|
||||
// POST is only allowed on containers.
|
||||
// Metadata only has the resource URI in case it has resource metadata.
|
||||
if (!this.isPostAllowed(metadata)) {
|
||||
if (!this.isPostAllowed(resourceType)) {
|
||||
allowedMethods.delete('POST');
|
||||
}
|
||||
|
||||
if (!this.isPutAllowed(metadata)) {
|
||||
if (!this.isPutAllowed(metadata, resourceType)) {
|
||||
allowedMethods.delete('PUT');
|
||||
}
|
||||
|
||||
if (!this.isDeleteAllowed(metadata)) {
|
||||
if (!this.isPatchAllowed(resourceType)) {
|
||||
allowedMethods.delete('PATCH');
|
||||
}
|
||||
|
||||
if (!this.isDeleteAllowed(metadata, resourceType)) {
|
||||
allowedMethods.delete('DELETE');
|
||||
}
|
||||
|
||||
// If we are sure the resource does not exist: only keep methods that can create a new resource.
|
||||
if (metadata.has(SOLID_ERROR.terms.errorResponse, NotFoundHttpError.uri)) {
|
||||
for (const method of allowedMethods) {
|
||||
if (!NEW_RESOURCE_ALLOWED_METHODS.has(method)) {
|
||||
// Containers can only be created by PUT, documents also by PATCH
|
||||
if (method !== 'PUT' && (method !== 'PATCH' || resourceType === ResourceType.container)) {
|
||||
allowedMethods.delete(method);
|
||||
}
|
||||
}
|
||||
@ -76,18 +96,23 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
|
||||
/**
|
||||
* POST is only allowed on containers.
|
||||
* The metadata URI is only valid in case there is resource metadata,
|
||||
* otherwise it is just a blank node.
|
||||
*/
|
||||
private isPostAllowed(metadata: RepresentationMetadata): boolean {
|
||||
return !metadata.has(RDF.terms.type, LDP.terms.Resource) || isContainerPath(metadata.identifier.value);
|
||||
private isPostAllowed(resourceType: ResourceType): boolean {
|
||||
return resourceType !== ResourceType.document;
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT is not allowed on existing containers.
|
||||
*/
|
||||
private isPutAllowed(metadata: RepresentationMetadata): boolean {
|
||||
return !metadata.has(RDF.terms.type, LDP.terms.Resource) || !isContainerPath(metadata.identifier.value);
|
||||
private isPutAllowed(metadata: RepresentationMetadata, resourceType: ResourceType): boolean {
|
||||
return resourceType !== ResourceType.container || !metadata.has(RDF.terms.type, LDP.terms.Resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH is not allowed on containers.
|
||||
*/
|
||||
private isPatchAllowed(resourceType: ResourceType): boolean {
|
||||
return resourceType !== ResourceType.container;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,14 +122,14 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
*
|
||||
* Note that the identifier value check only works if the metadata is not about an error.
|
||||
*/
|
||||
private isDeleteAllowed(metadata: RepresentationMetadata): boolean {
|
||||
if (!isContainerPath(metadata.identifier.value)) {
|
||||
private isDeleteAllowed(metadata: RepresentationMetadata, resourceType: ResourceType): boolean {
|
||||
if (resourceType !== ResourceType.container) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isStorage = metadata.has(RDF.terms.type, PIM.terms.Storage);
|
||||
const isEmpty = metadata.has(LDP.terms.contains);
|
||||
return !isStorage && !isEmpty;
|
||||
const isEmpty = !metadata.has(LDP.terms.contains);
|
||||
return !isStorage && isEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,6 +30,14 @@ describe('An AllowAcceptHeaderWriter', (): void => {
|
||||
{ [RDF.type]: [ LDP.terms.Resource, LDP.terms.Container, PIM.terms.Storage ]},
|
||||
);
|
||||
const error404 = new RepresentationMetadata({ [SOLID_ERROR.errorResponse]: NotFoundHttpError.uri });
|
||||
const error404Container = new RepresentationMetadata({
|
||||
[SOLID_ERROR.errorResponse]: NotFoundHttpError.uri,
|
||||
[SOLID_ERROR.target]: 'http://example.com/foo/',
|
||||
});
|
||||
const error404Document = new RepresentationMetadata({
|
||||
[SOLID_ERROR.errorResponse]: NotFoundHttpError.uri,
|
||||
[SOLID_ERROR.target]: 'http://example.com/foo/bar',
|
||||
});
|
||||
const error405 = new RepresentationMetadata(
|
||||
{ [SOLID_ERROR.errorResponse]: MethodNotAllowedHttpError.uri, [SOLID_ERROR.disallowedMethod]: 'PUT' },
|
||||
);
|
||||
@ -57,33 +65,36 @@ describe('An AllowAcceptHeaderWriter', (): void => {
|
||||
expect(headers['accept-post']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns all methods except PUT for an empty container.', async(): Promise<void> => {
|
||||
it('returns all methods except PUT/PATCH for an empty container.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: emptyContainer })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
expect(typeof headers.allow).toBe('string');
|
||||
expect(new Set(headers.allow!.split(', ')))
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH', 'DELETE' ]));
|
||||
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'DELETE' ]));
|
||||
expect(headers['accept-patch']).toBeUndefined();
|
||||
expect(headers['accept-put']).toBeUndefined();
|
||||
expect(headers['accept-post']).toBe('*/*');
|
||||
});
|
||||
|
||||
it('returns all methods except PUT/DELETE for a non-empty container.', async(): Promise<void> => {
|
||||
it('returns all methods except PUT/PATCH/DELETE for a non-empty container.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: fullContainer })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
expect(typeof headers.allow).toBe('string');
|
||||
expect(new Set(headers.allow!.split(', ')))
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH' ]));
|
||||
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST' ]));
|
||||
expect(headers['accept-patch']).toBeUndefined();
|
||||
expect(headers['accept-put']).toBeUndefined();
|
||||
expect(headers['accept-post']).toBe('*/*');
|
||||
});
|
||||
|
||||
it('returns all methods except PUT/DELETE for a storage container.', async(): Promise<void> => {
|
||||
it('returns all methods except PUT/PATCH/DELETE for a storage container.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: storageContainer })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
expect(typeof headers.allow).toBe('string');
|
||||
expect(new Set(headers.allow!.split(', ')))
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH' ]));
|
||||
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST' ]));
|
||||
expect(headers['accept-patch']).toBeUndefined();
|
||||
expect(headers['accept-put']).toBeUndefined();
|
||||
expect(headers['accept-post']).toBe('*/*');
|
||||
});
|
||||
|
||||
@ -98,6 +109,28 @@ describe('An AllowAcceptHeaderWriter', (): void => {
|
||||
expect(headers['accept-post']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns PUT if the target does not exist and is a document.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: error404Document })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
expect(typeof headers.allow).toBe('string');
|
||||
expect(new Set(headers.allow!.split(', ')))
|
||||
.toEqual(new Set([ 'PUT', 'PATCH' ]));
|
||||
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
|
||||
expect(headers['accept-put']).toBe('*/*');
|
||||
expect(headers['accept-post']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns PUT if the target does not exist and is a container.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: error404Container })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
expect(typeof headers.allow).toBe('string');
|
||||
expect(new Set(headers.allow!.split(', ')))
|
||||
.toEqual(new Set([ 'PUT' ]));
|
||||
expect(headers['accept-patch']).toBeUndefined();
|
||||
expect(headers['accept-put']).toBe('*/*');
|
||||
expect(headers['accept-post']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removes methods that are not allowed by a 405 error.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: error405 })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
|
@ -19,11 +19,12 @@ export async function getResource(
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('link')).toContain(`<${LDP.Resource}>; rel="type"`);
|
||||
expect(response.headers.get('link')).toContain(`<${url}.acl>; rel="acl"`);
|
||||
expect(response.headers.get('accept-patch')).toBe('text/n3, application/sparql-update');
|
||||
|
||||
if (isContainer) {
|
||||
expect(response.headers.get('link')).toContain(`<${LDP.Container}>; rel="type"`);
|
||||
expect(response.headers.get('link')).toContain(`<${LDP.BasicContainer}>; rel="type"`);
|
||||
} else {
|
||||
expect(response.headers.get('accept-patch')).toBe('text/n3, application/sparql-update');
|
||||
}
|
||||
if (expected?.contentType) {
|
||||
expect(response.headers.get('content-type')).toBe(expected.contentType);
|
||||
|
Loading…
x
Reference in New Issue
Block a user