code.dwrz.net

Go monorepo.
Log | Files | Refs

resolve_credentials.go (13948B)


      1 package config
      2 
      3 import (
      4 	"context"
      5 	"fmt"
      6 	"net/url"
      7 	"time"
      8 
      9 	"github.com/aws/aws-sdk-go-v2/aws"
     10 	"github.com/aws/aws-sdk-go-v2/credentials"
     11 	"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
     12 	"github.com/aws/aws-sdk-go-v2/credentials/endpointcreds"
     13 	"github.com/aws/aws-sdk-go-v2/credentials/processcreds"
     14 	"github.com/aws/aws-sdk-go-v2/credentials/ssocreds"
     15 	"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
     16 	"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
     17 	"github.com/aws/aws-sdk-go-v2/service/sso"
     18 	"github.com/aws/aws-sdk-go-v2/service/sts"
     19 )
     20 
     21 const (
     22 	// valid credential source values
     23 	credSourceEc2Metadata  = "Ec2InstanceMetadata"
     24 	credSourceEnvironment  = "Environment"
     25 	credSourceECSContainer = "EcsContainer"
     26 )
     27 
     28 var (
     29 	ecsContainerEndpoint = "http://169.254.170.2" // not constant to allow for swapping during unit-testing
     30 )
     31 
     32 // resolveCredentials extracts a credential provider from slice of config
     33 // sources.
     34 //
     35 // If an explicit credential provider is not found the resolver will fallback
     36 // to resolving credentials by extracting a credential provider from EnvConfig
     37 // and SharedConfig.
     38 func resolveCredentials(ctx context.Context, cfg *aws.Config, configs configs) error {
     39 	found, err := resolveCredentialProvider(ctx, cfg, configs)
     40 	if found || err != nil {
     41 		return err
     42 	}
     43 
     44 	return resolveCredentialChain(ctx, cfg, configs)
     45 }
     46 
     47 // resolveCredentialProvider extracts the first instance of Credentials from the
     48 // config slices.
     49 //
     50 // The resolved CredentialProvider will be wrapped in a cache to ensure the
     51 // credentials are only refreshed when needed. This also protects the
     52 // credential provider to be used concurrently.
     53 //
     54 // Config providers used:
     55 // * credentialsProviderProvider
     56 func resolveCredentialProvider(ctx context.Context, cfg *aws.Config, configs configs) (bool, error) {
     57 	credProvider, found, err := getCredentialsProvider(ctx, configs)
     58 	if !found || err != nil {
     59 		return false, err
     60 	}
     61 
     62 	cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, credProvider)
     63 	if err != nil {
     64 		return false, err
     65 	}
     66 
     67 	return true, nil
     68 }
     69 
     70 // resolveCredentialChain resolves a credential provider chain using EnvConfig
     71 // and SharedConfig if present in the slice of provided configs.
     72 //
     73 // The resolved CredentialProvider will be wrapped in a cache to ensure the
     74 // credentials are only refreshed when needed. This also protects the
     75 // credential provider to be used concurrently.
     76 func resolveCredentialChain(ctx context.Context, cfg *aws.Config, configs configs) (err error) {
     77 	envConfig, sharedConfig, other := getAWSConfigSources(configs)
     78 
     79 	// When checking if a profile was specified programmatically we should only consider the "other"
     80 	// configuration sources that have been provided. This ensures we correctly honor the expected credential
     81 	// hierarchy.
     82 	_, sharedProfileSet, err := getSharedConfigProfile(ctx, other)
     83 	if err != nil {
     84 		return err
     85 	}
     86 
     87 	switch {
     88 	case sharedProfileSet:
     89 		err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other)
     90 	case envConfig.Credentials.HasKeys():
     91 		cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials}
     92 	case len(envConfig.WebIdentityTokenFilePath) > 0:
     93 		err = assumeWebIdentity(ctx, cfg, envConfig.WebIdentityTokenFilePath, envConfig.RoleARN, envConfig.RoleSessionName, configs)
     94 	default:
     95 		err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other)
     96 	}
     97 	if err != nil {
     98 		return err
     99 	}
    100 
    101 	// Wrap the resolved provider in a cache so the SDK will cache credentials.
    102 	cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, cfg.Credentials)
    103 	if err != nil {
    104 		return err
    105 	}
    106 
    107 	return nil
    108 }
    109 
    110 func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedConfig *SharedConfig, configs configs) (err error) {
    111 
    112 	switch {
    113 	case sharedConfig.Source != nil:
    114 		// Assume IAM role with credentials source from a different profile.
    115 		err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig.Source, configs)
    116 
    117 	case sharedConfig.Credentials.HasKeys():
    118 		// Static Credentials from Shared Config/Credentials file.
    119 		cfg.Credentials = credentials.StaticCredentialsProvider{
    120 			Value: sharedConfig.Credentials,
    121 		}
    122 
    123 	case len(sharedConfig.CredentialSource) != 0:
    124 		err = resolveCredsFromSource(ctx, cfg, envConfig, sharedConfig, configs)
    125 
    126 	case len(sharedConfig.WebIdentityTokenFile) != 0:
    127 		// Credentials from Assume Web Identity token require an IAM Role, and
    128 		// that roll will be assumed. May be wrapped with another assume role
    129 		// via SourceProfile.
    130 		return assumeWebIdentity(ctx, cfg, sharedConfig.WebIdentityTokenFile, sharedConfig.RoleARN, sharedConfig.RoleSessionName, configs)
    131 
    132 	case sharedConfig.hasSSOConfiguration():
    133 		err = resolveSSOCredentials(ctx, cfg, sharedConfig, configs)
    134 
    135 	case len(sharedConfig.CredentialProcess) != 0:
    136 		// Get credentials from CredentialProcess
    137 		err = processCredentials(ctx, cfg, sharedConfig, configs)
    138 
    139 	case len(envConfig.ContainerCredentialsEndpoint) != 0:
    140 		err = resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs)
    141 
    142 	case len(envConfig.ContainerCredentialsRelativePath) != 0:
    143 		err = resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs)
    144 
    145 	default:
    146 		err = resolveEC2RoleCredentials(ctx, cfg, configs)
    147 	}
    148 	if err != nil {
    149 		return err
    150 	}
    151 
    152 	if len(sharedConfig.RoleARN) > 0 {
    153 		return credsFromAssumeRole(ctx, cfg, sharedConfig, configs)
    154 	}
    155 
    156 	return nil
    157 }
    158 
    159 func resolveSSOCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error {
    160 	if err := sharedConfig.validateSSOConfiguration(); err != nil {
    161 		return err
    162 	}
    163 
    164 	var options []func(*ssocreds.Options)
    165 	v, found, err := getSSOProviderOptions(ctx, configs)
    166 	if err != nil {
    167 		return err
    168 	}
    169 	if found {
    170 		options = append(options, v)
    171 	}
    172 
    173 	cfgCopy := cfg.Copy()
    174 	cfgCopy.Region = sharedConfig.SSORegion
    175 
    176 	cfg.Credentials = ssocreds.New(sso.NewFromConfig(cfgCopy), sharedConfig.SSOAccountID, sharedConfig.SSORoleName, sharedConfig.SSOStartURL, options...)
    177 
    178 	return nil
    179 }
    180 
    181 func ecsContainerURI(path string) string {
    182 	return fmt.Sprintf("%s%s", ecsContainerEndpoint, path)
    183 }
    184 
    185 func processCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error {
    186 	var opts []func(*processcreds.Options)
    187 
    188 	options, found, err := getProcessCredentialOptions(ctx, configs)
    189 	if err != nil {
    190 		return err
    191 	}
    192 	if found {
    193 		opts = append(opts, options)
    194 	}
    195 
    196 	cfg.Credentials = processcreds.NewProvider(sharedConfig.CredentialProcess, opts...)
    197 
    198 	return nil
    199 }
    200 
    201 func resolveLocalHTTPCredProvider(ctx context.Context, cfg *aws.Config, endpointURL, authToken string, configs configs) error {
    202 	var resolveErr error
    203 
    204 	parsed, err := url.Parse(endpointURL)
    205 	if err != nil {
    206 		resolveErr = fmt.Errorf("invalid URL, %w", err)
    207 	} else {
    208 		host := parsed.Hostname()
    209 		if len(host) == 0 {
    210 			resolveErr = fmt.Errorf("unable to parse host from local HTTP cred provider URL")
    211 		} else if isLoopback, loopbackErr := isLoopbackHost(host); loopbackErr != nil {
    212 			resolveErr = fmt.Errorf("failed to resolve host %q, %v", host, loopbackErr)
    213 		} else if !isLoopback {
    214 			resolveErr = fmt.Errorf("invalid endpoint host, %q, only loopback hosts are allowed", host)
    215 		}
    216 	}
    217 
    218 	if resolveErr != nil {
    219 		return resolveErr
    220 	}
    221 
    222 	return resolveHTTPCredProvider(ctx, cfg, endpointURL, authToken, configs)
    223 }
    224 
    225 func resolveHTTPCredProvider(ctx context.Context, cfg *aws.Config, url, authToken string, configs configs) error {
    226 	optFns := []func(*endpointcreds.Options){
    227 		func(options *endpointcreds.Options) {
    228 			if len(authToken) != 0 {
    229 				options.AuthorizationToken = authToken
    230 			}
    231 			options.APIOptions = cfg.APIOptions
    232 			if cfg.Retryer != nil {
    233 				options.Retryer = cfg.Retryer()
    234 			}
    235 		},
    236 	}
    237 
    238 	optFn, found, err := getEndpointCredentialProviderOptions(ctx, configs)
    239 	if err != nil {
    240 		return err
    241 	}
    242 	if found {
    243 		optFns = append(optFns, optFn)
    244 	}
    245 
    246 	provider := endpointcreds.New(url, optFns...)
    247 
    248 	cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider, func(options *aws.CredentialsCacheOptions) {
    249 		options.ExpiryWindow = 5 * time.Minute
    250 	})
    251 	if err != nil {
    252 		return err
    253 	}
    254 
    255 	return nil
    256 }
    257 
    258 func resolveCredsFromSource(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedCfg *SharedConfig, configs configs) (err error) {
    259 	switch sharedCfg.CredentialSource {
    260 	case credSourceEc2Metadata:
    261 		return resolveEC2RoleCredentials(ctx, cfg, configs)
    262 
    263 	case credSourceEnvironment:
    264 		cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials}
    265 
    266 	case credSourceECSContainer:
    267 		if len(envConfig.ContainerCredentialsRelativePath) == 0 {
    268 			return fmt.Errorf("EcsContainer was specified as the credential_source, but 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' was not set")
    269 		}
    270 		return resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs)
    271 
    272 	default:
    273 		return fmt.Errorf("credential_source values must be EcsContainer, Ec2InstanceMetadata, or Environment")
    274 	}
    275 
    276 	return nil
    277 }
    278 
    279 func resolveEC2RoleCredentials(ctx context.Context, cfg *aws.Config, configs configs) error {
    280 	optFns := make([]func(*ec2rolecreds.Options), 0, 2)
    281 
    282 	optFn, found, err := getEC2RoleCredentialProviderOptions(ctx, configs)
    283 	if err != nil {
    284 		return err
    285 	}
    286 	if found {
    287 		optFns = append(optFns, optFn)
    288 	}
    289 
    290 	optFns = append(optFns, func(o *ec2rolecreds.Options) {
    291 		// Only define a client from config if not already defined.
    292 		if o.Client == nil {
    293 			o.Client = imds.NewFromConfig(*cfg)
    294 		}
    295 	})
    296 
    297 	provider := ec2rolecreds.New(optFns...)
    298 
    299 	cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider)
    300 	if err != nil {
    301 		return err
    302 	}
    303 
    304 	return nil
    305 }
    306 
    307 func getAWSConfigSources(cfgs configs) (*EnvConfig, *SharedConfig, configs) {
    308 	var (
    309 		envConfig    *EnvConfig
    310 		sharedConfig *SharedConfig
    311 		other        configs
    312 	)
    313 
    314 	for i := range cfgs {
    315 		switch c := cfgs[i].(type) {
    316 		case EnvConfig:
    317 			if envConfig == nil {
    318 				envConfig = &c
    319 			}
    320 		case *EnvConfig:
    321 			if envConfig == nil {
    322 				envConfig = c
    323 			}
    324 		case SharedConfig:
    325 			if sharedConfig == nil {
    326 				sharedConfig = &c
    327 			}
    328 		case *SharedConfig:
    329 			if envConfig == nil {
    330 				sharedConfig = c
    331 			}
    332 		default:
    333 			other = append(other, c)
    334 		}
    335 	}
    336 
    337 	if envConfig == nil {
    338 		envConfig = &EnvConfig{}
    339 	}
    340 
    341 	if sharedConfig == nil {
    342 		sharedConfig = &SharedConfig{}
    343 	}
    344 
    345 	return envConfig, sharedConfig, other
    346 }
    347 
    348 // AssumeRoleTokenProviderNotSetError is an error returned when creating a
    349 // session when the MFAToken option is not set when shared config is configured
    350 // load assume a role with an MFA token.
    351 type AssumeRoleTokenProviderNotSetError struct{}
    352 
    353 // Error is the error message
    354 func (e AssumeRoleTokenProviderNotSetError) Error() string {
    355 	return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.")
    356 }
    357 
    358 func assumeWebIdentity(ctx context.Context, cfg *aws.Config, filepath string, roleARN, sessionName string, configs configs) error {
    359 	if len(filepath) == 0 {
    360 		return fmt.Errorf("token file path is not set")
    361 	}
    362 
    363 	if len(roleARN) == 0 {
    364 		return fmt.Errorf("role ARN is not set")
    365 	}
    366 
    367 	optFns := []func(*stscreds.WebIdentityRoleOptions){
    368 		func(options *stscreds.WebIdentityRoleOptions) {
    369 			options.RoleSessionName = sessionName
    370 		},
    371 	}
    372 
    373 	optFn, found, err := getWebIdentityCredentialProviderOptions(ctx, configs)
    374 	if err != nil {
    375 		return err
    376 	}
    377 	if found {
    378 		optFns = append(optFns, optFn)
    379 	}
    380 
    381 	provider := stscreds.NewWebIdentityRoleProvider(sts.NewFromConfig(*cfg), roleARN, stscreds.IdentityTokenFile(filepath), optFns...)
    382 
    383 	cfg.Credentials = provider
    384 
    385 	return nil
    386 }
    387 
    388 func credsFromAssumeRole(ctx context.Context, cfg *aws.Config, sharedCfg *SharedConfig, configs configs) (err error) {
    389 	optFns := []func(*stscreds.AssumeRoleOptions){
    390 		func(options *stscreds.AssumeRoleOptions) {
    391 			options.RoleSessionName = sharedCfg.RoleSessionName
    392 			if sharedCfg.RoleDurationSeconds != nil {
    393 				if *sharedCfg.RoleDurationSeconds/time.Minute > 15 {
    394 					options.Duration = *sharedCfg.RoleDurationSeconds
    395 				}
    396 			}
    397 			// Assume role with external ID
    398 			if len(sharedCfg.ExternalID) > 0 {
    399 				options.ExternalID = aws.String(sharedCfg.ExternalID)
    400 			}
    401 
    402 			// Assume role with MFA
    403 			if len(sharedCfg.MFASerial) != 0 {
    404 				options.SerialNumber = aws.String(sharedCfg.MFASerial)
    405 			}
    406 		},
    407 	}
    408 
    409 	optFn, found, err := getAssumeRoleCredentialProviderOptions(ctx, configs)
    410 	if err != nil {
    411 		return err
    412 	}
    413 	if found {
    414 		optFns = append(optFns, optFn)
    415 	}
    416 
    417 	{
    418 		// Synthesize options early to validate configuration errors sooner to ensure a token provider
    419 		// is present if the SerialNumber was set.
    420 		var o stscreds.AssumeRoleOptions
    421 		for _, fn := range optFns {
    422 			fn(&o)
    423 		}
    424 		if o.TokenProvider == nil && o.SerialNumber != nil {
    425 			return AssumeRoleTokenProviderNotSetError{}
    426 		}
    427 	}
    428 
    429 	cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(*cfg), sharedCfg.RoleARN, optFns...)
    430 
    431 	return nil
    432 }
    433 
    434 // wrapWithCredentialsCache will wrap provider with an aws.CredentialsCache
    435 // with the provided options if the provider is not already a
    436 // aws.CredentialsCache.
    437 func wrapWithCredentialsCache(
    438 	ctx context.Context,
    439 	cfgs configs,
    440 	provider aws.CredentialsProvider,
    441 	optFns ...func(options *aws.CredentialsCacheOptions),
    442 ) (aws.CredentialsProvider, error) {
    443 	_, ok := provider.(*aws.CredentialsCache)
    444 	if ok {
    445 		return provider, nil
    446 	}
    447 
    448 	credCacheOptions, optionsFound, err := getCredentialsCacheOptionsProvider(ctx, cfgs)
    449 	if err != nil {
    450 		return nil, err
    451 	}
    452 
    453 	// force allocation of a new slice if the additional options are
    454 	// needed, to prevent overwriting the passed in slice of options.
    455 	optFns = optFns[:len(optFns):len(optFns)]
    456 	if optionsFound {
    457 		optFns = append(optFns, credCacheOptions)
    458 	}
    459 
    460 	return aws.NewCredentialsCache(provider, optFns...), nil
    461 }