-
Notifications
You must be signed in to change notification settings - Fork 619
Add a base credential provider package #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,4 @@ | ||
/node_modules/ | ||
*.js | ||
*.js.map | ||
*.d.ts |
11 changes: 11 additions & 0 deletions
11
packages/credential-provider-base/__tests__/CredentialError.ts
This file contains hidden or 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,11 @@ | ||
import {CredentialError} from "../lib/CredentialError"; | ||
|
||
describe('CredentialError', () => { | ||
it('should direct the chain to proceed to the next link by default', () => { | ||
expect(new CredentialError('PANIC').tryNextLink).toBe(true); | ||
}); | ||
|
||
it('should allow errors to halt the chain', () => { | ||
expect(new CredentialError('PANIC', false).tryNextLink).toBe(false); | ||
}); | ||
}); |
This file contains hidden or 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,70 @@ | ||
import {chain} from "../lib/chain"; | ||
import {fromCredentials} from "../lib/fromCredentials"; | ||
import {isCredentials} from "../lib/isCredentials"; | ||
import {CredentialError} from "../lib/CredentialError"; | ||
|
||
describe('chain', () => { | ||
it('should distill many credential providers into one', async () => { | ||
const provider = chain( | ||
fromCredentials({accessKeyId: 'foo', secretAccessKey: 'bar'}), | ||
fromCredentials({accessKeyId: 'baz', secretAccessKey: 'quux'}), | ||
); | ||
|
||
expect(isCredentials(await provider())).toBe(true); | ||
}); | ||
|
||
it('should return the resolved value of the first successful promise', async () => { | ||
const creds = {accessKeyId: 'foo', secretAccessKey: 'bar'}; | ||
const provider = chain( | ||
() => Promise.reject(new CredentialError('Move along')), | ||
() => Promise.reject(new CredentialError('Nothing to see here')), | ||
fromCredentials(creds) | ||
); | ||
|
||
expect(await provider()).toEqual(creds); | ||
}); | ||
|
||
it('should not invoke subsequent providers one resolves', async () => { | ||
const creds = {accessKeyId: 'foo', secretAccessKey: 'bar'}; | ||
const providers = [ | ||
jest.fn(() => Promise.reject(new CredentialError('Move along'))), | ||
jest.fn(() => Promise.resolve(creds)), | ||
jest.fn(() => fail('This provider should not be invoked')) | ||
]; | ||
|
||
expect(await chain(...providers)()).toEqual(creds); | ||
expect(providers[0].mock.calls.length).toBe(1); | ||
expect(providers[1].mock.calls.length).toBe(1); | ||
expect(providers[2].mock.calls.length).toBe(0); | ||
}); | ||
|
||
it( | ||
'should not invoke subsequent providers one is rejected with a terminal error', | ||
async () => { | ||
const providers = [ | ||
jest.fn(() => Promise.reject(new CredentialError('Move along'))), | ||
jest.fn(() => Promise.reject( | ||
new CredentialError('Stop here', false) | ||
)), | ||
jest.fn(() => fail('This provider should not be invoked')) | ||
]; | ||
|
||
await chain(...providers)().then( | ||
() => { throw new Error('The promise should have been rejected'); }, | ||
err => { | ||
expect(err.message).toBe('Stop here'); | ||
expect(providers[0].mock.calls.length).toBe(1); | ||
expect(providers[1].mock.calls.length).toBe(1); | ||
expect(providers[2].mock.calls.length).toBe(0); | ||
} | ||
); | ||
} | ||
); | ||
|
||
it('should reject chains with no links', async () => { | ||
await chain()().then( | ||
() => { throw new Error('The promise should have been rejected'); }, | ||
() => { /* Promise rejected as expected */ } | ||
); | ||
}); | ||
}); |
18 changes: 18 additions & 0 deletions
18
packages/credential-provider-base/__tests__/fromCredentials.ts
This file contains hidden or 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,18 @@ | ||
import {CredentialProvider, Credentials} from "@aws/types"; | ||
import {fromCredentials} from "../lib/fromCredentials"; | ||
|
||
describe('fromCredentials', () => { | ||
it('should convert credentials into a credential provider', async () => { | ||
const credentials: Credentials = { | ||
accessKeyId: 'foo', | ||
secretAccessKey: 'bar', | ||
sessionToken: 'baz', | ||
expiration: Math.floor(Date.now().valueOf() / 1000), | ||
}; | ||
const provider: CredentialProvider = fromCredentials(credentials); | ||
|
||
expect(typeof provider).toBe('function'); | ||
expect(provider()).toBeInstanceOf(Promise); | ||
expect(await provider()).toEqual(credentials); | ||
}); | ||
}); |
57 changes: 57 additions & 0 deletions
57
packages/credential-provider-base/__tests__/isCredentials.ts
This file contains hidden or 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,57 @@ | ||
import {isCredentials} from "../lib/isCredentials"; | ||
|
||
describe('isCredentials', () => { | ||
const minimalCredentials = {accessKeyId: 'foo', secretAccessKey: 'bar'}; | ||
|
||
it('should reject scalar values', () => { | ||
for (let scalar of ['foo', 12, 1.2, true, null, undefined]) { | ||
expect(isCredentials(scalar)).toBe(false); | ||
} | ||
}); | ||
|
||
it('should accept an object with an accessKeyId and secretAccessKey', () => { | ||
expect(isCredentials(minimalCredentials)).toBe(true); | ||
}); | ||
|
||
it('should reject objects where accessKeyId is not a string', () => { | ||
expect(isCredentials({ | ||
...minimalCredentials, | ||
accessKeyId: 123, | ||
})).toBe(false); | ||
}); | ||
|
||
it('should reject objects where secretAccessKey is not a string', () => { | ||
expect(isCredentials({ | ||
...minimalCredentials, | ||
secretAccessKey: 123, | ||
})).toBe(false); | ||
}); | ||
|
||
it('should accept credentials with a sessionToken', () => { | ||
expect(isCredentials({ | ||
...minimalCredentials, | ||
sessionToken: 'baz', | ||
})).toBe(true); | ||
}); | ||
|
||
it('should reject credentials where sessionToken is not a string', () => { | ||
expect(isCredentials({ | ||
...minimalCredentials, | ||
sessionToken: 123, | ||
})).toBe(false); | ||
}); | ||
|
||
it('should accept credentials with an expiration', () => { | ||
expect(isCredentials({ | ||
...minimalCredentials, | ||
expiration: 0, | ||
})).toBe(true); | ||
}); | ||
|
||
it('should reject credentials where expiration is not a number', () => { | ||
expect(isCredentials({ | ||
...minimalCredentials, | ||
expiration: 'quux', | ||
})).toBe(false); | ||
}); | ||
}); |
This file contains hidden or 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,38 @@ | ||
import {memoize} from "../lib/memoize"; | ||
|
||
describe('memoize', () => { | ||
it('should cache the resolved provider for permanent credentials', async () => { | ||
const creds = {accessKeyId: 'foo', secretAccessKey: 'bar'}; | ||
const provider = jest.fn(() => Promise.resolve(creds)); | ||
const memoized = memoize(provider); | ||
|
||
expect(await memoized()).toEqual(creds); | ||
expect(provider.mock.calls.length).toBe(1); | ||
expect(await memoized()).toEqual(creds); | ||
expect(provider.mock.calls.length).toBe(1); | ||
}); | ||
|
||
it('should invoke provider again when credentials expire', async () => { | ||
const clockMock = Date.now = jest.fn(); | ||
clockMock.mockReturnValue(0); | ||
const provider = jest.fn(() => Promise.resolve({ | ||
accessKeyId: 'foo', | ||
secretAccessKey: 'bar', | ||
expiration: Date.now() + 600, // expires in ten minutes | ||
})); | ||
const memoized = memoize(provider); | ||
|
||
expect((await memoized()).accessKeyId).toEqual('foo'); | ||
expect(provider.mock.calls.length).toBe(1); | ||
expect((await memoized()).secretAccessKey).toEqual('bar'); | ||
expect(provider.mock.calls.length).toBe(1); | ||
|
||
clockMock.mockReset(); | ||
clockMock.mockReturnValue(601000); // One second past previous expiration | ||
|
||
expect((await memoized()).accessKeyId).toEqual('foo'); | ||
expect(provider.mock.calls.length).toBe(2); | ||
expect((await memoized()).secretAccessKey).toEqual('bar'); | ||
expect(provider.mock.calls.length).toBe(2); | ||
}); | ||
}); |
This file contains hidden or 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,5 @@ | ||
export * from './lib/chain'; | ||
export * from './lib/CredentialError'; | ||
export * from './lib/fromCredentials'; | ||
export * from './lib/isCredentials'; | ||
export * from './lib/memoize'; |
This file contains hidden or 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,16 @@ | ||
import {chain} from './chain'; | ||
|
||
/** | ||
* An error representing a failure of an individual credential provider. | ||
* | ||
* This error class has special meaning to the {@link chain} method. If a | ||
* provider in the chain is rejected with an error, the chain will only proceed | ||
* to the next provider if the value of the `tryNextLink` property on the error | ||
* is truthy. This allows individual providers to halt the chain and also | ||
* ensures the chain will stop if an entirely unexpected error is encountered. | ||
*/ | ||
export class CredentialError extends Error { | ||
constructor(message: string, public readonly tryNextLink: boolean = true) { | ||
super(message); | ||
} | ||
} |
This file contains hidden or 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,39 @@ | ||
import {CredentialProvider} from "@aws/types"; | ||
import {CredentialError} from "./CredentialError"; | ||
|
||
/** | ||
* Compose a single credential provider function from multiple credential | ||
* providers. The first provider in the argument list will always be invoked; | ||
* subsequent providers in the list will be invoked in the order in which the | ||
* were received if the preceding provider did not successfully resolve. | ||
* | ||
* If no providers were received or no provider resolves successfully, the | ||
* returned promise will be rejected. | ||
*/ | ||
export function chain( | ||
...providers: Array<CredentialProvider> | ||
): CredentialProvider { | ||
return () => { | ||
providers = providers.slice(0); | ||
let provider = providers.shift(); | ||
if (provider === undefined) { | ||
return Promise.reject(new CredentialError( | ||
'No credential providers in chain' | ||
)); | ||
} | ||
let promise = provider(); | ||
while (provider = providers.shift()) { | ||
promise = promise.catch((provider => { | ||
return (err: CredentialError) => { | ||
if (err.tryNextLink) { | ||
return provider(); | ||
} | ||
|
||
throw err; | ||
} | ||
})(provider)); | ||
} | ||
|
||
return promise; | ||
} | ||
} |
This file contains hidden or 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,10 @@ | ||
import {CredentialProvider, Credentials} from "@aws/types"; | ||
|
||
/** | ||
* Convert a static credentials object into a credential provider function. | ||
*/ | ||
export function fromCredentials( | ||
credentials: Credentials | ||
): CredentialProvider { | ||
return () => Promise.resolve(credentials); | ||
} |
This file contains hidden or 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,14 @@ | ||
import {Credentials} from '@aws/types'; | ||
|
||
/** | ||
* Evaluate the provided argument and determine if it represents a static | ||
* credentials object. | ||
*/ | ||
export function isCredentials(arg: any): arg is Credentials { | ||
return typeof arg === 'object' | ||
&& arg !== null | ||
&& typeof arg.accessKeyId === 'string' | ||
&& typeof arg.secretAccessKey === 'string' | ||
&& ['string', 'undefined'].indexOf(typeof arg.sessionToken) > -1 | ||
&& ['number', 'undefined'].indexOf(typeof arg.expiration) > -1; | ||
} |
This file contains hidden or 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,36 @@ | ||
import {CredentialProvider} from "@aws/types"; | ||
|
||
/** | ||
* Decorates a credential provider with credential-specific memoization. If the | ||
* decorated provider returns permanent credentials, it will only be invoked | ||
* once; if the decorated provider returns temporary credentials, it will be | ||
* invoked again when the validity of the returned credentials is less than 5 | ||
* minutes. | ||
*/ | ||
export function memoize(provider: CredentialProvider): CredentialProvider { | ||
let result = provider(); | ||
let isConstant: boolean = false; | ||
|
||
return () => { | ||
if (isConstant) { | ||
return result; | ||
} | ||
|
||
return result.then(credentials => { | ||
if (!credentials.expiration) { | ||
isConstant = true; | ||
return credentials; | ||
} | ||
|
||
if (credentials.expiration - 300 > getEpochTs()) { | ||
return credentials; | ||
} | ||
|
||
return result = provider(); | ||
}); | ||
} | ||
} | ||
|
||
function getEpochTs() { | ||
return Math.floor(Date.now() / 1000); | ||
} |
This file contains hidden or 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,26 @@ | ||
{ | ||
"name": "@aws/credential-provider-base", | ||
"version": "0.0.1", | ||
"private": true, | ||
"description": "AWS credential provider shared core", | ||
"main": "index.js", | ||
"scripts": { | ||
"prepublishOnly": "tsc", | ||
"pretest": "tsc", | ||
"test": "jest" | ||
}, | ||
"keywords": [ | ||
"aws", | ||
"credentials" | ||
], | ||
"author": "[email protected]", | ||
"license": "UNLICENSED", | ||
"dependencies": { | ||
"@aws/types": "^0.0.1" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^19.2.2", | ||
"jest": "^19.0.2", | ||
"typescript": "^2.3" | ||
} | ||
} |
This file contains hidden or 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,13 @@ | ||
{ | ||
"compilerOptions": { | ||
"module": "commonjs", | ||
"target": "es5", | ||
"declaration": true, | ||
"strict": true, | ||
"sourceMap": true, | ||
"lib": [ | ||
"es5", | ||
"es2015.promise" | ||
] | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: Should a test also be included for sessionToken/expiration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure