mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add required ACP headers
This commit is contained in:
parent
f3e7a20800
commit
fa1dee573e
@ -10,8 +10,18 @@
|
|||||||
"@id": "urn:solid-server:default:Middleware",
|
"@id": "urn:solid-server:default:Middleware",
|
||||||
"@type": "SequenceHandler",
|
"@type": "SequenceHandler",
|
||||||
"handlers": [
|
"handlers": [
|
||||||
{ "@id": "urn:solid-server:default:Middleware_Header" },
|
{
|
||||||
{ "@id": "urn:solid-server:default:Middleware_Cors" }
|
"comment": "These handlers can be executed in any order.",
|
||||||
|
"@id": "urn:solid-server:default:ParallelMiddleware",
|
||||||
|
"@type": "ParallelHandler",
|
||||||
|
"handlers": [
|
||||||
|
{ "@id": "urn:solid-server:default:Middleware_Header" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "CORS has to be last since it can close the connection.",
|
||||||
|
"@id": "urn:solid-server:default:Middleware_Cors"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -37,6 +37,28 @@
|
|||||||
"@type": "SubfolderResourcesGenerator",
|
"@type": "SubfolderResourcesGenerator",
|
||||||
"subfolders": [ "acp" ]
|
"subfolders": [ "acp" ]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"comment": "Middleware exposes the required ACP headers.",
|
||||||
|
"@id": "urn:solid-server:default:ParallelMiddleware",
|
||||||
|
"@type": "ParallelHandler",
|
||||||
|
"handlers": [{
|
||||||
|
"@type": "AcpHeaderHandler",
|
||||||
|
"targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
||||||
|
"strategy": { "@id": "urn:solid-server:default:AcrIdentifierStrategy" },
|
||||||
|
"modes": [
|
||||||
|
"http://www.w3.org/ns/auth/acl#Read",
|
||||||
|
"http://www.w3.org/ns/auth/acl#Append",
|
||||||
|
"http://www.w3.org/ns/auth/acl#Write",
|
||||||
|
"http://www.w3.org/ns/auth/acl#Control"
|
||||||
|
],
|
||||||
|
"attributes": [
|
||||||
|
"http://www.w3.org/ns/solid/acp#target",
|
||||||
|
"http://www.w3.org/ns/solid/acp#agent",
|
||||||
|
"http://www.w3.org/ns/solid/acp#client",
|
||||||
|
"http://www.w3.org/ns/solid/acp#issuer"
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"comment": "In case of ACP authorization the ACR resources determine authorization.",
|
"comment": "In case of ACP authorization the ACR resources determine authorization.",
|
||||||
"@id": "urn:solid-server:default:AuthResourceHttpHandler",
|
"@id": "urn:solid-server:default:AuthResourceHttpHandler",
|
||||||
|
@ -292,6 +292,7 @@ export * from './server/WebSocketHandler';
|
|||||||
export * from './server/WebSocketServerFactory';
|
export * from './server/WebSocketServerFactory';
|
||||||
|
|
||||||
// Server/Middleware
|
// Server/Middleware
|
||||||
|
export * from './server/middleware/AcpHeaderHandler';
|
||||||
export * from './server/middleware/CorsHandler';
|
export * from './server/middleware/CorsHandler';
|
||||||
export * from './server/middleware/HeaderHandler';
|
export * from './server/middleware/HeaderHandler';
|
||||||
export * from './server/middleware/StaticAssetHandler';
|
export * from './server/middleware/StaticAssetHandler';
|
||||||
|
39
src/server/middleware/AcpHeaderHandler.ts
Normal file
39
src/server/middleware/AcpHeaderHandler.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import type { AuxiliaryIdentifierStrategy } from '../../http/auxiliary/AuxiliaryIdentifierStrategy';
|
||||||
|
import type { TargetExtractor } from '../../http/input/identifier/TargetExtractor';
|
||||||
|
import { addHeader } from '../../util/HeaderUtil';
|
||||||
|
import { ACP } from '../../util/Vocabularies';
|
||||||
|
import type { HttpHandlerInput } from '../HttpHandler';
|
||||||
|
import { HttpHandler } from '../HttpHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all the required ACP headers as defined at
|
||||||
|
* https://solid.github.io/authorization-panel/acp-specification/#conforming-acp-server
|
||||||
|
*/
|
||||||
|
export class AcpHeaderHandler extends HttpHandler {
|
||||||
|
private readonly targetExtractor: TargetExtractor;
|
||||||
|
private readonly strategy: AuxiliaryIdentifierStrategy;
|
||||||
|
private readonly modes: string[];
|
||||||
|
private readonly attributes: string[];
|
||||||
|
|
||||||
|
public constructor(targetExtractor: TargetExtractor, strategy: AuxiliaryIdentifierStrategy,
|
||||||
|
modes: string[], attributes: string[]) {
|
||||||
|
super();
|
||||||
|
this.targetExtractor = targetExtractor;
|
||||||
|
this.strategy = strategy;
|
||||||
|
this.modes = modes;
|
||||||
|
this.attributes = attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle({ request, response }: HttpHandlerInput): Promise<void> {
|
||||||
|
const identifier = await this.targetExtractor.handleSafe({ request });
|
||||||
|
if (!this.strategy.isAuxiliaryIdentifier(identifier)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const linkValues = [
|
||||||
|
`<${ACP.AccessControlResource}>; rel="type"`,
|
||||||
|
...this.modes.map((mode): string => `<${mode}>; rel="${ACP.grant}"`),
|
||||||
|
...this.attributes.map((attribute): string => `<${attribute}>; rel="${ACP.attribute}"`),
|
||||||
|
];
|
||||||
|
addHeader(response, 'Link', linkValues);
|
||||||
|
}
|
||||||
|
}
|
@ -118,6 +118,10 @@ export const ACL = createVocabulary('http://www.w3.org/ns/auth/acl#',
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ACP = createVocabulary('http://www.w3.org/ns/solid/acp#',
|
export const ACP = createVocabulary('http://www.w3.org/ns/solid/acp#',
|
||||||
|
// Used for ACP middleware headers
|
||||||
|
'AccessControlResource',
|
||||||
|
'grant',
|
||||||
|
'attribute',
|
||||||
// Access Control Resource
|
// Access Control Resource
|
||||||
'resource',
|
'resource',
|
||||||
'accessControl',
|
'accessControl',
|
||||||
|
@ -118,4 +118,21 @@ describe.each(stores)('An LDP handler with ACP using %s', (name, { storeConfig,
|
|||||||
response = await fetch(baseUrl);
|
response = await fetch(baseUrl);
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns the required Link headers.', async(): Promise<void> => {
|
||||||
|
const baseAcr = joinUrl(baseUrl, '.acr');
|
||||||
|
const response = await fetch(baseAcr, { method: 'OPTIONS' });
|
||||||
|
const linkHeaders = response.headers.get('link');
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/solid/acp#AccessControlResource>; rel="type"');
|
||||||
|
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/auth/acl#Read>; rel="http://www.w3.org/ns/solid/acp#grant"');
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/auth/acl#Append>; rel="http://www.w3.org/ns/solid/acp#grant"');
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/auth/acl#Write>; rel="http://www.w3.org/ns/solid/acp#grant"');
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/auth/acl#Control>; rel="http://www.w3.org/ns/solid/acp#grant"');
|
||||||
|
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/solid/acp#target>; rel="http://www.w3.org/ns/solid/acp#attribute"');
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/solid/acp#agent>; rel="http://www.w3.org/ns/solid/acp#attribute"');
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/solid/acp#client>; rel="http://www.w3.org/ns/solid/acp#attribute"');
|
||||||
|
expect(linkHeaders).toContain('<http://www.w3.org/ns/solid/acp#issuer>; rel="http://www.w3.org/ns/solid/acp#attribute"');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,20 +4,24 @@
|
|||||||
"css:config/app/main/default.json",
|
"css:config/app/main/default.json",
|
||||||
"css:config/app/init/default.json",
|
"css:config/app/init/default.json",
|
||||||
"css:config/app/setup/disabled.json",
|
"css:config/app/setup/disabled.json",
|
||||||
|
|
||||||
"css:config/http/handler/simple.json",
|
"css:config/http/handler/simple.json",
|
||||||
"css:config/http/middleware/no-websockets.json",
|
"css:config/http/middleware/no-websockets.json",
|
||||||
"css:config/http/server-factory/no-websockets.json",
|
"css:config/http/server-factory/no-websockets.json",
|
||||||
"css:config/http/static/default.json",
|
"css:config/http/static/default.json",
|
||||||
"css:config/identity/access/public.json",
|
"css:config/identity/access/public.json",
|
||||||
|
|
||||||
"css:config/identity/handler/default.json",
|
"css:config/identity/handler/default.json",
|
||||||
"css:config/identity/ownership/token.json",
|
"css:config/identity/ownership/token.json",
|
||||||
"css:config/identity/pod/static.json",
|
"css:config/identity/pod/static.json",
|
||||||
|
|
||||||
"css:config/ldp/authentication/debug-auth-header.json",
|
"css:config/ldp/authentication/debug-auth-header.json",
|
||||||
"css:config/ldp/authorization/acp.json",
|
"css:config/ldp/authorization/acp.json",
|
||||||
"css:config/ldp/handler/default.json",
|
"css:config/ldp/handler/default.json",
|
||||||
"css:config/ldp/metadata-parser/default.json",
|
"css:config/ldp/metadata-parser/default.json",
|
||||||
"css:config/ldp/metadata-writer/default.json",
|
"css:config/ldp/metadata-writer/default.json",
|
||||||
"css:config/ldp/modes/default.json",
|
"css:config/ldp/modes/default.json",
|
||||||
|
|
||||||
"css:config/storage/key-value/memory.json",
|
"css:config/storage/key-value/memory.json",
|
||||||
"css:config/storage/middleware/default.json",
|
"css:config/storage/middleware/default.json",
|
||||||
"css:config/util/auxiliary/acr.json",
|
"css:config/util/auxiliary/acr.json",
|
||||||
@ -30,7 +34,7 @@
|
|||||||
],
|
],
|
||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
"comment": "An HTTP server with only the LDP handler as HttpHandler and an unsecure authenticator.",
|
"comment": "An HTTP server with only the LDP handler as HttpHandler and an unsecure authenticator using ACP.",
|
||||||
"@id": "urn:solid-server:test:Instances",
|
"@id": "urn:solid-server:test:Instances",
|
||||||
"@type": "RecordObject",
|
"@type": "RecordObject",
|
||||||
"record": [
|
"record": [
|
||||||
|
47
test/unit/server/middleware/AcpHeaderHandler.test.ts
Normal file
47
test/unit/server/middleware/AcpHeaderHandler.test.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { ACP } from '@solid/access-control-policy/dist/constant/acp';
|
||||||
|
import { createResponse } from 'node-mocks-http';
|
||||||
|
import type { AuxiliaryIdentifierStrategy } from '../../../../src/http/auxiliary/AuxiliaryIdentifierStrategy';
|
||||||
|
import type { TargetExtractor } from '../../../../src/http/input/identifier/TargetExtractor';
|
||||||
|
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
||||||
|
import type { HttpResponse } from '../../../../src/server/HttpResponse';
|
||||||
|
import { AcpHeaderHandler } from '../../../../src/server/middleware/AcpHeaderHandler';
|
||||||
|
import { ACL } from '../../../../src/util/Vocabularies';
|
||||||
|
import { SimpleSuffixStrategy } from '../../../util/SimpleSuffixStrategy';
|
||||||
|
|
||||||
|
describe('an AcpHeaderHandler', (): void => {
|
||||||
|
const request: HttpRequest = {} as any;
|
||||||
|
let response: HttpResponse;
|
||||||
|
const modes = [ ACL.Read, ACL.Write ];
|
||||||
|
const attributes = [ ACP.agent, ACP.client ];
|
||||||
|
let targetExtractor: jest.Mocked<TargetExtractor>;
|
||||||
|
let strategy: AuxiliaryIdentifierStrategy;
|
||||||
|
let handler: AcpHeaderHandler;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
response = createResponse() as HttpResponse;
|
||||||
|
targetExtractor = {
|
||||||
|
handleSafe: jest.fn().mockResolvedValue({ path: 'http://example.org/foo/bar' }),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
strategy = new SimpleSuffixStrategy('.acr');
|
||||||
|
|
||||||
|
handler = new AcpHeaderHandler(targetExtractor, strategy, modes, attributes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds no headers if the target is not an ACR.', async(): Promise<void> => {
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(response.getHeaders()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds all the required headers.', async(): Promise<void> => {
|
||||||
|
targetExtractor.handleSafe.mockResolvedValueOnce({ path: 'http://example.org/foo/bar.acr' });
|
||||||
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
|
expect(response.getHeaders()).toEqual({ link: [
|
||||||
|
'<http://www.w3.org/ns/solid/acp#AccessControlResource>; rel="type"',
|
||||||
|
'<http://www.w3.org/ns/auth/acl#Read>; rel="http://www.w3.org/ns/solid/acp#grant"',
|
||||||
|
'<http://www.w3.org/ns/auth/acl#Write>; rel="http://www.w3.org/ns/solid/acp#grant"',
|
||||||
|
'<http://www.w3.org/ns/solid/acp#agent>; rel="http://www.w3.org/ns/solid/acp#attribute"',
|
||||||
|
'<http://www.w3.org/ns/solid/acp#client>; rel="http://www.w3.org/ns/solid/acp#attribute"',
|
||||||
|
]});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user