-
Notifications
You must be signed in to change notification settings - Fork 125
Expand file tree
/
Copy pathenvironment-access.ts
More file actions
280 lines (254 loc) · 11.3 KB
/
Copy pathenvironment-access.ts
File metadata and controls
280 lines (254 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import * as cxapi from '@aws-cdk/cx-api';
import { SDK } from '../aws-auth';
import { EnvironmentResources, EnvironmentResourcesRegistry } from './environment-resources';
import { warn } from '../../cli/messages';
import { IoMessaging } from '../../toolkit/cli-io-host';
import { ToolkitError } from '../../toolkit/error';
import { formatErrorMessage } from '../../util/format-error';
import { CredentialsOptions, SdkForEnvironment, SdkProvider } from '../aws-auth/sdk-provider';
import { Mode } from '../plugin/mode';
import { replaceEnvPlaceholders, StringWithoutPlaceholders } from '../util/placeholders';
/**
* Access particular AWS resources, based on information from the CX manifest
*
* It is not possible to grab direct access to AWS credentials; 9 times out of 10
* we have to allow for role assumption, and role assumption can only work if
* there is a CX Manifest that contains a role ARN.
*
* This class exists so new code isn't tempted to go and get SDK credentials directly.
*/
export class EnvironmentAccess {
private readonly sdkCache = new Map<string, SdkForEnvironment>();
private readonly environmentResources: EnvironmentResourcesRegistry;
private readonly ioHost: IoMessaging['ioHost'];
private readonly action: IoMessaging['action'];
constructor(private readonly sdkProvider: SdkProvider, toolkitStackName: string, { ioHost, action }: IoMessaging) {
this.environmentResources = new EnvironmentResourcesRegistry(toolkitStackName);
this.ioHost = ioHost;
this.action = action;
}
/**
* Resolves the environment for a stack.
*/
public async resolveStackEnvironment(stack: cxapi.CloudFormationStackArtifact): Promise<cxapi.Environment> {
return this.sdkProvider.resolveEnvironment(stack.environment);
}
/**
* Get an SDK to access the given stack's environment for stack operations
*
* Will ask plugins for readonly credentials if available, use the default
* AWS credentials if not.
*
* Will assume the deploy role if configured on the stack. Check the default `deploy-role`
* policies to see what you can do with this role.
*/
public async accessStackForReadOnlyStackOperations(stack: cxapi.CloudFormationStackArtifact): Promise<TargetEnvironment> {
return this.accessStackForStackOperations(stack, Mode.ForReading);
}
/**
* Get an SDK to access the given stack's environment for stack operations
*
* Will ask plugins for mutating credentials if available, use the default AWS
* credentials if not. The `mode` parameter is only used for querying
* plugins.
*
* Will assume the deploy role if configured on the stack. Check the default `deploy-role`
* policies to see what you can do with this role.
*/
public async accessStackForMutableStackOperations(stack: cxapi.CloudFormationStackArtifact): Promise<TargetEnvironment> {
return this.accessStackForStackOperations(stack, Mode.ForWriting);
}
/**
* Get an SDK to access the given stack's environment for environmental lookups
*
* Will use a plugin if available, use the default AWS credentials if not.
* The `mode` parameter is only used for querying plugins.
*
* Will assume the lookup role if configured on the stack. Check the default `lookup-role`
* policies to see what you can do with this role. It can generally read everything
* in the account that does not require KMS access.
*
* ---
*
* For backwards compatibility reasons, there are some scenarios that are handled here:
*
* 1. The lookup role may not exist (it was added in bootstrap stack version 7). If so:
* a. Return the default credentials if the default credentials are for the stack account
* (you will notice this as `isFallbackCredentials=true`).
* b. Throw an error if the default credentials are not for the stack account.
*
* 2. The lookup role may not have the correct permissions (for example, ReadOnlyAccess was added in
* bootstrap stack version 8); the stack will have a minimum version number on it.
* a. If it does not we throw an error which should be handled in the calling
* function (and fallback to use a different role, etc)
*
* Upon success, caller will have an SDK for the right account, which may or may not have
* the right permissions.
*/
public async accessStackForLookup(stack: cxapi.CloudFormationStackArtifact): Promise<TargetEnvironment> {
if (!stack.environment) {
throw new ToolkitError(`The stack ${stack.displayName} does not have an environment`);
}
const lookupEnv = await this.prepareSdk({
environment: stack.environment,
mode: Mode.ForReading,
assumeRoleArn: stack.lookupRole?.arn,
assumeRoleExternalId: stack.lookupRole?.assumeRoleExternalId,
assumeRoleAdditionalOptions: stack.lookupRole?.assumeRoleAdditionalOptions,
});
// if we succeed in assuming the lookup role, make sure we have the correct bootstrap stack version
if (lookupEnv.didAssumeRole && stack.lookupRole?.bootstrapStackVersionSsmParameter && stack.lookupRole.requiresBootstrapStackVersion) {
const version = await lookupEnv.resources.versionFromSsmParameter(stack.lookupRole.bootstrapStackVersionSsmParameter);
if (version < stack.lookupRole.requiresBootstrapStackVersion) {
throw new ToolkitError(`Bootstrap stack version '${stack.lookupRole.requiresBootstrapStackVersion}' is required, found version '${version}'. To get rid of this error, please upgrade to bootstrap version >= ${stack.lookupRole.requiresBootstrapStackVersion}`);
}
}
if (lookupEnv.isFallbackCredentials) {
const arn = await lookupEnv.replacePlaceholders(stack.lookupRole?.arn);
await this.ioHost.notify(warn(this.action, `Lookup role ${arn} was not assumed. Proceeding with default credentials.`));
}
return lookupEnv;
}
/**
* Get an SDK to access the given stack's environment for reading stack attributes
*
* Will use a plugin if available, use the default AWS credentials if not.
* The `mode` parameter is only used for querying plugins.
*
* Will try to assume the lookup role if given, will use the regular stack operations
* access (deploy-role) otherwise. When calling this, you should assume that you will get
* the least privileged role, so don't try to use it for anything the `deploy-role`
* wouldn't be able to do. Also you cannot rely on being able to read encrypted anything.
*/
public async accessStackForLookupBestEffort(stack: cxapi.CloudFormationStackArtifact): Promise<TargetEnvironment> {
if (!stack.environment) {
throw new ToolkitError(`The stack ${stack.displayName} does not have an environment`);
}
try {
return await this.accessStackForLookup(stack);
} catch (e: any) {
await this.ioHost.notify(warn(this.action, `${formatErrorMessage(e)}`));
}
return this.accessStackForStackOperations(stack, Mode.ForReading);
}
/**
* Get an SDK to access the given stack's environment for stack operations
*
* Will use a plugin if available, use the default AWS credentials if not.
* The `mode` parameter is only used for querying plugins.
*
* Will assume the deploy role if configured on the stack. Check the default `deploy-role`
* policies to see what you can do with this role.
*/
private async accessStackForStackOperations(stack: cxapi.CloudFormationStackArtifact, mode: Mode): Promise<TargetEnvironment> {
if (!stack.environment) {
throw new ToolkitError(`The stack ${stack.displayName} does not have an environment`);
}
return this.prepareSdk({
environment: stack.environment,
mode,
assumeRoleArn: stack.assumeRoleArn,
assumeRoleExternalId: stack.assumeRoleExternalId,
assumeRoleAdditionalOptions: stack.assumeRoleAdditionalOptions,
});
}
/**
* Prepare an SDK for use in the given environment and optionally with a role assumed.
*/
private async prepareSdk(
options: PrepareSdkRoleOptions,
): Promise<TargetEnvironment> {
const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(options.environment);
// Substitute any placeholders with information about the current environment
const { assumeRoleArn } = await replaceEnvPlaceholders({
assumeRoleArn: options.assumeRoleArn,
}, resolvedEnvironment, this.sdkProvider);
const stackSdk = await this.cachedSdkForEnvironment(resolvedEnvironment, options.mode, {
assumeRoleArn,
assumeRoleExternalId: options.assumeRoleExternalId,
assumeRoleAdditionalOptions: options.assumeRoleAdditionalOptions,
});
return {
sdk: stackSdk.sdk,
resolvedEnvironment,
resources: this.environmentResources.for(resolvedEnvironment, stackSdk.sdk, { ioHost: this.ioHost, action: this.action }),
// If we asked for a role, did not successfully assume it, and yet got here without an exception: that
// means we must have fallback credentials.
isFallbackCredentials: !stackSdk.didAssumeRole && !!assumeRoleArn,
didAssumeRole: stackSdk.didAssumeRole,
replacePlaceholders: async <A extends string | undefined>(str: A) => {
const ret = await replaceEnvPlaceholders({ str }, resolvedEnvironment, this.sdkProvider);
return ret.str;
},
};
}
private async cachedSdkForEnvironment(
environment: cxapi.Environment,
mode: Mode,
options?: CredentialsOptions,
) {
const cacheKeyElements = [
environment.account,
environment.region,
`${mode}`,
options?.assumeRoleArn ?? '',
options?.assumeRoleExternalId ?? '',
];
if (options?.assumeRoleAdditionalOptions) {
cacheKeyElements.push(JSON.stringify(options.assumeRoleAdditionalOptions));
}
const cacheKey = cacheKeyElements.join(':');
const existing = this.sdkCache.get(cacheKey);
if (existing) {
return existing;
}
const ret = await this.sdkProvider.forEnvironment(environment, mode, options);
this.sdkCache.set(cacheKey, ret);
return ret;
}
}
/**
* SDK obtained by assuming the deploy role
* for a given environment
*/
export interface TargetEnvironment {
/**
* The SDK for the given environment
*/
readonly sdk: SDK;
/**
* The resolved environment for the stack
* (no more 'unknown-account/unknown-region')
*/
readonly resolvedEnvironment: cxapi.Environment;
/**
* Access class for environmental resources to help the deployment
*/
readonly resources: EnvironmentResources;
/**
* Whether or not we assumed a role in the process of getting these credentials
*/
readonly didAssumeRole: boolean;
/**
* Whether or not these are fallback credentials
*
* Fallback credentials means that assuming the intended role failed, but the
* base credentials happen to be for the right account so we just picked those
* and hope the future SDK calls succeed.
*
* This is a backwards compatibility mechanism from around the time we introduced
* deployment roles.
*/
readonly isFallbackCredentials: boolean;
/**
* Replace environment placeholders according to the current environment
*/
replacePlaceholders(x: string | undefined): Promise<StringWithoutPlaceholders | undefined>;
}
interface PrepareSdkRoleOptions {
readonly environment: cxapi.Environment;
readonly mode: Mode;
readonly assumeRoleArn?: string;
readonly assumeRoleExternalId?: string;
readonly assumeRoleAdditionalOptions?: { [key: string]: any };
}