Skip to content

Commit

Permalink
feat: Create PathBasedAuthorizer
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Jul 27, 2021
1 parent d95db60 commit f4833d2
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/authorization/PathBasedAuthorizer.ts
Original file line number Diff line number Diff line change
@@ -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.');
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './authorization/Authorization';
export * from './authorization/Authorizer';
export * from './authorization/AuxiliaryAuthorizer';
export * from './authorization/DenyAllAuthorizer';
export * from './authorization/PathBasedAuthorizer';
export * from './authorization/WebAclAuthorization';
export * from './authorization/WebAclAuthorizer';

Expand Down
51 changes: 51 additions & 0 deletions test/unit/authorization/PathBasedAuthorizer.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit f4833d2

Please sign in to comment.