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