resolve_credentials.go 14 KB

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