Skip to content

Commit 84cbcb6

Browse files
janariodiehlaws
authored andcommitted
aws/session: Add support for chaining assume IAM role from shared config (aws#2579)
Adds support chaining assume role credentials from the shared config/credentials files. This change allows you to create an assume role chain of multiple levels of assumed IAM roles. The config profile the deepest in the chain must use static credentials, or `credential_source`. If the deepest profile doesn't have either of these the session will fail to load. Fixes the SDK's shared config credential source not assuming a role with environment and ECS credentials. EC2 credentials were already supported. Fix aws#2528 Fix aws#2385 Also adds the ability to specify the Handlers the SDK should use at the SessionWithOptions. This allows the a set of handlers to be provided at the very beginning of the session credential chain.
1 parent 6749451 commit 84cbcb6

12 files changed

+851
-619
lines changed

aws/credentials/credentials.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ package credentials
5050

5151
import (
5252
"fmt"
53-
"github.com/aws/aws-sdk-go/aws/awserr"
5453
"sync"
5554
"time"
55+
56+
"github.com/aws/aws-sdk-go/aws/awserr"
5657
)
5758

5859
// AnonymousCredentials is an empty Credential object that can be used as

aws/credentials/stscreds/assume_role_provider.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ type AssumeRoleProvider struct {
200200
// by a random percentage between 0 and MaxJitterFraction. MaxJitterFrac must
201201
// have a value between 0 and 1. Any other value may lead to expected behavior.
202202
// With a MaxJitterFrac value of 0, default) will no jitter will be used.
203-
//
203+
//
204204
// For example, with a Duration of 30m and a MaxJitterFrac of 0.1, the
205205
// AssumeRole call will be made with an arbitrary Duration between 27m and
206206
// 30m.
@@ -258,7 +258,6 @@ func NewCredentialsWithClient(svc AssumeRoler, roleARN string, options ...func(*
258258

259259
// Retrieve generates a new set of temporary credentials using STS.
260260
func (p *AssumeRoleProvider) Retrieve() (credentials.Value, error) {
261-
262261
// Apply defaults where parameters are not set.
263262
if p.RoleSessionName == "" {
264263
// Try to work out a role name that will hopefully end up unique.

aws/request/handlers.go

+45
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,51 @@ func (h *Handlers) Clear() {
5959
h.Complete.Clear()
6060
}
6161

62+
// IsEmpty returns if there are no handlers in any of the handlerlists.
63+
func (h *Handlers) IsEmpty() bool {
64+
if h.Validate.Len() != 0 {
65+
return false
66+
}
67+
if h.Build.Len() != 0 {
68+
return false
69+
}
70+
if h.Send.Len() != 0 {
71+
return false
72+
}
73+
if h.Sign.Len() != 0 {
74+
return false
75+
}
76+
if h.Unmarshal.Len() != 0 {
77+
return false
78+
}
79+
if h.UnmarshalStream.Len() != 0 {
80+
return false
81+
}
82+
if h.UnmarshalMeta.Len() != 0 {
83+
return false
84+
}
85+
if h.UnmarshalError.Len() != 0 {
86+
return false
87+
}
88+
if h.ValidateResponse.Len() != 0 {
89+
return false
90+
}
91+
if h.Retry.Len() != 0 {
92+
return false
93+
}
94+
if h.AfterRetry.Len() != 0 {
95+
return false
96+
}
97+
if h.CompleteAttempt.Len() != 0 {
98+
return false
99+
}
100+
if h.Complete.Len() != 0 {
101+
return false
102+
}
103+
104+
return true
105+
}
106+
62107
// A HandlerListRunItem represents an entry in the HandlerList which
63108
// is being run.
64109
type HandlerListRunItem struct {

aws/session/credentials.go

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package session
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/aws/aws-sdk-go/aws"
8+
"github.com/aws/aws-sdk-go/aws/awserr"
9+
"github.com/aws/aws-sdk-go/aws/credentials"
10+
"github.com/aws/aws-sdk-go/aws/credentials/processcreds"
11+
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
12+
"github.com/aws/aws-sdk-go/aws/defaults"
13+
"github.com/aws/aws-sdk-go/aws/request"
14+
"github.com/aws/aws-sdk-go/internal/shareddefaults"
15+
)
16+
17+
// valid credential source values
18+
const (
19+
credSourceEc2Metadata = "Ec2InstanceMetadata"
20+
credSourceEnvironment = "Environment"
21+
credSourceECSContainer = "EcsContainer"
22+
)
23+
24+
func resolveCredentials(cfg *aws.Config,
25+
envCfg envConfig, sharedCfg sharedConfig,
26+
handlers request.Handlers,
27+
sessOpts Options,
28+
) (*credentials.Credentials, error) {
29+
// Credentials from Assume Role with specific credentials source.
30+
if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.CredentialSource) > 0 {
31+
return resolveCredsFromSource(cfg, envCfg, sharedCfg, handlers, sessOpts)
32+
}
33+
34+
// Credentials from environment variables
35+
if len(envCfg.Creds.AccessKeyID) > 0 {
36+
return credentials.NewStaticCredentialsFromCreds(envCfg.Creds), nil
37+
}
38+
39+
// Fallback to the "default" credential resolution chain.
40+
return resolveCredsFromProfile(cfg, envCfg, sharedCfg, handlers, sessOpts)
41+
}
42+
43+
func resolveCredsFromProfile(cfg *aws.Config,
44+
envCfg envConfig, sharedCfg sharedConfig,
45+
handlers request.Handlers,
46+
sessOpts Options,
47+
) (*credentials.Credentials, error) {
48+
49+
if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.RoleARN) > 0 && sharedCfg.AssumeRoleSource != nil {
50+
// Assume IAM role with credentials source from a different profile.
51+
cred, err := resolveCredsFromProfile(cfg, envCfg, *sharedCfg.AssumeRoleSource, handlers, sessOpts)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
cfgCp := *cfg
57+
cfgCp.Credentials = cred
58+
return credsFromAssumeRole(cfgCp, handlers, sharedCfg, sessOpts)
59+
60+
} else if len(sharedCfg.Creds.AccessKeyID) > 0 {
61+
// Static Credentials from Shared Config/Credentials file.
62+
return credentials.NewStaticCredentialsFromCreds(
63+
sharedCfg.Creds,
64+
), nil
65+
66+
} else if len(sharedCfg.CredentialProcess) > 0 {
67+
// Credential Process credentials from Shared Config/Credentials file.
68+
return processcreds.NewCredentials(
69+
sharedCfg.CredentialProcess,
70+
), nil
71+
72+
} else if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.CredentialSource) > 0 {
73+
// Assume IAM Role with specific credential source.
74+
return resolveCredsFromSource(cfg, envCfg, sharedCfg, handlers, sessOpts)
75+
}
76+
77+
// Fallback to default credentials provider, include mock errors
78+
// for the credential chain so user can identify why credentials
79+
// failed to be retrieved.
80+
return credentials.NewCredentials(&credentials.ChainProvider{
81+
VerboseErrors: aws.BoolValue(cfg.CredentialsChainVerboseErrors),
82+
Providers: []credentials.Provider{
83+
&credProviderError{
84+
Err: awserr.New("EnvAccessKeyNotFound",
85+
"failed to find credentials in the environment.", nil),
86+
},
87+
&credProviderError{
88+
Err: awserr.New("SharedCredsLoad",
89+
fmt.Sprintf("failed to load profile, %s.", envCfg.Profile), nil),
90+
},
91+
defaults.RemoteCredProvider(*cfg, handlers),
92+
},
93+
}), nil
94+
}
95+
96+
func resolveCredsFromSource(cfg *aws.Config,
97+
envCfg envConfig, sharedCfg sharedConfig,
98+
handlers request.Handlers,
99+
sessOpts Options,
100+
) (*credentials.Credentials, error) {
101+
// if both credential_source and source_profile have been set, return an
102+
// error as this is undefined behavior. Only one can be used at a time
103+
// within a profile.
104+
if len(sharedCfg.AssumeRole.SourceProfile) > 0 {
105+
return nil, ErrSharedConfigSourceCollision
106+
}
107+
108+
cfgCp := *cfg
109+
switch sharedCfg.AssumeRole.CredentialSource {
110+
case credSourceEc2Metadata:
111+
p := defaults.RemoteCredProvider(cfgCp, handlers)
112+
cfgCp.Credentials = credentials.NewCredentials(p)
113+
114+
case credSourceEnvironment:
115+
cfgCp.Credentials = credentials.NewStaticCredentialsFromCreds(envCfg.Creds)
116+
117+
case credSourceECSContainer:
118+
if len(os.Getenv(shareddefaults.ECSCredsProviderEnvVar)) == 0 {
119+
return nil, ErrSharedConfigECSContainerEnvVarEmpty
120+
}
121+
122+
p := defaults.RemoteCredProvider(cfgCp, handlers)
123+
cfgCp.Credentials = credentials.NewCredentials(p)
124+
125+
default:
126+
return nil, ErrSharedConfigInvalidCredSource
127+
}
128+
129+
return credsFromAssumeRole(cfgCp, handlers, sharedCfg, sessOpts)
130+
}
131+
132+
func credsFromAssumeRole(cfg aws.Config,
133+
handlers request.Handlers,
134+
sharedCfg sharedConfig,
135+
sessOpts Options,
136+
) (*credentials.Credentials, error) {
137+
if len(sharedCfg.AssumeRole.MFASerial) > 0 && sessOpts.AssumeRoleTokenProvider == nil {
138+
// AssumeRole Token provider is required if doing Assume Role
139+
// with MFA.
140+
return nil, AssumeRoleTokenProviderNotSetError{}
141+
}
142+
143+
return stscreds.NewCredentials(
144+
&Session{
145+
Config: &cfg,
146+
Handlers: handlers.Copy(),
147+
},
148+
sharedCfg.AssumeRole.RoleARN,
149+
func(opt *stscreds.AssumeRoleProvider) {
150+
opt.RoleSessionName = sharedCfg.AssumeRole.RoleSessionName
151+
152+
// Assume role with external ID
153+
if len(sharedCfg.AssumeRole.ExternalID) > 0 {
154+
opt.ExternalID = aws.String(sharedCfg.AssumeRole.ExternalID)
155+
}
156+
157+
// Assume role with MFA
158+
if len(sharedCfg.AssumeRole.MFASerial) > 0 {
159+
opt.SerialNumber = aws.String(sharedCfg.AssumeRole.MFASerial)
160+
opt.TokenProvider = sessOpts.AssumeRoleTokenProvider
161+
}
162+
},
163+
), nil
164+
}
165+
166+
// AssumeRoleTokenProviderNotSetError is an error returned when creating a session when the
167+
// MFAToken option is not set when shared config is configured load assume a
168+
// role with an MFA token.
169+
type AssumeRoleTokenProviderNotSetError struct{}
170+
171+
// Code is the short id of the error.
172+
func (e AssumeRoleTokenProviderNotSetError) Code() string {
173+
return "AssumeRoleTokenProviderNotSetError"
174+
}
175+
176+
// Message is the description of the error
177+
func (e AssumeRoleTokenProviderNotSetError) Message() string {
178+
return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.")
179+
}
180+
181+
// OrigErr is the underlying error that caused the failure.
182+
func (e AssumeRoleTokenProviderNotSetError) OrigErr() error {
183+
return nil
184+
}
185+
186+
// Error satisfies the error interface.
187+
func (e AssumeRoleTokenProviderNotSetError) Error() string {
188+
return awserr.SprintError(e.Code(), e.Message(), "", nil)
189+
}
190+
191+
type credProviderError struct {
192+
Err error
193+
}
194+
195+
var emptyCreds = credentials.Value{}
196+
197+
func (c credProviderError) Retrieve() (credentials.Value, error) {
198+
return credentials.Value{}, c.Err
199+
}
200+
func (c credProviderError) IsExpired() bool {
201+
return true
202+
}

0 commit comments

Comments
 (0)