-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
104 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |