-
Notifications
You must be signed in to change notification settings - Fork 5
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
Feature/readsecrets + brief readme #6
base: master
Are you sure you want to change the base?
Changes from 24 commits
e2bd89c
c71e92f
033358e
c9f9732
e6c7ac4
8e895b8
3220f70
923bc00
c37c1d8
158f1c8
ba45d72
dc50d15
0e890ba
970cc0f
6144dd0
7ff33c9
6656321
bc466bd
84c4f30
11b624a
ade1a80
d25862c
7ccd1f3
90ab2fc
1986cd1
581abff
de7e7b7
5a144d8
63a7fb0
2e7a030
d31c5ab
b310b89
38b4024
c44409f
abefcf1
d872778
bbb0e73
a226e38
3ef9303
c71e870
7da1325
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
// Require the libraries | ||
import localVarRequest from 'request'; | ||
import querystring from 'querystring' | ||
|
||
import querystring from 'querystring'; | ||
|
||
// Import a list of the LUSID APIs | ||
const lusid = require('../api'); | ||
|
@@ -15,12 +14,6 @@ It contains a property for each of the LUSID APIs. | |
*/ | ||
import {Api} from "../apis"; | ||
|
||
|
||
// Set the default path to the secrets file | ||
const secretsPath = './secrets.json' | ||
// Set the default amount of seconds before token expiry to call a refresh | ||
const refreshLimit = 3580 | ||
|
||
/* | ||
To authenticate with a third party identity provider, a number of credentials | ||
are required e.g. username, password, clientId, clientSecret etc. | ||
|
@@ -124,39 +117,98 @@ export class Client { | |
basePath: string | ||
// The available API endpoints | ||
api: Api | ||
|
||
// The path to the secrets file which may be used to store credentials | ||
secretsFilePath: string | ||
secretsFilePath: string = process.cwd() + '/secrets.json' | ||
secretsFileContent: { api: {} } | ||
|
||
// The credential access details | ||
private tokenUrlDetails: [Source, string] | ||
private usernameDetails: [Source, string] | ||
private passwordDetails: [Source, string] | ||
private clientIdDetails: [Source, string] | ||
private clientSecretDetails: [Source, string] | ||
|
||
// The refresh limit in seconds before token expiry to trigger a refresh | ||
refreshLimit: number = refreshLimit | ||
refreshLimit: number = 3580 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also allow providing a custom refreshLimit with this set as a sensible default? |
||
|
||
private loadSecretsFile(): object | null { | ||
|
||
try | ||
{ | ||
this.secretsFileContent = require( this.secretsFilePath ); | ||
} | ||
catch( e ) | ||
{ | ||
// not necessarily a problem. even if the format of the source file is bad, this will reset the internal validation to an empty configuration | ||
|
||
this.secretsFileContent = { api: {} }; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are your thoughts on try/catch handling vs if/then handling? We could check that the file exists and if it does not raise a warning rather than silently defaulting to a default empty set of values. I'm not sure which is better suited to Typescript and more specifically Node.js |
||
|
||
return this.secretsFileContent; | ||
|
||
} | ||
|
||
// Constructor method which takes the details on where to find the credentials | ||
constructor( | ||
tokenUrlDetails: [Source, string], | ||
usernameDetails: [Source, string], | ||
passwordDetails: [Source, string], | ||
clientIdDetails: [Source, string], | ||
clientSecretDetails: [Source, string], | ||
apiUrlDetails: [Source, string], | ||
{ | ||
tokenUrlDetails, | ||
usernameDetails, | ||
passwordDetails, | ||
clientIdDetails, | ||
clientSecretDetails, | ||
apiUrlDetails | ||
}: { | ||
tokenUrlDetails?: [Source, string], | ||
usernameDetails?: [Source, string], | ||
passwordDetails?: [Source, string], | ||
clientIdDetails?: [Source, string], | ||
clientSecretDetails?: [Source, string], | ||
apiUrlDetails?: [Source, string], | ||
} = { | ||
tokenUrlDetails: [Source.Secrets, 'tokenUrl'], | ||
usernameDetails: [Source.Secrets, 'username'], | ||
passwordDetails: [Source.Secrets, 'password'], | ||
clientIdDetails: [Source.Secrets, 'clientId'], | ||
clientSecretDetails: [Source.Secrets, 'clientSecret'], | ||
apiUrlDetails: [Source.Secrets, 'apiUrl'] | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting! What are the benefits to this over having the types in the constructor as there were previously? |
||
) { | ||
|
||
// Set the path to the secrets file | ||
this.secretsFilePath = secretsPath | ||
|
||
// Set the credential details | ||
this.tokenUrlDetails = tokenUrlDetails | ||
this.usernameDetails = usernameDetails | ||
this.passwordDetails = passwordDetails | ||
this.clientIdDetails = clientIdDetails | ||
this.clientSecretDetails = clientSecretDetails | ||
|
||
// Set the base path for the API | ||
this.basePath = this.fetchCredentials(apiUrlDetails[0], apiUrlDetails[1]) | ||
// attempt to get disk config by default. if it doesn't exist it is still fine | ||
this.loadSecretsFile(); | ||
|
||
// provide init + default to environment vars below | ||
this.tokenUrlDetails = !!tokenUrlDetails ? tokenUrlDetails : ( | ||
// [Source.Environment, 'FBN_TOKEN_URL'] | ||
[Source.Secrets, 'tokenUrl'] | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the defaults are already specified in the constructor above in what situation would !!tokenUrlDetails resolve to False? In that situation what is the value of providing the same details "[Source.Secrets, 'tokenUrl']" as the default? |
||
|
||
this.usernameDetails = !!usernameDetails ? usernameDetails : ( | ||
// [Source.Environment, 'FBN_USERNAME'] | ||
[Source.Secrets, 'username'] | ||
); | ||
|
||
this.passwordDetails = !!passwordDetails ? passwordDetails : ( | ||
// [Source.Environment, 'FBN_PASSWORD'] | ||
[Source.Secrets, 'password'] | ||
); | ||
|
||
this.clientIdDetails = !!clientIdDetails ? clientIdDetails : ( | ||
// [Source.Environment, 'FBN_CLIENT_ID'] | ||
[Source.Secrets, 'clientId'] | ||
); | ||
|
||
this.clientSecretDetails = !!clientSecretDetails ? clientSecretDetails : ( | ||
// [Source.Environment, 'FBN_CLIENT_SECRET'] | ||
[Source.Secrets, 'clientSecret'] | ||
); | ||
|
||
// initialize base path at this moment, since it is needed below | ||
this.basePath = !!apiUrlDetails ? this.fetchConfigurationItem( apiUrlDetails[0], apiUrlDetails[1] ) : ( | ||
// this.fetchConfigurationItem( Source.Environment, 'FBN_CLIENT_SECRET' ) | ||
this.fetchConfigurationItem( Source.Secrets, 'apiUrl' ) | ||
); | ||
|
||
// Set the authentications to use oauth2 | ||
this.authentications = {'oauth2': new Oauth2(undefined, 0,0,0,0)} | ||
|
@@ -183,8 +235,8 @@ export class Client { | |
// Wrap each method with token refresh logic | ||
this.api[apiName][prop] = this.apiFunctionWrapper( | ||
this.api[apiName][prop], | ||
this.api[apiName], | ||
this) | ||
this.api[apiName] | ||
) | ||
} | ||
|
||
} | ||
|
@@ -196,91 +248,83 @@ export class Client { | |
The function below is a wrapper function which wraps the input function | ||
'apiFunction' with token refresh logic to ensure uninterrupted access to LUSID. | ||
*/ | ||
public apiFunctionWrapper(apiFunction, api, self) { | ||
|
||
// Return a function | ||
return function() { | ||
// Collect the arguments passed into the wrapper to use later | ||
var topLevelArguments = arguments | ||
// Return a promise to ensure that the function remains 'thenable' | ||
return new Promise(function(resolve, reject) { | ||
private apiFunctionWrapper(apiFunction, api ) { | ||
|
||
// Return a function, thus not immediately invoking | ||
|
||
return ( ...args ) => { | ||
MikeMcGarry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Return a promise to ensure that the function remains '.then()-able' | ||
return new Promise( (resolve, reject) => { | ||
MikeMcGarry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Trigger a token refresh | ||
var oauthPopulated = self.refreshToken( | ||
self.authentications['oauth2'], | ||
self.refreshLimit, | ||
self.tokenUrlDetails, | ||
self.usernameDetails, | ||
self.passwordDetails, | ||
self.clientIdDetails, | ||
self.clientSecretDetails | ||
) | ||
// Once this has completed pass the oauth2 details on | ||
oauthPopulated.then(function(oauth2Details: Oauth2) { | ||
this.refreshToken( | ||
this.authentications.oauth2, | ||
this.refreshLimit, | ||
this.tokenUrlDetails, | ||
this.usernameDetails, | ||
this.passwordDetails, | ||
this.clientIdDetails, | ||
this.clientSecretDetails | ||
).then( (oauth2Details: Oauth2) => { | ||
|
||
// Update the clients oauth2 details | ||
self.authentications.oauth2 = oauth2Details | ||
this.authentications.oauth2 = oauth2Details | ||
// Update the access token of the api being called | ||
api['authentications']['oauth2']['accessToken'] = self.authentications.oauth2.accessToken | ||
api.authentications.oauth2.accessToken = this.authentications.oauth2.accessToken | ||
|
||
/* | ||
Resolve the promise with the function that was wrapped | ||
In this case api is the api that this function is a part of, | ||
this is required to ensure that the function is called | ||
in the right context. The second argument topLevelArguments | ||
is the arguments passed into the Wrapper | ||
*/ | ||
resolve(apiFunction.apply(api, topLevelArguments)) | ||
|
||
resolve( apiFunction.apply( api, args ) ); | ||
|
||
}) | ||
// Error handling | ||
.catch((err) => reject(err)) | ||
}) | ||
} | ||
} | ||
|
||
/* | ||
The fetchCredentials function gets the credentials from the specified source | ||
*/ | ||
private fetchCredentials(source: Source, value: string): string { | ||
|
||
// Environment source | ||
if (source == Source.Environment) { | ||
// Set the credential using an environment variable | ||
var credential: string = process.env[value] || 'MISSING' | ||
// If it the environment variable does not exist then throw error | ||
if (credential == 'MISSING') { | ||
throw "Environment variable " + value + " has not been specified" | ||
} | ||
private fetchConfigurationItem( sourceType: Source, itemName: string ): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice function! I like that it is not requiring the secrets file for each item as it was earlier. |
||
|
||
// Raw or variable source | ||
} else if (source == Source.Raw) { | ||
// Set the credential directly | ||
var credential: string = value | ||
|
||
// Secrets source | ||
} else if (source = Source.Secrets) { | ||
// Try and import the credentials file | ||
try { | ||
var credentials: any = require(this.secretsFilePath) | ||
// If there are any problems throw an error | ||
} catch (e) { | ||
throw "File " + this.secretsFilePath + " could not be found/read, ensure that it exists in the client folder" | ||
} | ||
// If the property exists in the secrets file set the credential | ||
if (credentials.hasOwnProperty(value)) { | ||
var credential: string = credentials[value] | ||
} else { | ||
throw "The specified value " + value + " does not exist in the file" | ||
} | ||
switch( sourceType ) | ||
{ | ||
case Source.Environment: | ||
|
||
if( !!process.env[ itemName ] ) | ||
{ | ||
return process.env[ itemName ]; | ||
} | ||
|
||
throw `Environment variable ${itemName} has not been specified`; | ||
|
||
break; | ||
case Source.Raw: | ||
|
||
return itemName; | ||
|
||
break; | ||
case Source.Secrets: | ||
|
||
if( !!this.secretsFileContent.api[ itemName ] ) | ||
{ | ||
return this.secretsFileContent.api[ itemName ]; | ||
} | ||
|
||
throw `Configuration item ${itemName} not found in secrets file`; | ||
|
||
break; | ||
default: | ||
|
||
throw `Source is not valid, must be one of ${Object.keys( Source ).join( ", " )}`; | ||
|
||
// If the source is incorrectly specified | ||
} else { | ||
let availableSources: string[] | ||
for (var availableSource in Source) { | ||
availableSources.push(availableSource) | ||
} | ||
throw "Source is not valid, must be one of " + availableSources.toString() | ||
} | ||
|
||
// Return the credential | ||
return credential | ||
} | ||
|
||
/* | ||
|
@@ -313,11 +357,11 @@ export class Client { | |
|
||
// If so, populate the credentials | ||
var credentials = new Credentials( | ||
this.fetchCredentials(tokenUrlDetails[0], tokenUrlDetails[1]), | ||
this.fetchCredentials(usernameDetails[0], usernameDetails[1]), | ||
this.fetchCredentials(passwordDetails[0], passwordDetails[1]), | ||
this.fetchCredentials(clientIdDetails[0], clientIdDetails[1]), | ||
this.fetchCredentials(clientSecretDetails[0], clientSecretDetails[1]) | ||
this.fetchConfigurationItem(tokenUrlDetails[0], tokenUrlDetails[1]), | ||
this.fetchConfigurationItem(usernameDetails[0], usernameDetails[1]), | ||
this.fetchConfigurationItem(passwordDetails[0], passwordDetails[1]), | ||
this.fetchConfigurationItem(clientIdDetails[0], clientIdDetails[1]), | ||
this.fetchConfigurationItem(clientSecretDetails[0], clientSecretDetails[1]) | ||
) | ||
|
||
// Get a new access token using these credentials | ||
|
@@ -385,7 +429,8 @@ export class Client { | |
clientId: string, | ||
clientSecret: string | ||
): Promise<Oauth2> { | ||
// Returns a rpomise | ||
|
||
// Returns a promise | ||
|
||
return new Promise((resolve, reject) => { | ||
|
||
|
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.
Now that we are looking at this in detail, do we think the "secrets.json" path should live in the client class or should the file path be passed into the constructor? This allows any file path to be specified when creating a client.