aws_v2.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package integrations
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  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/service/sts"
  10. smithyhttp "github.com/aws/smithy-go/transport/http"
  11. "github.com/porter-dev/porter/api/types"
  12. "gorm.io/gorm"
  13. )
  14. // AWSIntegration is an auth mechanism that uses a AWS IAM user to
  15. // authenticate
  16. type AWSIntegration struct {
  17. gorm.Model
  18. // The id of the user that linked this auth mechanism
  19. UserID uint `json:"user_id"`
  20. // The project that this integration belongs to
  21. ProjectID uint `json:"project_id"`
  22. // The AWS arn this is integration is linked to
  23. AWSArn string `json:"aws_arn"`
  24. // The optional AWS region (required by some session configurations)
  25. AWSRegion string `json:"aws_region"`
  26. // The assumed role ARN to use for sessions
  27. AWSAssumeRoleArn string
  28. // ------------------------------------------------------------------
  29. // All fields encrypted before storage.
  30. // ------------------------------------------------------------------
  31. // The AWS cluster ID
  32. // See https://github.com/kubernetes-sigs/aws-iam-authenticator#what-is-a-cluster-id
  33. AWSClusterID []byte `json:"aws_cluster_id"`
  34. // The AWS access key for this IAM user
  35. AWSAccessKeyID []byte `json:"aws_access_key_id"`
  36. // The AWS secret key for this IAM user
  37. AWSSecretAccessKey []byte `json:"aws_secret_access_key"`
  38. // An optional session token, if the user is assuming a role
  39. AWSSessionToken []byte `json:"aws_session_token"`
  40. }
  41. func (a *AWSIntegration) ToAWSIntegrationType() types.AWSIntegration {
  42. return types.AWSIntegration{
  43. CreatedAt: a.CreatedAt,
  44. ID: a.ID,
  45. UserID: a.UserID,
  46. ProjectID: a.ProjectID,
  47. AWSArn: a.AWSArn,
  48. }
  49. }
  50. // Config returns a populated AWS Config for use with aws-go-sdk-v2 services
  51. func (a *AWSIntegration) Config() aws.Config {
  52. awsConf := aws.Config{
  53. Credentials: credentials.NewStaticCredentialsProvider(
  54. *aws.String(string(a.AWSAccessKeyID)),
  55. *aws.String(string(a.AWSSecretAccessKey)),
  56. *aws.String(string(a.AWSSessionToken)),
  57. ),
  58. }
  59. if a.AWSRegion != "" {
  60. awsConf.Region = a.AWSRegion
  61. }
  62. return awsConf
  63. }
  64. // PopulateAWSArn uses the access key/secret to get the caller identity, and
  65. // attaches it to the AWS integration
  66. func (a *AWSIntegration) PopulateAWSArn(ctx context.Context) error {
  67. svc := sts.NewFromConfig(a.Config())
  68. result, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
  69. if err != nil {
  70. return err
  71. }
  72. a.AWSArn = *result.Arn
  73. return nil
  74. }
  75. // GetBearerToken retrieves a bearer token for an AWS account
  76. func (a *AWSIntegration) GetBearerToken(
  77. ctx context.Context,
  78. getTokenCache GetTokenCacheFunc,
  79. setTokenCache SetTokenCacheFunc,
  80. clusterID string,
  81. shouldClusterIdOverride bool,
  82. ) (string, error) {
  83. cache, err := getTokenCache()
  84. // check the token cache for a non-expired token
  85. if cache != nil {
  86. if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
  87. return string(tok), nil
  88. }
  89. }
  90. var validClusterId string
  91. if shouldClusterIdOverride {
  92. validClusterId = clusterID
  93. } else {
  94. validClusterId = string(a.AWSClusterID)
  95. if validClusterId == "" {
  96. validClusterId = clusterID
  97. }
  98. }
  99. token, err := a.GetWithSTS(ctx, clusterID)
  100. if err != nil {
  101. return "", err
  102. }
  103. setTokenCache(token.Token, token.Expiration)
  104. return token.Token, nil
  105. }
  106. // Token is generated and used by Kubernetes client-go to authenticate with a Kubernetes cluster.
  107. // Original source: https://github.com/weaveworks/eksctl/blob/5f2a59056a4852470c66502205d2db0aa7c84c5e/pkg/eks/auth/generator.go#LL46C24-L46C24
  108. type Token struct {
  109. Token string
  110. Expiration time.Time
  111. }
  112. const (
  113. clusterIDHeader = "x-k8s-aws-id"
  114. presignedURLExpiration = 10 * time.Minute
  115. v1Prefix = "k8s-aws-v1."
  116. )
  117. // GetWithSTS returns a token valid for clusterID using the given STS client.
  118. // This implementation follows the steps outlined here:
  119. // https://github.com/kubernetes-sigs/aws-iam-authenticator#api-authorization-from-outside-a-cluster
  120. // We either add this implementation or have to maintain two versions of STS since aws-iam-authenticator is
  121. // not switching over to aws-go-sdk-v2.
  122. func (a AWSIntegration) GetWithSTS(ctx context.Context, clusterID string) (Token, error) {
  123. presignClient := sts.NewPresignClient(sts.NewFromConfig(a.Config()))
  124. // generate a sts:GetCallerIdentity request and add our custom cluster ID header
  125. presignedURLRequest, err := presignClient.PresignGetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}, func(presignOptions *sts.PresignOptions) {
  126. presignOptions.ClientOptions = append(presignOptions.ClientOptions, a.appendPresignHeaderValuesFunc(clusterID))
  127. })
  128. if err != nil {
  129. return Token{}, fmt.Errorf("failed to presign caller identity: %w", err)
  130. }
  131. tokenExpiration := time.Now().Local().Add(presignedURLExpiration)
  132. // Add the token with k8s-aws-v1. prefix.
  133. return Token{v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLRequest.URL)), tokenExpiration}, nil
  134. }
  135. func (a AWSIntegration) appendPresignHeaderValuesFunc(clusterID string) func(stsOptions *sts.Options) {
  136. return func(stsOptions *sts.Options) {
  137. // Add clusterId Header
  138. stsOptions.APIOptions = append(stsOptions.APIOptions, smithyhttp.SetHeaderValue(clusterIDHeader, clusterID))
  139. // Add X-Amz-Expires query param
  140. stsOptions.APIOptions = append(stsOptions.APIOptions, smithyhttp.SetHeaderValue("X-Amz-Expires", "60"))
  141. }
  142. }