1package session
2
3import (
4	"fmt"
5
6	"github.com/aws/aws-sdk-go/aws/awserr"
7	"github.com/aws/aws-sdk-go/aws/credentials"
8	"github.com/aws/aws-sdk-go/aws/endpoints"
9	"github.com/aws/aws-sdk-go/internal/ini"
10)
11
12const (
13	// Static Credentials group
14	accessKeyIDKey  = `aws_access_key_id`     // group required
15	secretAccessKey = `aws_secret_access_key` // group required
16	sessionTokenKey = `aws_session_token`     // optional
17
18	// Assume Role Credentials group
19	roleArnKey          = `role_arn`          // group required
20	sourceProfileKey    = `source_profile`    // group required (or credential_source)
21	credentialSourceKey = `credential_source` // group required (or source_profile)
22	externalIDKey       = `external_id`       // optional
23	mfaSerialKey        = `mfa_serial`        // optional
24	roleSessionNameKey  = `role_session_name` // optional
25
26	// CSM options
27	csmEnabledKey  = `csm_enabled`
28	csmHostKey     = `csm_host`
29	csmPortKey     = `csm_port`
30	csmClientIDKey = `csm_client_id`
31
32	// Additional Config fields
33	regionKey = `region`
34
35	// endpoint discovery group
36	enableEndpointDiscoveryKey = `endpoint_discovery_enabled` // optional
37
38	// External Credential Process
39	credentialProcessKey = `credential_process` // optional
40
41	// Web Identity Token File
42	webIdentityTokenFileKey = `web_identity_token_file` // optional
43
44	// Additional config fields for regional or legacy endpoints
45	stsRegionalEndpointSharedKey = `sts_regional_endpoints`
46
47	// Additional config fields for regional or legacy endpoints
48	s3UsEast1RegionalSharedKey = `s3_us_east_1_regional_endpoint`
49
50	// DefaultSharedConfigProfile is the default profile to be used when
51	// loading configuration from the config files if another profile name
52	// is not provided.
53	DefaultSharedConfigProfile = `default`
54
55	// S3 ARN Region Usage
56	s3UseARNRegionKey = "s3_use_arn_region"
57)
58
59// sharedConfig represents the configuration fields of the SDK config files.
60type sharedConfig struct {
61	// Credentials values from the config file. Both aws_access_key_id and
62	// aws_secret_access_key must be provided together in the same file to be
63	// considered valid. The values will be ignored if not a complete group.
64	// aws_session_token is an optional field that can be provided if both of
65	// the other two fields are also provided.
66	//
67	//	aws_access_key_id
68	//	aws_secret_access_key
69	//	aws_session_token
70	Creds credentials.Value
71
72	CredentialSource     string
73	CredentialProcess    string
74	WebIdentityTokenFile string
75
76	RoleARN         string
77	RoleSessionName string
78	ExternalID      string
79	MFASerial       string
80
81	SourceProfileName string
82	SourceProfile     *sharedConfig
83
84	// Region is the region the SDK should use for looking up AWS service
85	// endpoints and signing requests.
86	//
87	//	region
88	Region string
89
90	// EnableEndpointDiscovery can be enabled in the shared config by setting
91	// endpoint_discovery_enabled to true
92	//
93	//	endpoint_discovery_enabled = true
94	EnableEndpointDiscovery *bool
95
96	// CSM Options
97	CSMEnabled  *bool
98	CSMHost     string
99	CSMPort     string
100	CSMClientID string
101
102	// Specifies the Regional Endpoint flag for the SDK to resolve the endpoint for a service
103	//
104	// sts_regional_endpoints = regional
105	// This can take value as `LegacySTSEndpoint` or `RegionalSTSEndpoint`
106	STSRegionalEndpoint endpoints.STSRegionalEndpoint
107
108	// Specifies the Regional Endpoint flag for the SDK to resolve the endpoint for a service
109	//
110	// s3_us_east_1_regional_endpoint = regional
111	// This can take value as `LegacyS3UsEast1Endpoint` or `RegionalS3UsEast1Endpoint`
112	S3UsEast1RegionalEndpoint endpoints.S3UsEast1RegionalEndpoint
113
114	// Specifies if the S3 service should allow ARNs to direct the region
115	// the client's requests are sent to.
116	//
117	// s3_use_arn_region=true
118	S3UseARNRegion bool
119}
120
121type sharedConfigFile struct {
122	Filename string
123	IniData  ini.Sections
124}
125
126// loadSharedConfig retrieves the configuration from the list of files using
127// the profile provided. The order the files are listed will determine
128// precedence. Values in subsequent files will overwrite values defined in
129// earlier files.
130//
131// For example, given two files A and B. Both define credentials. If the order
132// of the files are A then B, B's credential values will be used instead of
133// A's.
134//
135// See sharedConfig.setFromFile for information how the config files
136// will be loaded.
137func loadSharedConfig(profile string, filenames []string, exOpts bool) (sharedConfig, error) {
138	if len(profile) == 0 {
139		profile = DefaultSharedConfigProfile
140	}
141
142	files, err := loadSharedConfigIniFiles(filenames)
143	if err != nil {
144		return sharedConfig{}, err
145	}
146
147	cfg := sharedConfig{}
148	profiles := map[string]struct{}{}
149	if err = cfg.setFromIniFiles(profiles, profile, files, exOpts); err != nil {
150		return sharedConfig{}, err
151	}
152
153	return cfg, nil
154}
155
156func loadSharedConfigIniFiles(filenames []string) ([]sharedConfigFile, error) {
157	files := make([]sharedConfigFile, 0, len(filenames))
158
159	for _, filename := range filenames {
160		sections, err := ini.OpenFile(filename)
161		if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ini.ErrCodeUnableToReadFile {
162			// Skip files which can't be opened and read for whatever reason
163			continue
164		} else if err != nil {
165			return nil, SharedConfigLoadError{Filename: filename, Err: err}
166		}
167
168		files = append(files, sharedConfigFile{
169			Filename: filename, IniData: sections,
170		})
171	}
172
173	return files, nil
174}
175
176func (cfg *sharedConfig) setFromIniFiles(profiles map[string]struct{}, profile string, files []sharedConfigFile, exOpts bool) error {
177	// Trim files from the list that don't exist.
178	var skippedFiles int
179	var profileNotFoundErr error
180	for _, f := range files {
181		if err := cfg.setFromIniFile(profile, f, exOpts); err != nil {
182			if _, ok := err.(SharedConfigProfileNotExistsError); ok {
183				// Ignore profiles not defined in individual files.
184				profileNotFoundErr = err
185				skippedFiles++
186				continue
187			}
188			return err
189		}
190	}
191	if skippedFiles == len(files) {
192		// If all files were skipped because the profile is not found, return
193		// the original profile not found error.
194		return profileNotFoundErr
195	}
196
197	if _, ok := profiles[profile]; ok {
198		// if this is the second instance of the profile the Assume Role
199		// options must be cleared because they are only valid for the
200		// first reference of a profile. The self linked instance of the
201		// profile only have credential provider options.
202		cfg.clearAssumeRoleOptions()
203	} else {
204		// First time a profile has been seen, It must either be a assume role
205		// or credentials. Assert if the credential type requires a role ARN,
206		// the ARN is also set.
207		if err := cfg.validateCredentialsRequireARN(profile); err != nil {
208			return err
209		}
210	}
211	profiles[profile] = struct{}{}
212
213	if err := cfg.validateCredentialType(); err != nil {
214		return err
215	}
216
217	// Link source profiles for assume roles
218	if len(cfg.SourceProfileName) != 0 {
219		// Linked profile via source_profile ignore credential provider
220		// options, the source profile must provide the credentials.
221		cfg.clearCredentialOptions()
222
223		srcCfg := &sharedConfig{}
224		err := srcCfg.setFromIniFiles(profiles, cfg.SourceProfileName, files, exOpts)
225		if err != nil {
226			// SourceProfile that doesn't exist is an error in configuration.
227			if _, ok := err.(SharedConfigProfileNotExistsError); ok {
228				err = SharedConfigAssumeRoleError{
229					RoleARN:       cfg.RoleARN,
230					SourceProfile: cfg.SourceProfileName,
231				}
232			}
233			return err
234		}
235
236		if !srcCfg.hasCredentials() {
237			return SharedConfigAssumeRoleError{
238				RoleARN:       cfg.RoleARN,
239				SourceProfile: cfg.SourceProfileName,
240			}
241		}
242
243		cfg.SourceProfile = srcCfg
244	}
245
246	return nil
247}
248
249// setFromFile loads the configuration from the file using the profile
250// provided. A sharedConfig pointer type value is used so that multiple config
251// file loadings can be chained.
252//
253// Only loads complete logically grouped values, and will not set fields in cfg
254// for incomplete grouped values in the config. Such as credentials. For
255// example if a config file only includes aws_access_key_id but no
256// aws_secret_access_key the aws_access_key_id will be ignored.
257func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile, exOpts bool) error {
258	section, ok := file.IniData.GetSection(profile)
259	if !ok {
260		// Fallback to to alternate profile name: profile <name>
261		section, ok = file.IniData.GetSection(fmt.Sprintf("profile %s", profile))
262		if !ok {
263			return SharedConfigProfileNotExistsError{Profile: profile, Err: nil}
264		}
265	}
266
267	if exOpts {
268		// Assume Role Parameters
269		updateString(&cfg.RoleARN, section, roleArnKey)
270		updateString(&cfg.ExternalID, section, externalIDKey)
271		updateString(&cfg.MFASerial, section, mfaSerialKey)
272		updateString(&cfg.RoleSessionName, section, roleSessionNameKey)
273		updateString(&cfg.SourceProfileName, section, sourceProfileKey)
274		updateString(&cfg.CredentialSource, section, credentialSourceKey)
275		updateString(&cfg.Region, section, regionKey)
276
277		if v := section.String(stsRegionalEndpointSharedKey); len(v) != 0 {
278			sre, err := endpoints.GetSTSRegionalEndpoint(v)
279			if err != nil {
280				return fmt.Errorf("failed to load %s from shared config, %s, %v",
281					stsRegionalEndpointSharedKey, file.Filename, err)
282			}
283			cfg.STSRegionalEndpoint = sre
284		}
285
286		if v := section.String(s3UsEast1RegionalSharedKey); len(v) != 0 {
287			sre, err := endpoints.GetS3UsEast1RegionalEndpoint(v)
288			if err != nil {
289				return fmt.Errorf("failed to load %s from shared config, %s, %v",
290					s3UsEast1RegionalSharedKey, file.Filename, err)
291			}
292			cfg.S3UsEast1RegionalEndpoint = sre
293		}
294	}
295
296	updateString(&cfg.CredentialProcess, section, credentialProcessKey)
297	updateString(&cfg.WebIdentityTokenFile, section, webIdentityTokenFileKey)
298
299	// Shared Credentials
300	creds := credentials.Value{
301		AccessKeyID:     section.String(accessKeyIDKey),
302		SecretAccessKey: section.String(secretAccessKey),
303		SessionToken:    section.String(sessionTokenKey),
304		ProviderName:    fmt.Sprintf("SharedConfigCredentials: %s", file.Filename),
305	}
306	if creds.HasKeys() {
307		cfg.Creds = creds
308	}
309
310	// Endpoint discovery
311	updateBoolPtr(&cfg.EnableEndpointDiscovery, section, enableEndpointDiscoveryKey)
312
313	// CSM options
314	updateBoolPtr(&cfg.CSMEnabled, section, csmEnabledKey)
315	updateString(&cfg.CSMHost, section, csmHostKey)
316	updateString(&cfg.CSMPort, section, csmPortKey)
317	updateString(&cfg.CSMClientID, section, csmClientIDKey)
318
319	updateBool(&cfg.S3UseARNRegion, section, s3UseARNRegionKey)
320
321	return nil
322}
323
324func (cfg *sharedConfig) validateCredentialsRequireARN(profile string) error {
325	var credSource string
326
327	switch {
328	case len(cfg.SourceProfileName) != 0:
329		credSource = sourceProfileKey
330	case len(cfg.CredentialSource) != 0:
331		credSource = credentialSourceKey
332	case len(cfg.WebIdentityTokenFile) != 0:
333		credSource = webIdentityTokenFileKey
334	}
335
336	if len(credSource) != 0 && len(cfg.RoleARN) == 0 {
337		return CredentialRequiresARNError{
338			Type:    credSource,
339			Profile: profile,
340		}
341	}
342
343	return nil
344}
345
346func (cfg *sharedConfig) validateCredentialType() error {
347	// Only one or no credential type can be defined.
348	if !oneOrNone(
349		len(cfg.SourceProfileName) != 0,
350		len(cfg.CredentialSource) != 0,
351		len(cfg.CredentialProcess) != 0,
352		len(cfg.WebIdentityTokenFile) != 0,
353	) {
354		return ErrSharedConfigSourceCollision
355	}
356
357	return nil
358}
359
360func (cfg *sharedConfig) hasCredentials() bool {
361	switch {
362	case len(cfg.SourceProfileName) != 0:
363	case len(cfg.CredentialSource) != 0:
364	case len(cfg.CredentialProcess) != 0:
365	case len(cfg.WebIdentityTokenFile) != 0:
366	case cfg.Creds.HasKeys():
367	default:
368		return false
369	}
370
371	return true
372}
373
374func (cfg *sharedConfig) clearCredentialOptions() {
375	cfg.CredentialSource = ""
376	cfg.CredentialProcess = ""
377	cfg.WebIdentityTokenFile = ""
378	cfg.Creds = credentials.Value{}
379}
380
381func (cfg *sharedConfig) clearAssumeRoleOptions() {
382	cfg.RoleARN = ""
383	cfg.ExternalID = ""
384	cfg.MFASerial = ""
385	cfg.RoleSessionName = ""
386	cfg.SourceProfileName = ""
387}
388
389func oneOrNone(bs ...bool) bool {
390	var count int
391
392	for _, b := range bs {
393		if b {
394			count++
395			if count > 1 {
396				return false
397			}
398		}
399	}
400
401	return true
402}
403
404// updateString will only update the dst with the value in the section key, key
405// is present in the section.
406func updateString(dst *string, section ini.Section, key string) {
407	if !section.Has(key) {
408		return
409	}
410	*dst = section.String(key)
411}
412
413// updateBool will only update the dst with the value in the section key, key
414// is present in the section.
415func updateBool(dst *bool, section ini.Section, key string) {
416	if !section.Has(key) {
417		return
418	}
419	*dst = section.Bool(key)
420}
421
422// updateBoolPtr will only update the dst with the value in the section key,
423// key is present in the section.
424func updateBoolPtr(dst **bool, section ini.Section, key string) {
425	if !section.Has(key) {
426		return
427	}
428	*dst = new(bool)
429	**dst = section.Bool(key)
430}
431
432// SharedConfigLoadError is an error for the shared config file failed to load.
433type SharedConfigLoadError struct {
434	Filename string
435	Err      error
436}
437
438// Code is the short id of the error.
439func (e SharedConfigLoadError) Code() string {
440	return "SharedConfigLoadError"
441}
442
443// Message is the description of the error
444func (e SharedConfigLoadError) Message() string {
445	return fmt.Sprintf("failed to load config file, %s", e.Filename)
446}
447
448// OrigErr is the underlying error that caused the failure.
449func (e SharedConfigLoadError) OrigErr() error {
450	return e.Err
451}
452
453// Error satisfies the error interface.
454func (e SharedConfigLoadError) Error() string {
455	return awserr.SprintError(e.Code(), e.Message(), "", e.Err)
456}
457
458// SharedConfigProfileNotExistsError is an error for the shared config when
459// the profile was not find in the config file.
460type SharedConfigProfileNotExistsError struct {
461	Profile string
462	Err     error
463}
464
465// Code is the short id of the error.
466func (e SharedConfigProfileNotExistsError) Code() string {
467	return "SharedConfigProfileNotExistsError"
468}
469
470// Message is the description of the error
471func (e SharedConfigProfileNotExistsError) Message() string {
472	return fmt.Sprintf("failed to get profile, %s", e.Profile)
473}
474
475// OrigErr is the underlying error that caused the failure.
476func (e SharedConfigProfileNotExistsError) OrigErr() error {
477	return e.Err
478}
479
480// Error satisfies the error interface.
481func (e SharedConfigProfileNotExistsError) Error() string {
482	return awserr.SprintError(e.Code(), e.Message(), "", e.Err)
483}
484
485// SharedConfigAssumeRoleError is an error for the shared config when the
486// profile contains assume role information, but that information is invalid
487// or not complete.
488type SharedConfigAssumeRoleError struct {
489	RoleARN       string
490	SourceProfile string
491}
492
493// Code is the short id of the error.
494func (e SharedConfigAssumeRoleError) Code() string {
495	return "SharedConfigAssumeRoleError"
496}
497
498// Message is the description of the error
499func (e SharedConfigAssumeRoleError) Message() string {
500	return fmt.Sprintf(
501		"failed to load assume role for %s, source profile %s has no shared credentials",
502		e.RoleARN, e.SourceProfile,
503	)
504}
505
506// OrigErr is the underlying error that caused the failure.
507func (e SharedConfigAssumeRoleError) OrigErr() error {
508	return nil
509}
510
511// Error satisfies the error interface.
512func (e SharedConfigAssumeRoleError) Error() string {
513	return awserr.SprintError(e.Code(), e.Message(), "", nil)
514}
515
516// CredentialRequiresARNError provides the error for shared config credentials
517// that are incorrectly configured in the shared config or credentials file.
518type CredentialRequiresARNError struct {
519	// type of credentials that were configured.
520	Type string
521
522	// Profile name the credentials were in.
523	Profile string
524}
525
526// Code is the short id of the error.
527func (e CredentialRequiresARNError) Code() string {
528	return "CredentialRequiresARNError"
529}
530
531// Message is the description of the error
532func (e CredentialRequiresARNError) Message() string {
533	return fmt.Sprintf(
534		"credential type %s requires role_arn, profile %s",
535		e.Type, e.Profile,
536	)
537}
538
539// OrigErr is the underlying error that caused the failure.
540func (e CredentialRequiresARNError) OrigErr() error {
541	return nil
542}
543
544// Error satisfies the error interface.
545func (e CredentialRequiresARNError) Error() string {
546	return awserr.SprintError(e.Code(), e.Message(), "", nil)
547}
548