123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- package config
- import (
- "context"
- "fmt"
- "net/url"
- "time"
- "github.com/aws/aws-sdk-go-v2/aws"
- "github.com/aws/aws-sdk-go-v2/credentials"
- "github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
- "github.com/aws/aws-sdk-go-v2/credentials/endpointcreds"
- "github.com/aws/aws-sdk-go-v2/credentials/processcreds"
- "github.com/aws/aws-sdk-go-v2/credentials/ssocreds"
- "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
- "github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
- "github.com/aws/aws-sdk-go-v2/service/sso"
- "github.com/aws/aws-sdk-go-v2/service/ssooidc"
- "github.com/aws/aws-sdk-go-v2/service/sts"
- )
- const (
- // valid credential source values
- credSourceEc2Metadata = "Ec2InstanceMetadata"
- credSourceEnvironment = "Environment"
- credSourceECSContainer = "EcsContainer"
- )
- var (
- ecsContainerEndpoint = "http://169.254.170.2" // not constant to allow for swapping during unit-testing
- )
- // resolveCredentials extracts a credential provider from slice of config
- // sources.
- //
- // If an explicit credential provider is not found the resolver will fallback
- // to resolving credentials by extracting a credential provider from EnvConfig
- // and SharedConfig.
- func resolveCredentials(ctx context.Context, cfg *aws.Config, configs configs) error {
- found, err := resolveCredentialProvider(ctx, cfg, configs)
- if found || err != nil {
- return err
- }
- return resolveCredentialChain(ctx, cfg, configs)
- }
- // resolveCredentialProvider extracts the first instance of Credentials from the
- // config slices.
- //
- // The resolved CredentialProvider will be wrapped in a cache to ensure the
- // credentials are only refreshed when needed. This also protects the
- // credential provider to be used concurrently.
- //
- // Config providers used:
- // * credentialsProviderProvider
- func resolveCredentialProvider(ctx context.Context, cfg *aws.Config, configs configs) (bool, error) {
- credProvider, found, err := getCredentialsProvider(ctx, configs)
- if !found || err != nil {
- return false, err
- }
- cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, credProvider)
- if err != nil {
- return false, err
- }
- return true, nil
- }
- // resolveCredentialChain resolves a credential provider chain using EnvConfig
- // and SharedConfig if present in the slice of provided configs.
- //
- // The resolved CredentialProvider will be wrapped in a cache to ensure the
- // credentials are only refreshed when needed. This also protects the
- // credential provider to be used concurrently.
- func resolveCredentialChain(ctx context.Context, cfg *aws.Config, configs configs) (err error) {
- envConfig, sharedConfig, other := getAWSConfigSources(configs)
- // When checking if a profile was specified programmatically we should only consider the "other"
- // configuration sources that have been provided. This ensures we correctly honor the expected credential
- // hierarchy.
- _, sharedProfileSet, err := getSharedConfigProfile(ctx, other)
- if err != nil {
- return err
- }
- switch {
- case sharedProfileSet:
- err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other)
- case envConfig.Credentials.HasKeys():
- cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials}
- case len(envConfig.WebIdentityTokenFilePath) > 0:
- err = assumeWebIdentity(ctx, cfg, envConfig.WebIdentityTokenFilePath, envConfig.RoleARN, envConfig.RoleSessionName, configs)
- default:
- err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other)
- }
- if err != nil {
- return err
- }
- // Wrap the resolved provider in a cache so the SDK will cache credentials.
- cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, cfg.Credentials)
- if err != nil {
- return err
- }
- return nil
- }
- func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedConfig *SharedConfig, configs configs) (err error) {
- switch {
- case sharedConfig.Source != nil:
- // Assume IAM role with credentials source from a different profile.
- err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig.Source, configs)
- case sharedConfig.Credentials.HasKeys():
- // Static Credentials from Shared Config/Credentials file.
- cfg.Credentials = credentials.StaticCredentialsProvider{
- Value: sharedConfig.Credentials,
- }
- case len(sharedConfig.CredentialSource) != 0:
- err = resolveCredsFromSource(ctx, cfg, envConfig, sharedConfig, configs)
- case len(sharedConfig.WebIdentityTokenFile) != 0:
- // Credentials from Assume Web Identity token require an IAM Role, and
- // that roll will be assumed. May be wrapped with another assume role
- // via SourceProfile.
- return assumeWebIdentity(ctx, cfg, sharedConfig.WebIdentityTokenFile, sharedConfig.RoleARN, sharedConfig.RoleSessionName, configs)
- case sharedConfig.hasSSOConfiguration():
- err = resolveSSOCredentials(ctx, cfg, sharedConfig, configs)
- case len(sharedConfig.CredentialProcess) != 0:
- // Get credentials from CredentialProcess
- err = processCredentials(ctx, cfg, sharedConfig, configs)
- case len(envConfig.ContainerCredentialsEndpoint) != 0:
- err = resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs)
- case len(envConfig.ContainerCredentialsRelativePath) != 0:
- err = resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs)
- default:
- err = resolveEC2RoleCredentials(ctx, cfg, configs)
- }
- if err != nil {
- return err
- }
- if len(sharedConfig.RoleARN) > 0 {
- return credsFromAssumeRole(ctx, cfg, sharedConfig, configs)
- }
- return nil
- }
- func resolveSSOCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error {
- if err := sharedConfig.validateSSOConfiguration(); err != nil {
- return err
- }
- var options []func(*ssocreds.Options)
- v, found, err := getSSOProviderOptions(ctx, configs)
- if err != nil {
- return err
- }
- if found {
- options = append(options, v)
- }
- cfgCopy := cfg.Copy()
- if sharedConfig.SSOSession != nil {
- ssoTokenProviderOptionsFn, found, err := getSSOTokenProviderOptions(ctx, configs)
- if err != nil {
- return fmt.Errorf("failed to get SSOTokenProviderOptions from config sources, %w", err)
- }
- var optFns []func(*ssocreds.SSOTokenProviderOptions)
- if found {
- optFns = append(optFns, ssoTokenProviderOptionsFn)
- }
- cfgCopy.Region = sharedConfig.SSOSession.SSORegion
- cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedConfig.SSOSession.Name)
- if err != nil {
- return err
- }
- oidcClient := ssooidc.NewFromConfig(cfgCopy)
- tokenProvider := ssocreds.NewSSOTokenProvider(oidcClient, cachedPath, optFns...)
- options = append(options, func(o *ssocreds.Options) {
- o.SSOTokenProvider = tokenProvider
- o.CachedTokenFilepath = cachedPath
- })
- } else {
- cfgCopy.Region = sharedConfig.SSORegion
- }
- cfg.Credentials = ssocreds.New(sso.NewFromConfig(cfgCopy), sharedConfig.SSOAccountID, sharedConfig.SSORoleName, sharedConfig.SSOStartURL, options...)
- return nil
- }
- func ecsContainerURI(path string) string {
- return fmt.Sprintf("%s%s", ecsContainerEndpoint, path)
- }
- func processCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error {
- var opts []func(*processcreds.Options)
- options, found, err := getProcessCredentialOptions(ctx, configs)
- if err != nil {
- return err
- }
- if found {
- opts = append(opts, options)
- }
- cfg.Credentials = processcreds.NewProvider(sharedConfig.CredentialProcess, opts...)
- return nil
- }
- func resolveLocalHTTPCredProvider(ctx context.Context, cfg *aws.Config, endpointURL, authToken string, configs configs) error {
- var resolveErr error
- parsed, err := url.Parse(endpointURL)
- if err != nil {
- resolveErr = fmt.Errorf("invalid URL, %w", err)
- } else {
- host := parsed.Hostname()
- if len(host) == 0 {
- resolveErr = fmt.Errorf("unable to parse host from local HTTP cred provider URL")
- } else if isLoopback, loopbackErr := isLoopbackHost(host); loopbackErr != nil {
- resolveErr = fmt.Errorf("failed to resolve host %q, %v", host, loopbackErr)
- } else if !isLoopback {
- resolveErr = fmt.Errorf("invalid endpoint host, %q, only loopback hosts are allowed", host)
- }
- }
- if resolveErr != nil {
- return resolveErr
- }
- return resolveHTTPCredProvider(ctx, cfg, endpointURL, authToken, configs)
- }
- func resolveHTTPCredProvider(ctx context.Context, cfg *aws.Config, url, authToken string, configs configs) error {
- optFns := []func(*endpointcreds.Options){
- func(options *endpointcreds.Options) {
- if len(authToken) != 0 {
- options.AuthorizationToken = authToken
- }
- options.APIOptions = cfg.APIOptions
- if cfg.Retryer != nil {
- options.Retryer = cfg.Retryer()
- }
- },
- }
- optFn, found, err := getEndpointCredentialProviderOptions(ctx, configs)
- if err != nil {
- return err
- }
- if found {
- optFns = append(optFns, optFn)
- }
- provider := endpointcreds.New(url, optFns...)
- cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider, func(options *aws.CredentialsCacheOptions) {
- options.ExpiryWindow = 5 * time.Minute
- })
- if err != nil {
- return err
- }
- return nil
- }
- func resolveCredsFromSource(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedCfg *SharedConfig, configs configs) (err error) {
- switch sharedCfg.CredentialSource {
- case credSourceEc2Metadata:
- return resolveEC2RoleCredentials(ctx, cfg, configs)
- case credSourceEnvironment:
- cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials}
- case credSourceECSContainer:
- if len(envConfig.ContainerCredentialsRelativePath) == 0 {
- return fmt.Errorf("EcsContainer was specified as the credential_source, but 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' was not set")
- }
- return resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs)
- default:
- return fmt.Errorf("credential_source values must be EcsContainer, Ec2InstanceMetadata, or Environment")
- }
- return nil
- }
- func resolveEC2RoleCredentials(ctx context.Context, cfg *aws.Config, configs configs) error {
- optFns := make([]func(*ec2rolecreds.Options), 0, 2)
- optFn, found, err := getEC2RoleCredentialProviderOptions(ctx, configs)
- if err != nil {
- return err
- }
- if found {
- optFns = append(optFns, optFn)
- }
- optFns = append(optFns, func(o *ec2rolecreds.Options) {
- // Only define a client from config if not already defined.
- if o.Client == nil {
- o.Client = imds.NewFromConfig(*cfg)
- }
- })
- provider := ec2rolecreds.New(optFns...)
- cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider)
- if err != nil {
- return err
- }
- return nil
- }
- func getAWSConfigSources(cfgs configs) (*EnvConfig, *SharedConfig, configs) {
- var (
- envConfig *EnvConfig
- sharedConfig *SharedConfig
- other configs
- )
- for i := range cfgs {
- switch c := cfgs[i].(type) {
- case EnvConfig:
- if envConfig == nil {
- envConfig = &c
- }
- case *EnvConfig:
- if envConfig == nil {
- envConfig = c
- }
- case SharedConfig:
- if sharedConfig == nil {
- sharedConfig = &c
- }
- case *SharedConfig:
- if envConfig == nil {
- sharedConfig = c
- }
- default:
- other = append(other, c)
- }
- }
- if envConfig == nil {
- envConfig = &EnvConfig{}
- }
- if sharedConfig == nil {
- sharedConfig = &SharedConfig{}
- }
- return envConfig, sharedConfig, other
- }
- // AssumeRoleTokenProviderNotSetError is an error returned when creating a
- // session when the MFAToken option is not set when shared config is configured
- // load assume a role with an MFA token.
- type AssumeRoleTokenProviderNotSetError struct{}
- // Error is the error message
- func (e AssumeRoleTokenProviderNotSetError) Error() string {
- return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.")
- }
- func assumeWebIdentity(ctx context.Context, cfg *aws.Config, filepath string, roleARN, sessionName string, configs configs) error {
- if len(filepath) == 0 {
- return fmt.Errorf("token file path is not set")
- }
- if len(roleARN) == 0 {
- return fmt.Errorf("role ARN is not set")
- }
- optFns := []func(*stscreds.WebIdentityRoleOptions){
- func(options *stscreds.WebIdentityRoleOptions) {
- options.RoleSessionName = sessionName
- },
- }
- optFn, found, err := getWebIdentityCredentialProviderOptions(ctx, configs)
- if err != nil {
- return err
- }
- if found {
- optFns = append(optFns, optFn)
- }
- provider := stscreds.NewWebIdentityRoleProvider(sts.NewFromConfig(*cfg), roleARN, stscreds.IdentityTokenFile(filepath), optFns...)
- cfg.Credentials = provider
- return nil
- }
- func credsFromAssumeRole(ctx context.Context, cfg *aws.Config, sharedCfg *SharedConfig, configs configs) (err error) {
- optFns := []func(*stscreds.AssumeRoleOptions){
- func(options *stscreds.AssumeRoleOptions) {
- options.RoleSessionName = sharedCfg.RoleSessionName
- if sharedCfg.RoleDurationSeconds != nil {
- if *sharedCfg.RoleDurationSeconds/time.Minute > 15 {
- options.Duration = *sharedCfg.RoleDurationSeconds
- }
- }
- // Assume role with external ID
- if len(sharedCfg.ExternalID) > 0 {
- options.ExternalID = aws.String(sharedCfg.ExternalID)
- }
- // Assume role with MFA
- if len(sharedCfg.MFASerial) != 0 {
- options.SerialNumber = aws.String(sharedCfg.MFASerial)
- }
- },
- }
- optFn, found, err := getAssumeRoleCredentialProviderOptions(ctx, configs)
- if err != nil {
- return err
- }
- if found {
- optFns = append(optFns, optFn)
- }
- {
- // Synthesize options early to validate configuration errors sooner to ensure a token provider
- // is present if the SerialNumber was set.
- var o stscreds.AssumeRoleOptions
- for _, fn := range optFns {
- fn(&o)
- }
- if o.TokenProvider == nil && o.SerialNumber != nil {
- return AssumeRoleTokenProviderNotSetError{}
- }
- }
- cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(*cfg), sharedCfg.RoleARN, optFns...)
- return nil
- }
- // wrapWithCredentialsCache will wrap provider with an aws.CredentialsCache
- // with the provided options if the provider is not already a
- // aws.CredentialsCache.
- func wrapWithCredentialsCache(
- ctx context.Context,
- cfgs configs,
- provider aws.CredentialsProvider,
- optFns ...func(options *aws.CredentialsCacheOptions),
- ) (aws.CredentialsProvider, error) {
- _, ok := provider.(*aws.CredentialsCache)
- if ok {
- return provider, nil
- }
- credCacheOptions, optionsFound, err := getCredentialsCacheOptionsProvider(ctx, cfgs)
- if err != nil {
- return nil, err
- }
- // force allocation of a new slice if the additional options are
- // needed, to prevent overwriting the passed in slice of options.
- optFns = optFns[:len(optFns):len(optFns)]
- if optionsFound {
- optFns = append(optFns, credCacheOptions)
- }
- return aws.NewCredentialsCache(provider, optFns...), nil
- }
|