feat: Create PathBasedAuthorizer

This commit is contained in:
Joachim Van Herwegen 2021-07-27 10:53:14 +02:00
parent d95db60fe1
commit f4833d2534
3 changed files with 104 additions and 0 deletions

View File

@ -0,0 +1,52 @@
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/PathUtil';
import type { Authorization } from './Authorization';
import type { AuthorizerArgs } from './Authorizer';
import { Authorizer } from './Authorizer';
/**
* Redirects requests to specific authorizers based on their identifier.
* The keys in the input map will be converted to regular expressions.
* The regular expressions should all start with a slash
* and will be evaluated relative to the base URL.
*
* Will error if no match is found.
*/
export class PathBasedAuthorizer extends Authorizer {
private readonly baseUrl: string;
private readonly paths: Map<RegExp, Authorizer>;
public constructor(baseUrl: string, paths: Record<string, Authorizer>) {
super();
this.baseUrl = ensureTrailingSlash(baseUrl);
const entries = Object.entries(paths).map(([ key, val ]): [RegExp, Authorizer] => [ new RegExp(key, 'u'), val ]);
this.paths = new Map(entries);
}
public async canHandle(input: AuthorizerArgs): Promise<void> {
const authorizer = this.findAuthorizer(input.identifier.path);
await authorizer.canHandle(input);
}
public async handle(input: AuthorizerArgs): Promise<Authorization> {
const authorizer = this.findAuthorizer(input.identifier.path);
return authorizer.handle(input);
}
/**
* Find the authorizer corresponding to the given path.
* Errors if there is no match.
*/
private findAuthorizer(path: string): Authorizer {
if (path.startsWith(this.baseUrl)) {
// We want to keep the leading slash
const relative = path.slice(trimTrailingSlashes(this.baseUrl).length);
for (const [ regex, authorizer ] of this.paths) {
if (regex.test(relative)) {
return authorizer;
}
}
}
throw new NotImplementedHttpError('No regex matches the given path.');
}
}

View File

@ -13,6 +13,7 @@ export * from './authorization/Authorization';
export * from './authorization/Authorizer'; export * from './authorization/Authorizer';
export * from './authorization/AuxiliaryAuthorizer'; export * from './authorization/AuxiliaryAuthorizer';
export * from './authorization/DenyAllAuthorizer'; export * from './authorization/DenyAllAuthorizer';
export * from './authorization/PathBasedAuthorizer';
export * from './authorization/WebAclAuthorization'; export * from './authorization/WebAclAuthorization';
export * from './authorization/WebAclAuthorizer'; export * from './authorization/WebAclAuthorizer';

View File

@ -0,0 +1,51 @@
import type { Authorizer, AuthorizerArgs } from '../../../src/authorization/Authorizer';
import { PathBasedAuthorizer } from '../../../src/authorization/PathBasedAuthorizer';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
describe('A PathBasedAuthorizer', (): void => {
const baseUrl = 'http://test.com/foo/';
let input: AuthorizerArgs;
let authorizers: jest.Mocked<Authorizer>[];
let authorizer: PathBasedAuthorizer;
beforeEach(async(): Promise<void> => {
input = {
identifier: { path: `${baseUrl}first` },
permissions: { read: true, append: false, write: false, control: false },
credentials: { webId: 'http://alice.test.com/card#me' },
};
authorizers = [
{ canHandle: jest.fn(), handle: jest.fn() },
{ canHandle: jest.fn(), handle: jest.fn() },
] as any;
const paths = {
'/first': authorizers[0],
'/second': authorizers[1],
};
authorizer = new PathBasedAuthorizer(baseUrl, paths);
});
it('can only handle requests with a matching path.', async(): Promise<void> => {
input.identifier.path = 'http://wrongsite/';
await expect(authorizer.canHandle(input)).rejects.toThrow(NotImplementedHttpError);
input.identifier.path = `${baseUrl}third`;
await expect(authorizer.canHandle(input)).rejects.toThrow(NotImplementedHttpError);
input.identifier.path = `${baseUrl}first`;
await expect(authorizer.canHandle(input)).resolves.toBeUndefined();
input.identifier.path = `${baseUrl}second`;
await expect(authorizer.canHandle(input)).resolves.toBeUndefined();
});
it('can only handle requests supported by the stored authorizers.', async(): Promise<void> => {
await expect(authorizer.canHandle(input)).resolves.toBeUndefined();
authorizers[0].canHandle.mockRejectedValueOnce(new Error('not supported'));
await expect(authorizer.canHandle(input)).rejects.toThrow('not supported');
});
it('passes the handle requests to the matching authorizer.', async(): Promise<void> => {
await expect(authorizer.handle(input)).resolves.toBeUndefined();
expect(authorizers[0].handle).toHaveBeenCalledTimes(1);
expect(authorizers[0].handle).toHaveBeenLastCalledWith(input);
});
});