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