-
Notifications
You must be signed in to change notification settings - Fork 90
/
assumer_aws_iam.go
346 lines (294 loc) · 11.3 KB
/
assumer_aws_iam.go
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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package cfaws
import (
"context"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/aws-sdk-go-v2/service/sts/types"
"github.com/common-fate/clio"
"github.com/common-fate/granted/pkg/securestorage"
"gopkg.in/ini.v1"
)
// Implements Assumer
type AwsIamAssumer struct {
}
// Default behaviour is to use the sdk to retrieve the credentials from the file
// For launching the console there is an extra step GetFederationToken that happens after this to get a session token
func (aia *AwsIamAssumer) AssumeTerminal(ctx context.Context, c *Profile, configOpts ConfigOpts) (aws.Credentials, error) {
// check if the valid credentials are available in session credential store
sessionCredStorage := securestorage.NewSecureSessionCredentialStorage()
cachedCreds, err := sessionCredStorage.GetCredentials(c.AWSConfig.Profile)
if err != nil {
clio.Debugw("error loading cached credentials", "error", err)
} else if cachedCreds != nil && !cachedCreds.Expired() {
clio.Debugw("credentials found in cache", "expires", cachedCreds.Expires.String(), "canExpire", cachedCreds.CanExpire, "timeNow", time.Now().String())
return *cachedCreds, err
}
clio.Debugw("refreshing credentials", "reason", "not found")
if c.HasSecureStorageIAMCredentials {
secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()
creds, err := secureIAMCredentialStorage.GetCredentials(c.Name)
if err != nil {
return aws.Credentials{}, err
}
/**If the IAM credentials in secure storage are valid and no MFA is required:
*[profile example]
*region = us-west-2
*credential_process = dgranted credential-process --profile=example
**/
if c.AWSConfig.MFASerial == "" {
return creds, nil
}
cfg, err := config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken)))
if err != nil {
return aws.Credentials{}, err
}
/**If the IAM credentials in secure storage MFA is required:
*[profile example]
*region = us-west-2
*mfa_serial = arn:aws:iam::616777145260:mfa
*credential_process = dgranted credential-process --profile=example
**/
clio.Debugw("generating temporary credentials", "assumer", aia.Type(), "profile_type", "base_profile_with_mfa")
creds, err = aia.getTemporaryCreds(ctx, cfg, c, configOpts)
if err != nil {
return aws.Credentials{}, err
}
if err := sessionCredStorage.StoreCredentials(c.AWSConfig.Profile, creds); err != nil {
clio.Warnf("Error caching credentials, MFA token will be requested before current token is expired")
}
return creds, nil
}
//using ~/.aws/credentials file for creds
opts := []func(*config.LoadOptions) error{
// load the config profile
config.WithSharedConfigProfile(c.Name),
}
var credentials aws.Credentials
// if the aws profile contains 'role_arn' then having this option will return the temporary credentials
if c.AWSConfig.RoleARN != "" {
clio.Debugw("generating temporary credentials", "assumer", aia.Type(), "profile_type", "with_role_arn")
opts = append(opts, config.WithAssumeRoleCredentialOptions(func(aro *stscreds.AssumeRoleOptions) {
// check if the MFAToken code is provided as argument
// if provided then use it instead of prompting for MFAToken code.
if configOpts.MFATokenCode != "" {
aro.TokenProvider = func() (string, error) {
return configOpts.MFATokenCode, nil
}
} else {
// set the token provider up
aro.TokenProvider = MfaTokenProvider
}
aro.Duration = configOpts.Duration
/**If the mfa_serial is defined on the root profile, we need to set it in this config so that the aws SDK knows to prompt for MFA token:
*[profile base]
*region = us-west-2
*mfa_serial = arn:aws:iam::616777145260:mfa
*[profile prod]
*role_arn = XXXXXXX
*source_profile = base
**/
if len(c.Parents) > 0 {
if c.Parents[0].AWSConfig.MFASerial != "" {
aro.SerialNumber = aws.String(c.Parents[0].AWSConfig.MFASerial)
}
} else {
if c.AWSConfig.MFASerial != "" {
aro.SerialNumber = aws.String(c.AWSConfig.MFASerial)
}
}
if c.AWSConfig.RoleSessionName != "" {
aro.RoleSessionName = c.AWSConfig.RoleSessionName
} else {
aro.RoleSessionName = sessionName()
}
}))
cfg, err := config.LoadDefaultConfig(ctx, opts...)
if err != nil {
return aws.Credentials{}, err
}
credentials, err = aws.NewCredentialsCache(cfg.Credentials).Retrieve(ctx)
if err != nil {
return aws.Credentials{}, err
}
} else {
// load the creds from the credentials file
cfg, err := config.LoadDefaultConfig(ctx, opts...)
if err != nil {
return aws.Credentials{}, err
}
/**
* Retrieve STS credentials when a base profile uses MFA
*
* ~/.aws/config
* [profile prod]
* region = ***
* mfa_serial = ***
*
* ~/.aws/credentials
* [profile prod]
* aws_access_key_id = ***
* aws_secret_access_key = ***
**/
if c.AWSConfig.MFASerial != "" {
clio.Debugw("generating temporary credentials", "assumer", aia.Type(), "profile_type", "base_profile_with_mfa")
credentials, err = aia.getTemporaryCreds(ctx, cfg, c, configOpts)
if err != nil {
return aws.Credentials{}, err
}
} else {
// else for normal shared credentails, retrieve the long living credentials
clio.Debugw("generating long-lived credentials", "assumer", aia.Type(), "profile_type", "credentials")
credentials, err = aws.NewCredentialsCache(cfg.Credentials).Retrieve(ctx)
if err != nil {
return aws.Credentials{}, err
}
}
}
if err := sessionCredStorage.StoreCredentials(c.AWSConfig.Profile, credentials); err != nil {
clio.Warnf("Error caching credentials, MFA token will be requested before current token is expired")
}
// inform the user about using the secure storage to securely store IAM user credentials
// if it has no parents and it reached this point, it must have had plain text credentials
// if it has parents, and the root is not a secure storage iam profile, then it has plain text credentials
if len(c.Parents) == 0 || !c.Parents[0].HasSecureStorageIAMCredentials {
clio.Warnf("Profile %s has plaintext credentials stored in the AWS credentials file", c.Name)
clio.Infof("To move the credentials to secure storage, run 'granted credentials import %s'", c.Name)
}
return credentials, nil
}
// if required will get a FederationToken to be used to launch the console
// This is required if the iam profile does not assume a role using sts.AssumeRole
func (aia *AwsIamAssumer) AssumeConsole(ctx context.Context, c *Profile, configOpts ConfigOpts) (aws.Credentials, error) {
if c.AWSConfig.Credentials.SessionToken != "" {
clio.Debug("found existing session token in credentials for IAM profile, using this to launch the console")
return c.AWSConfig.Credentials, nil
} else if c.AWSConfig.RoleARN == "" {
return getFederationToken(ctx, c)
} else {
// profile assume a role
return aia.AssumeTerminal(ctx, c, configOpts)
}
}
// A unique key which identifies this assumer e.g AWS-SSO or GOOGLE-AWS-AUTH
func (aia *AwsIamAssumer) Type() string {
return "AWS_IAM"
}
// Matches the profile type on whether it is not an sso profile.
// this will also match other types that are not sso profiles so it should be the last option checked when determining the profile type
func (aia *AwsIamAssumer) ProfileMatchesType(rawProfile *ini.Section, parsedProfile config.SharedConfig) bool {
return parsedProfile.SSOAccountID == ""
}
var allowAllPolicy = `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAll",
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}`
// GetFederationToken is used when launching a console session with long-lived IAM credentials profiles
// GetFederation token uses an allow all IAM policy so that the console session will be able to access everything
// If this is not provided, the session cannot do anything in the console
func getFederationToken(ctx context.Context, c *Profile) (aws.Credentials, error) {
opts := []func(*config.LoadOptions) error{
// load the config profile
config.WithSharedConfigProfile(c.Name),
}
// load the creds from the credentials file
cfg, err := config.LoadDefaultConfig(ctx, opts...)
if err != nil {
return aws.Credentials{}, err
}
client := sts.NewFromConfig(cfg)
caller, err := client.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
if err != nil {
return aws.Credentials{}, err
}
tags, userName := getSessionTags(caller)
// name is truncated to ensure it meets the maximum length requirements for the AWS api
out, err := client.GetFederationToken(ctx, &sts.GetFederationTokenInput{Name: aws.String(truncateString(userName, 32)), Policy: aws.String(allowAllPolicy),
// tags are added to the federation token
Tags: tags,
})
if err != nil {
return aws.Credentials{}, err
}
return TypeCredsToAwsCreds(*out.Credentials), err
}
// getTemporaryCreds will call STS to obtain temporary credentials. Will prompt for MFA code.
func (aia *AwsIamAssumer) getTemporaryCreds(ctx context.Context, cfg aws.Config, c *Profile, configOpts ConfigOpts) (aws.Credentials, error) {
stsClient := sts.New(sts.Options{
Credentials: cfg.Credentials,
Region: c.AWSConfig.Region,
})
mfaCode := configOpts.MFATokenCode
if mfaCode == "" {
code, err := MfaTokenProvider()
if err != nil {
return aws.Credentials{}, err
}
mfaCode = code
}
sessionTokenOutput, err := stsClient.GetSessionToken(ctx, &sts.GetSessionTokenInput{
SerialNumber: aws.String(c.AWSConfig.MFASerial),
TokenCode: aws.String(mfaCode),
})
if err != nil {
return aws.Credentials{}, err
}
newCredentials := aws.Credentials{
AccessKeyID: aws.ToString(sessionTokenOutput.Credentials.AccessKeyId),
SecretAccessKey: aws.ToString(sessionTokenOutput.Credentials.SecretAccessKey),
SessionToken: aws.ToString(sessionTokenOutput.Credentials.SessionToken),
CanExpire: true,
Expires: aws.ToTime(sessionTokenOutput.Credentials.Expiration),
Source: aia.Type(),
}
return newCredentials, nil
}
func truncateString(s string, length int) string {
if len(s) <= length {
return s
}
return s[:length]
}
func getSessionTags(caller *sts.GetCallerIdentityOutput) (tags []types.Tag, userName string) {
if caller == nil {
return
}
tags = []types.Tag{
{Key: aws.String("userID"), Value: caller.UserId},
{Key: aws.String("account"), Value: caller.Account},
{Key: aws.String("principalArn"), Value: caller.Arn},
}
if caller.UserId != nil {
userName = *caller.UserId
}
callerArn, err := arn.Parse(*caller.Arn)
if err != nil {
clio.Debugw("could not parse caller arn", "error", err)
return
}
// for an iam credential, the caller ARN.Resource will be user/<username>
// the idea here is to use the username portion as the federation token id
parts := strings.Split(callerArn.Resource, "/")
if len(parts) < 2 {
clio.Debugw("could not split caller resource", "resource", callerArn.Resource)
return
}
userName = parts[1]
tags = append(tags, types.Tag{
Key: aws.String("userName"),
Value: aws.String(userName),
})
return
}