1
- import type { AwsCredentialIdentity } from '@smithy/types' ;
1
+ import { inspect } from 'util' ;
2
+ import type { AwsCredentialIdentity , AwsCredentialIdentityProvider } from '@smithy/types' ;
2
3
import { debug , warning } from '../../logging' ;
3
- import { CredentialProviderSource , Mode , PluginHost } from '../plugin' ;
4
+ import { CredentialProviderSource , PluginProviderResult , Mode , PluginHost , SDKv2CompatibleCredentials , SDKv3CompatibleCredentialProvider , SDKv3CompatibleCredentials } from '../plugin' ;
5
+ import { credentialsAboutToExpire , makeCachingProvider } from './provider-caching' ;
4
6
5
7
/**
6
8
* Cache for credential providers.
@@ -15,9 +17,14 @@ import { CredentialProviderSource, Mode, PluginHost } from '../plugin';
15
17
* for the given account.
16
18
*/
17
19
export class CredentialPlugins {
18
- private readonly cache : { [ key : string ] : PluginCredentials | undefined } = { } ;
20
+ private readonly cache : { [ key : string ] : PluginCredentialsFetchResult | undefined } = { } ;
21
+ private readonly host : PluginHost ;
19
22
20
- public async fetchCredentialsFor ( awsAccountId : string , mode : Mode ) : Promise < PluginCredentials | undefined > {
23
+ constructor ( host ?: PluginHost ) {
24
+ this . host = host ?? PluginHost . instance ;
25
+ }
26
+
27
+ public async fetchCredentialsFor ( awsAccountId : string , mode : Mode ) : Promise < PluginCredentialsFetchResult | undefined > {
21
28
const key = `${ awsAccountId } -${ mode } ` ;
22
29
if ( ! ( key in this . cache ) ) {
23
30
this . cache [ key ] = await this . lookupCredentials ( awsAccountId , mode ) ;
@@ -26,13 +33,13 @@ export class CredentialPlugins {
26
33
}
27
34
28
35
public get availablePluginNames ( ) : string [ ] {
29
- return PluginHost . instance . credentialProviderSources . map ( ( s ) => s . name ) ;
36
+ return this . host . credentialProviderSources . map ( ( s ) => s . name ) ;
30
37
}
31
38
32
- private async lookupCredentials ( awsAccountId : string , mode : Mode ) : Promise < PluginCredentials | undefined > {
39
+ private async lookupCredentials ( awsAccountId : string , mode : Mode ) : Promise < PluginCredentialsFetchResult | undefined > {
33
40
const triedSources : CredentialProviderSource [ ] = [ ] ;
34
41
// Otherwise, inspect the various credential sources we have
35
- for ( const source of PluginHost . instance . credentialProviderSources ) {
42
+ for ( const source of this . host . credentialProviderSources ) {
36
43
let available : boolean ;
37
44
try {
38
45
available = await source . isAvailable ( ) ;
@@ -59,28 +66,108 @@ export class CredentialPlugins {
59
66
continue ;
60
67
}
61
68
debug ( `Using ${ source . name } credentials for account ${ awsAccountId } ` ) ;
62
- const providerOrCreds = await source . getProvider ( awsAccountId , mode ) ;
63
-
64
- // Backwards compatibility: if the plugin returns a ProviderChain, resolve that chain.
65
- // Otherwise it must have returned credentials.
66
- const credentials = ( providerOrCreds as any ) . resolvePromise
67
- ? await ( providerOrCreds as any ) . resolvePromise ( )
68
- : providerOrCreds ;
69
-
70
- // Another layer of backwards compatibility: in SDK v2, the credentials object
71
- // is both a container and a provider. So we need to force the refresh using getPromise.
72
- // In SDK v3, these two responsibilities are separate, and the getPromise doesn't exist.
73
- if ( ( credentials as any ) . getPromise ) {
74
- await ( credentials as any ) . getPromise ( ) ;
75
- }
76
69
77
- return { credentials, pluginName : source . name } ;
70
+ return {
71
+ credentials : await v3ProviderFromPlugin ( ( ) => source . getProvider ( awsAccountId , mode , {
72
+ supportsV3Providers : true ,
73
+ } ) ) ,
74
+ pluginName : source . name ,
75
+ } ;
78
76
}
79
77
return undefined ;
80
78
}
81
79
}
82
80
83
- export interface PluginCredentials {
84
- readonly credentials : AwsCredentialIdentity ;
81
+ /**
82
+ * Result from trying to fetch credentials from the Plugin host
83
+ */
84
+ export interface PluginCredentialsFetchResult {
85
+ /**
86
+ * SDK-v3 compatible credential provider
87
+ */
88
+ readonly credentials : AwsCredentialIdentityProvider ;
89
+
90
+ /**
91
+ * Name of plugin that successfully provided credentials
92
+ */
85
93
readonly pluginName : string ;
86
94
}
95
+
96
+ /**
97
+ * Take a function that calls the plugin, and turn it into an SDKv3-compatible credential provider.
98
+ *
99
+ * What we will do is the following:
100
+ *
101
+ * - Query the plugin and see what kind of result it gives us.
102
+ * - If the result is self-refreshing or doesn't need refreshing, we turn it into an SDKv3 provider
103
+ * and return it directly.
104
+ * * If the underlying return value is a provider, we will make it a caching provider
105
+ * (because we can't know if it will cache by itself or not).
106
+ * * If the underlying return value is a static credential, caching isn't relevant.
107
+ * * If the underlying return value is V2 credentials, those have caching built-in.
108
+ * - If the result is a static credential that expires, we will wrap it in an SDKv3 provider
109
+ * that will query the plugin again when the credential expires.
110
+ */
111
+ async function v3ProviderFromPlugin ( producer : ( ) => Promise < PluginProviderResult > ) : Promise < AwsCredentialIdentityProvider > {
112
+ const initial = await producer ( ) ;
113
+
114
+ if ( isV3Provider ( initial ) ) {
115
+ // Already a provider, make caching
116
+ return makeCachingProvider ( initial ) ;
117
+ } else if ( isV3Credentials ( initial ) && initial . expiration === undefined ) {
118
+ // Static credentials that don't need refreshing nor caching
119
+ return ( ) => Promise . resolve ( initial ) ;
120
+ } else if ( isV3Credentials ( initial ) && initial . expiration !== undefined ) {
121
+ // Static credentials that do need refreshing and caching
122
+ return refreshFromPluginProvider ( initial , producer ) ;
123
+ } else if ( isV2Credentials ( initial ) ) {
124
+ // V2 credentials that refresh and cache themselves
125
+ return v3ProviderFromV2Credentials ( initial ) ;
126
+ } else {
127
+ throw new Error ( `Plugin returned a value that doesn't resemble AWS credentials: ${ inspect ( initial ) } ` ) ;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Converts a V2 credential into a V3-compatible provider
133
+ */
134
+ function v3ProviderFromV2Credentials ( x : SDKv2CompatibleCredentials ) : AwsCredentialIdentityProvider {
135
+ return async ( ) => {
136
+ // Get will fetch or refresh as necessary
137
+ await x . getPromise ( ) ;
138
+
139
+ return {
140
+ accessKeyId : x . accessKeyId ,
141
+ secretAccessKey : x . secretAccessKey ,
142
+ sessionToken : x . sessionToken ,
143
+ expiration : x . expireTime ,
144
+ } ;
145
+ } ;
146
+ }
147
+
148
+ function refreshFromPluginProvider ( current : AwsCredentialIdentity , producer : ( ) => Promise < PluginProviderResult > ) : AwsCredentialIdentityProvider {
149
+ return async ( ) => {
150
+ // eslint-disable-next-line no-console
151
+ console . error ( current , Date . now ( ) ) ;
152
+ if ( credentialsAboutToExpire ( current ) ) {
153
+ const newCreds = await producer ( ) ;
154
+ if ( ! isV3Credentials ( newCreds ) ) {
155
+ throw new Error ( `Plugin initially returned static V3 credentials but now returned something else: ${ inspect ( newCreds ) } ` ) ;
156
+ }
157
+ current = newCreds ;
158
+ }
159
+ return current ;
160
+ } ;
161
+ }
162
+
163
+ function isV3Provider ( x : PluginProviderResult ) : x is SDKv3CompatibleCredentialProvider {
164
+ return typeof x === 'function' ;
165
+ }
166
+
167
+ function isV2Credentials ( x : PluginProviderResult ) : x is SDKv2CompatibleCredentials {
168
+ return ! ! ( x && typeof x === 'object' && x . accessKeyId && ( x as SDKv2CompatibleCredentials ) . getPromise ) ;
169
+ }
170
+
171
+ function isV3Credentials ( x : PluginProviderResult ) : x is SDKv3CompatibleCredentials {
172
+ return ! ! ( x && typeof x === 'object' && x . accessKeyId && ! isV2Credentials ( x ) ) ;
173
+ }
0 commit comments