From 957c88eaca0bfd666ad525ef607df4d60c4c4ea2 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 12 Oct 2020 17:58:58 +0200 Subject: [PATCH] CloudWatch: Re-implement authentication (#25548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CloudWatch: Revisit authentication Signed-off-by: Arve Knudsen * CloudWatch: Simplify auth code Signed-off-by: Arve Knudsen * Use ARN Signed-off-by: Arve Knudsen * Add Drone configuration Signed-off-by: Arve Knudsen * Remove unused code Signed-off-by: Arve Knudsen * Remove .drone.yml Signed-off-by: Arve Knudsen * Fix external ID usage Signed-off-by: Arve Knudsen * CloudWatch: Fix issues after merge Signed-off-by: Arve Knudsen * Remove stale code Signed-off-by: Arve Knudsen * Remove stale code Signed-off-by: Arve Knudsen * Use auth type enum Signed-off-by: Arve Knudsen * Fix test snapshot * Coordinate frontend and backend option names Signed-off-by: Arve Knudsen * Remove old comments Signed-off-by: Arve Knudsen * Fix front-end tests Signed-off-by: Arve Knudsen * Introduce session cache Signed-off-by: Arve Knudsen * Use constants Signed-off-by: Arve Knudsen * Fix field alignment * CloudWatch: Fix log message Signed-off-by: Arve Knudsen * Tidy go.mod Signed-off-by: Arve Knudsen * CloudWatch: Handle arn auth type Signed-off-by: Arve Knudsen * CloudWatch: Fix role assumption duration Signed-off-by: Arve Knudsen * Fix test Signed-off-by: Arve Knudsen * CloudWatch: Inline unnecessary constants Signed-off-by: Arve Knudsen * CloudWatch: Use serial comma in UI Signed-off-by: Arve Knudsen * CloudWatch: Inline unnecessary constants Signed-off-by: Arve Knudsen * CloudWatch: Fail if missing region Signed-off-by: Arve Knudsen * CloudWatch: Handle unconfigured region Signed-off-by: Arve Knudsen * CloudWatch: Log when using cached session Signed-off-by: Arve Knudsen * CloudWatch: Include region in cache key Signed-off-by: Arve Knudsen * Add UI warnings for lecagy support * Do not clear ARN fields whenging change authentication provider * Graph NG: annotations display (#27972) * Annotations support POC * Fix markers memoization * dev dashboard update * Update public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx * CloudWatch: Remove errors.BadRequest Signed-off-by: Arve Knudsen * CloudWatch: Undo unintentional change Signed-off-by: Arve Knudsen * Remove log line Signed-off-by: Arve Knudsen * Fix cache key computation Signed-off-by: Arve Knudsen * Add region to cache key Signed-off-by: Arve Knudsen * Improve log messages Signed-off-by: Arve Knudsen * CloudWatch: Add documentation Signed-off-by: Arve Knudsen * Improve tooltip Signed-off-by: Arve Knudsen * Improve docs Signed-off-by: Arve Knudsen * Improve docs Signed-off-by: Arve Knudsen * Improve docs Signed-off-by: Arve Knudsen * Improve tooltip Signed-off-by: Arve Knudsen * Add role assumption provisioning example Signed-off-by: Arve Knudsen * Add upgrade notes Signed-off-by: Arve Knudsen * Improve docs Signed-off-by: Arve Knudsen * Apply suggestions from code review Co-authored-by: Marcus Efraimsson * backend: use latest sdk (#28147) fixes #27713 via https://github.com/grafana/grafana-plugin-sdk-go/pull/227 * Docs: Update Permissions documentation (#28144) * removed overview.md * content updates * Update datasource_permissions.md * update content * content updates * Update organization_roles.md * Update docs/sources/enterprise/saml.md Co-authored-by: Kyle Brandt * Update dashboard_folder_permissions.md Co-authored-by: Kyle Brandt * area/grafana/toolkit: ci-package needs to use synchronous writes (#28148) * ci needs to use synchronous writes or the file ends up with zero length * Add instructions to upload license via UI (#28067) * Add UI license upload option, reformat Enterprise license activation section Added the option to upload a license file through the Server Admin UI, and did a little reformatting to make license activation look more like a process. * Headers not bold, hyphens not asterisks * Github: run metrics collector workflow every 10min (#28153) * GithubActions: Updated cron schedule * Updated * Docs: Update explore docs: remove dot at the end of line (#28151) HI - Removed Dot(.) at the end of line to make it consistent with other 2 points. Thanks, Ashish * Fix frontend tests Signed-off-by: Arve Knudsen * Fix frontend tests Signed-off-by: Arve Knudsen * Docs: Update upgrade notes Co-authored-by: Sofia Papagiannaki Co-authored-by: Dominik Prokop Co-authored-by: Marcus Efraimsson Co-authored-by: Kyle Brandt Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Brian Gann Co-authored-by: Mitch Seaman Co-authored-by: Torkel Ödegaard Co-authored-by: Torkel Ödegaard Co-authored-by: ashishagarwal06 <34888589+ashishagarwal06@users.noreply.github.com> --- CHANGELOG.md | 6 + docs/sources/administration/provisioning.md | 11 +- docs/sources/datasources/cloudwatch.md | 72 ++- docs/sources/installation/upgrading.md | 19 + go.mod | 1 - pkg/tsdb/cloudwatch/cloudwatch.go | 150 +++++- pkg/tsdb/cloudwatch/credentials.go | 176 ------- pkg/tsdb/cloudwatch/credentials_test.go | 123 ----- pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go | 436 ------------------ pkg/tsdb/cloudwatch/session.go | 36 ++ pkg/tsdb/cloudwatch/session_test.go | 107 +++++ pkg/tsdb/cloudwatch/test_utils.go | 18 +- .../components/ConfigEditor.test.tsx | 6 +- .../cloudwatch/components/ConfigEditor.tsx | 64 ++- .../__snapshots__/ConfigEditor.test.tsx.snap | 301 ++++++++++-- 15 files changed, 701 insertions(+), 825 deletions(-) delete mode 100644 pkg/tsdb/cloudwatch/credentials.go delete mode 100644 pkg/tsdb/cloudwatch/credentials_test.go delete mode 100644 pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go create mode 100644 pkg/tsdb/cloudwatch/session.go create mode 100644 pkg/tsdb/cloudwatch/session_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7590b0e5fcc..a7c2229b029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 7.3.0-beta1 (2020-10-14) + +### Breaking changes + +- **CloudWatch**: The AWS CloudWatch data source's authentication scheme has changed. See the [upgrade notes](https://grafana.com/docs/grafana/latest/installation/upgrading/#upgrading-to-v73) for details and how this may affect you. + # 7.2.1 (2020-10-08) ### Features / Enhancements diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index cb2f9b137cd..a5f3e0c6800 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -140,7 +140,7 @@ Please refer to each datasource documentation for specific provisioning examples | ------------- | ---------------------------------------------------------------------------------- | | Elasticsearch | Elasticsearch uses the `database` property to configure the index for a datasource | -#### Json Data +#### JSON Data Since not all datasources have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `jsonData` field. Here are the most common settings that the core datasources use. @@ -157,11 +157,12 @@ Since not all datasources have the same configuration settings we only have the | interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' | | logMessageField | string | Elasticsearch | Which field should be used as the log message | | logLevelField | string | Elasticsearch | Which field should be used to indicate the priority of the log message | -| authType | string | Cloudwatch | Auth provider. keys/credentials/arn | -| assumeRoleArn | string | Cloudwatch | ARN of Assume Role | -| defaultRegion | string | Cloudwatch | AWS region | +| authType | string | Cloudwatch | Auth provider. default/credentials/keys | +| externalId | string | Cloudwatch | Optional External ID | +| assumeRoleArn | string | Cloudwatch | Optional ARN role to assume | +| defaultRegion | string | Cloudwatch | Optional default AWS region | | customMetricsNamespaces | string | Cloudwatch | Namespaces of Custom Metrics | -| profile | string | Cloudwatch | Custom credentials profile | +| profile | string | Cloudwatch | Optional credentials profile | | tsdbVersion | string | OpenTSDB | Version | | tsdbResolution | string | OpenTSDB | Resolution | | sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' | diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 8e2a23a872e..b12ac226616 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -31,23 +31,28 @@ build dashboards or use Explore with CloudWatch metrics and CloudWatch Logs. | _Default_ | Default data source means that it will be pre-selected for new panels. | | _Default Region_ | Used in query editor to set region (can be changed on per query basis) | | _Custom Metrics namespace_ | Specify the CloudWatch namespace of Custom metrics | -| _Auth Provider_ | Specify the provider to get credentials. | -| _Credentials_ profile name | Specify the name of the profile to use (if you use `~/.aws/credentials` file), leave blank for default. | -| _Assume Role Arn_ | Specify the ARN of the role to assume | +| _Authentication Provider_ | Specify the authentication method. | +| _Credentials Profile Name_ | If you use "Credentials file" for _Authentication Provider_, optionally specify a non-default profile. | +| _Assume Role ARN_ | Optionally specify the ARN of a role to assume. | | _External ID_ | If you are assuming a role in another account, that has been created with an external ID, specify the external ID here. | ## Authentication -### IAM Roles +### AWS credentials -Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If your Grafana -server is running on AWS you can use IAM Roles and authentication will be handled automatically. +There are three different authentication methods available. `AWS SDK Default` performs no custom configuration at all and instead uses the [default provider](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html) as specified by the AWS SDK for Go. This requires you to configure your AWS credentials separately, such as if you've [configured the CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html), if you're [running on an EC2 instance](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html), [in an ECS task](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) or for a [Service Account in a Kubernetes cluster](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). + +`Credentials file` corresponds directly to the [SharedCredentialsProvider](https://docs.aws.amazon.com/sdk-for-go/api/aws/credentials/#SharedCredentialsProvider) provider in the Go SDK. In short, it will read the AWS shared credentials file and find the given profile. While `AWS SDK Default` will also find the shared credentials file, this option allows you to specify which profile to use without using environment variables. It doesn't have any implicit fallbacks to other credential providers, and will fail if using credentials from the credentials file doesn't work. + +`Access & secret key` corresponds to the [StaticProvider](https://docs.aws.amazon.com/sdk-for-go/api/aws/credentials/#StaticProvider) and uses the given access key ID and secret key to authenticate. This method doesn't have any fallbacks, and will fail if the provided key pair doesn't work. + +### IAM roles + +Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. Providing you have chosen the _AWS SDK Default_ authentication method, and your Grafana server is running on AWS, you can use IAM Roles to handle authentication automically. See the AWS documentation on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) -> **Note:** [AWS Role Switching](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-cli.html) is not supported at the moment. - -## IAM Policies +### IAM policies Grafana needs permissions granted via IAM to be able to read CloudWatch metrics and EC2 tags/instances/regions. You can attach these permissions to IAM roles and @@ -101,22 +106,26 @@ Here is a minimal policy example: } ``` -### AWS credentials +### Assuming a role -If Auth Provider is `Credentials file`, Grafana tries to get credentials in the following order. +The `Assume Role ARN` field allows you to specify which IAM role to assume, if any. When left blank, the provided credentials are used directly and the associated role or user should have the required permissions. If this field is non-blank, on the other hand, the provided credentials are used to perform an [sts:AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) call. -- Environment variables. (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) -- Hard-code credentials. -- Shared credentials file. -- IAM role for Amazon EC2. +### EKS IAM roles for service accounts -See the AWS documentation on [Configuring the AWS SDK for Go](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html) +The Grafana process in the container runs as user 472 (called "grafana"). When Kubernetes mounts your projected credentials, they will by default only be available to the root user. In order to allow user 472 to access the credentials (and avoid it falling back to the IAM role attached to the EC2 instance), you will need to provide a [security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) for your pod. + +``` +securityContext: + fsGroup: 472 + runAsUser: 472 + runAsGroup: 472 +``` ### AWS credentials file Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server. -> **Note:** If you think you have the credentials file in the right place but it is still not working then you might try moving your .aws file to '/usr/share/grafana/' and make sure your credentials file has at most 0644 permissions. +> **Note:** If you think you have the credentials file in the right place and it is still not working, you might try moving your .aws file to '/usr/share/grafana/' and make sure your credentials file has at most 0644 permissions. Example content: @@ -372,13 +381,25 @@ It's now possible to configure data sources using config files with Grafana's pr Here are some provisioning examples for this data source. +### Using AWS SDK Default + +```yaml +apiVersion: 1 +datasources: + - name: CloudWatch + type: cloudwatch + jsonData: + authType: default + defaultRegion: eu-west-2 +``` + ### Using credentials profile name (non-default) ```yaml apiVersion: 1 datasources: - - name: Cloudwatch + - name: CloudWatch type: cloudwatch jsonData: authType: credentials @@ -393,7 +414,7 @@ datasources: apiVersion: 1 datasources: - - name: Cloudwatch + - name: CloudWatch type: cloudwatch jsonData: authType: keys @@ -402,3 +423,16 @@ datasources: accessKey: '' secretKey: '' ``` + +### Using AWS SDK Default and ARN of IAM Role to Assume + +```yaml +apiVersion: 1 +datasources: + - name: CloudWatch + type: cloudwatch + jsonData: + authType: default + assumeRoleArn: arn:aws:iam::123456789012:root + defaultRegion: eu-west-2 +``` diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index ad1c1d1c6fa..cfc06825092 100755 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -277,3 +277,22 @@ For existing alert notification channels, there is no automatic migration of sto > Please note that when migrating a notification channel and later downgrading Grafana to an earlier version, the notification channel will not be able to read stored sensitive settings and, as a result, not function as expected. For provisioning of alert notification channels, refer to [Alert notification channels]({{< relref "../administration/provisioning.md#alert-notification-channels" >}}). + +## Upgrading to v7.3 + +### AWS CloudWatch data source + +The AWS CloudWatch data source's authentication scheme has changed in Grafana 7.3. Most importantly the authentication method _ARN_ has been removed, and a new one has been added: _AWS SDK Default_. Existing data source configurations using the former will fallback to the latter. Assuming an IAM role will still work though, and the old _ARN_ method would use the default AWS SDK authentication method under the hood anyway. + +Since _ARN_ has been removed as an authentication method, we have instead made it into an option for providing the ARN of an IAM role to assume. This works independently of the authentication method you choose. + +The new authentication method, _AWS SDK Default_, uses the default AWS Go SDK credential chain, which at the time of writing looks for credentials in the following order: + +1. Environment variables. +1. Shared credentials file. +1. If your application uses an ECS task definition or RunTask API operation, IAM role for tasks. +1. If your application is running on an Amazon EC2 instance, IAM role for Amazon EC2. + +The other authentication methods, _Access & secret key_ and _Credentials file_, have changed in regards to fallbacks. If these methods fail, they no longer fallback to other methods. e.g. environment variables. If you want fallbacks, you should use _AWS SDK Default_ instead. + +For more information and details, please refer to [Using AWS CloudWatch in Grafana]({{< relref "../datasources/cloudwatch.md#authentication" >}}). diff --git a/go.mod b/go.mod index 5346541a3b9..a03b638b141 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,6 @@ require ( github.com/go-sql-driver/mysql v1.5.0 github.com/go-stack/stack v1.8.0 github.com/gobwas/glob v0.2.3 - github.com/golang/mock v1.4.3 github.com/golang/protobuf v1.4.2 github.com/google/go-cmp v0.5.0 github.com/gosimple/slug v1.4.2 diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 0fe05ad920f..7e3d3b7a185 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "regexp" + "strings" "sync" "time" @@ -11,6 +12,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/client" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatch" @@ -31,8 +34,8 @@ import ( type datasourceInfo struct { Profile string Region string - AuthType string - AssumeRoleArn string + AuthType authType + AssumeRoleARN string ExternalID string Namespace string @@ -74,16 +77,105 @@ type cloudWatchExecutor struct { func (e *cloudWatchExecutor) newSession(region string) (*session.Session, error) { dsInfo := e.getDSInfo(region) - creds, err := getCredentials(dsInfo) + + bldr := strings.Builder{} + for i, s := range []string{ + dsInfo.AuthType.String(), dsInfo.AccessKey, dsInfo.Profile, dsInfo.AssumeRoleARN, region, + } { + if i != 0 { + bldr.WriteString(":") + } + bldr.WriteString(strings.ReplaceAll(s, ":", `\:`)) + } + cacheKey := bldr.String() + + sessCacheLock.RLock() + if env, ok := sessCache[cacheKey]; ok { + if env.expiration.After(time.Now().UTC()) { + sessCacheLock.RUnlock() + return env.session, nil + } + } + sessCacheLock.RUnlock() + + cfgs := []*aws.Config{ + { + CredentialsChainVerboseErrors: aws.Bool(true), + }, + } + + var regionCfg *aws.Config + if dsInfo.Region == defaultRegion { + plog.Warn("Region is set to \"default\", which is unsupported") + dsInfo.Region = "" + } + if dsInfo.Region != "" { + regionCfg = &aws.Config{Region: aws.String(dsInfo.Region)} + cfgs = append(cfgs, regionCfg) + } + + switch dsInfo.AuthType { + case authTypeSharedCreds: + plog.Debug("Authenticating towards AWS with shared credentials", "profile", dsInfo.Profile, + "region", dsInfo.Region) + cfgs = append(cfgs, &aws.Config{ + Credentials: credentials.NewSharedCredentials("", dsInfo.Profile), + }) + case authTypeKeys: + plog.Debug("Authenticating towards AWS with an access key pair", "region", dsInfo.Region) + cfgs = append(cfgs, &aws.Config{ + Credentials: credentials.NewStaticCredentials(dsInfo.AccessKey, dsInfo.SecretKey, ""), + }) + case authTypeDefault: + plog.Debug("Authenticating towards AWS with default SDK method", "region", dsInfo.Region) + default: + panic(fmt.Sprintf("Unrecognized authType: %d", dsInfo.AuthType)) + } + sess, err := newSession(cfgs...) if err != nil { return nil, err } - cfg := &aws.Config{ - Region: aws.String(dsInfo.Region), - Credentials: creds, + duration := stscreds.DefaultDuration + expiration := time.Now().Add(duration) + if dsInfo.AssumeRoleARN != "" { + // We should assume a role in AWS + plog.Debug("Trying to assume role in AWS", "arn", dsInfo.AssumeRoleARN) + + cfgs := []*aws.Config{ + { + CredentialsChainVerboseErrors: aws.Bool(true), + }, + { + Credentials: newSTSCredentials(sess, dsInfo.AssumeRoleARN, func(p *stscreds.AssumeRoleProvider) { + // Not sure if this is necessary, overlaps with p.Duration and is undocumented + p.Expiry.SetExpiration(expiration, 0) + p.Duration = duration + if dsInfo.ExternalID != "" { + p.ExternalID = aws.String(dsInfo.ExternalID) + } + }), + }, + } + if regionCfg != nil { + cfgs = append(cfgs, regionCfg) + } + sess, err = newSession(cfgs...) + if err != nil { + return nil, err + } } - return newSession(cfg) + + plog.Debug("Successfully created AWS session") + + sessCacheLock.Lock() + sessCache[cacheKey] = envelope{ + session: sess, + expiration: expiration, + } + sessCacheLock.Unlock() + + return sess, nil } func (e *cloudWatchExecutor) getCWClient(region string) (cloudwatchiface.CloudWatchAPI, error) { @@ -282,18 +374,54 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, queryCont return response, nil } +type authType int + +const ( + authTypeDefault authType = iota + authTypeSharedCreds + authTypeKeys +) + +func (at authType) String() string { + switch at { + case authTypeDefault: + return "default" + case authTypeSharedCreds: + return "sharedCreds" + case authTypeKeys: + return "keys" + default: + panic(fmt.Sprintf("Unrecognized auth type %d", at)) + } +} + func (e *cloudWatchExecutor) getDSInfo(region string) *datasourceInfo { if region == defaultRegion { region = e.DataSource.JsonData.Get("defaultRegion").MustString() } - authType := e.DataSource.JsonData.Get("authType").MustString() - assumeRoleArn := e.DataSource.JsonData.Get("assumeRoleArn").MustString() + atStr := e.DataSource.JsonData.Get("authType").MustString() + assumeRoleARN := e.DataSource.JsonData.Get("assumeRoleArn").MustString() externalID := e.DataSource.JsonData.Get("externalId").MustString() decrypted := e.DataSource.DecryptedValues() accessKey := decrypted["accessKey"] secretKey := decrypted["secretKey"] + at := authTypeDefault + switch atStr { + case "credentials": + at = authTypeSharedCreds + case "keys": + at = authTypeKeys + case "default": + at = authTypeDefault + case "arn": + at = authTypeDefault + plog.Warn("Authentication type \"arn\" is deprecated, falling back to default") + default: + plog.Warn("Unrecognized AWS authentication type", "type", atStr) + } + profile := e.DataSource.JsonData.Get("profile").MustString() if profile == "" { profile = e.DataSource.Database // legacy support @@ -302,8 +430,8 @@ func (e *cloudWatchExecutor) getDSInfo(region string) *datasourceInfo { return &datasourceInfo{ Region: region, Profile: profile, - AuthType: authType, - AssumeRoleArn: assumeRoleArn, + AuthType: at, + AssumeRoleARN: assumeRoleARN, ExternalID: externalID, AccessKey: accessKey, SecretKey: secretKey, diff --git a/pkg/tsdb/cloudwatch/credentials.go b/pkg/tsdb/cloudwatch/credentials.go deleted file mode 100644 index 8689983dc7c..00000000000 --- a/pkg/tsdb/cloudwatch/credentials.go +++ /dev/null @@ -1,176 +0,0 @@ -package cloudwatch - -import ( - "fmt" - "os" - "sync" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/client" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" - "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" - "github.com/aws/aws-sdk-go/aws/defaults" - "github.com/aws/aws-sdk-go/aws/ec2metadata" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" - "github.com/aws/aws-sdk-go/service/sts/stsiface" -) - -type envelope struct { - credentials *credentials.Credentials - expiration *time.Time -} - -var awsCredsCache = map[string]envelope{} -var credsCacheLock sync.RWMutex - -// Session factory. -// Stubbable by tests. -//nolint:gocritic -var newSession = func(cfgs ...*aws.Config) (*session.Session, error) { - return session.NewSession(cfgs...) -} - -// STS service factory. -// Stubbable by tests. -//nolint:gocritic -var newSTSService = func(p client.ConfigProvider, cfgs ...*aws.Config) stsiface.STSAPI { - return sts.New(p, cfgs...) -} - -// EC2Metadata service factory. -// Stubbable by tests. -//nolint:gocritic -var newEC2Metadata = func(p client.ConfigProvider, cfgs ...*aws.Config) *ec2metadata.EC2Metadata { - return ec2metadata.New(p, cfgs...) -} - -func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) { - cacheKey := fmt.Sprintf("%s:%s:%s:%s", dsInfo.AuthType, dsInfo.AccessKey, dsInfo.Profile, dsInfo.AssumeRoleArn) - credsCacheLock.RLock() - if env, ok := awsCredsCache[cacheKey]; ok { - if env.expiration != nil && env.expiration.After(time.Now().UTC()) { - result := env.credentials - credsCacheLock.RUnlock() - return result, nil - } - } - credsCacheLock.RUnlock() - - accessKeyID := "" - secretAccessKey := "" - sessionToken := "" - var expiration *time.Time = nil - if dsInfo.AuthType == "arn" { - params := &sts.AssumeRoleInput{ - RoleArn: aws.String(dsInfo.AssumeRoleArn), - RoleSessionName: aws.String("GrafanaSession"), - DurationSeconds: aws.Int64(900), - } - if dsInfo.ExternalID != "" { - params.ExternalId = aws.String(dsInfo.ExternalID) - } - - stsSess, err := newSession() - if err != nil { - return nil, err - } - stsCreds := credentials.NewChainCredentials( - []credentials.Provider{ - &credentials.EnvProvider{}, - &credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile}, - webIdentityProvider(stsSess), - remoteCredProvider(stsSess), - }) - stsConfig := &aws.Config{ - Region: aws.String(dsInfo.Region), - Credentials: stsCreds, - } - - sess, err := newSession(stsConfig) - if err != nil { - return nil, err - } - svc := newSTSService(sess, stsConfig) - resp, err := svc.AssumeRole(params) - if err != nil { - return nil, err - } - if resp.Credentials != nil { - accessKeyID = *resp.Credentials.AccessKeyId - secretAccessKey = *resp.Credentials.SecretAccessKey - sessionToken = *resp.Credentials.SessionToken - expiration = resp.Credentials.Expiration - } - } else { - now := time.Now() - e := now.Add(5 * time.Minute) - expiration = &e - } - - sess, err := newSession() - if err != nil { - return nil, err - } - creds := credentials.NewChainCredentials( - []credentials.Provider{ - &credentials.StaticProvider{Value: credentials.Value{ - AccessKeyID: accessKeyID, - SecretAccessKey: secretAccessKey, - SessionToken: sessionToken, - }}, - &credentials.EnvProvider{}, - &credentials.StaticProvider{Value: credentials.Value{ - AccessKeyID: dsInfo.AccessKey, - SecretAccessKey: dsInfo.SecretKey, - }}, - &credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile}, - webIdentityProvider(sess), - remoteCredProvider(sess), - }) - - credsCacheLock.Lock() - awsCredsCache[cacheKey] = envelope{ - credentials: creds, - expiration: expiration, - } - credsCacheLock.Unlock() - - return creds, nil -} - -func webIdentityProvider(sess client.ConfigProvider) credentials.Provider { - svc := newSTSService(sess) - - roleARN := os.Getenv("AWS_ROLE_ARN") - tokenFilepath := os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE") - roleSessionName := os.Getenv("AWS_ROLE_SESSION_NAME") - return stscreds.NewWebIdentityRoleProvider(svc, roleARN, roleSessionName, tokenFilepath) -} - -func remoteCredProvider(sess *session.Session) credentials.Provider { - ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") - - if len(ecsCredURI) > 0 { - return ecsCredProvider(sess, ecsCredURI) - } - return ec2RoleProvider(sess) -} - -func ecsCredProvider(sess *session.Session, uri string) credentials.Provider { - const host = `169.254.170.2` - - d := defaults.Get() - return endpointcreds.NewProviderClient( - *d.Config, - d.Handlers, - fmt.Sprintf("http://%s%s", host, uri), - func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute }) -} - -func ec2RoleProvider(sess client.ConfigProvider) credentials.Provider { - return &ec2rolecreds.EC2RoleProvider{Client: newEC2Metadata(sess), ExpiryWindow: 5 * time.Minute} -} diff --git a/pkg/tsdb/cloudwatch/credentials_test.go b/pkg/tsdb/cloudwatch/credentials_test.go deleted file mode 100644 index 785e8e035e3..00000000000 --- a/pkg/tsdb/cloudwatch/credentials_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package cloudwatch - -import ( - "os" - "testing" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/client" - "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" - "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds" - "github.com/aws/aws-sdk-go/aws/ec2metadata" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" - "github.com/aws/aws-sdk-go/service/sts/stsiface" - "github.com/golang/mock/gomock" - "github.com/grafana/grafana/pkg/tsdb/cloudwatch/mock_stsiface" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestECSCredProvider(t *testing.T) { - os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/abc/123") - t.Cleanup(func() { - os.Unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") - }) - - sess, err := session.NewSession() - require.NoError(t, err) - provider := remoteCredProvider(sess) - require.NotNil(t, provider) - - ecsProvider, ok := provider.(*endpointcreds.Provider) - require.NotNil(t, ecsProvider) - require.True(t, ok) - - assert.Equal(t, "http://169.254.170.2/abc/123", ecsProvider.Client.Endpoint) -} - -func TestDefaultEC2RoleProvider(t *testing.T) { - sess, err := session.NewSession() - require.NoError(t, err) - provider := remoteCredProvider(sess) - require.NotNil(t, provider) - - ec2Provider, ok := provider.(*ec2rolecreds.EC2RoleProvider) - require.NotNil(t, ec2Provider) - require.True(t, ok) -} - -func TestGetCredentials_ARNAuthType(t *testing.T) { - ctrl := gomock.NewController(t) - var stsMock *mock_stsiface.MockSTSAPI - - origNewSession := newSession - origNewSTSService := newSTSService - origNewEC2Metadata := newEC2Metadata - t.Cleanup(func() { - newSession = origNewSession - newSTSService = origNewSTSService - newEC2Metadata = origNewEC2Metadata - }) - newSession = func(cfgs ...*aws.Config) (*session.Session, error) { - return &session.Session{}, nil - } - newSTSService = func(p client.ConfigProvider, cfgs ...*aws.Config) stsiface.STSAPI { - return stsMock - } - newEC2Metadata = func(p client.ConfigProvider, cfgs ...*aws.Config) *ec2metadata.EC2Metadata { - return nil - } - - t.Run("Without external ID", func(t *testing.T) { - stsMock = mock_stsiface.NewMockSTSAPI(ctrl) - stsMock. - EXPECT(). - AssumeRole(gomock.Eq(&sts.AssumeRoleInput{ - RoleArn: aws.String(""), - DurationSeconds: aws.Int64(900), - RoleSessionName: aws.String("GrafanaSession"), - })). - Return(&sts.AssumeRoleOutput{ - Credentials: &sts.Credentials{ - AccessKeyId: aws.String("id"), - SecretAccessKey: aws.String("secret"), - SessionToken: aws.String("token"), - }, - }, nil). - Times(1) - - creds, err := getCredentials(&datasourceInfo{ - AuthType: "arn", - }) - require.NoError(t, err) - require.NotNil(t, creds) - }) - - t.Run("With external ID", func(t *testing.T) { - stsMock = mock_stsiface.NewMockSTSAPI(ctrl) - stsMock. - EXPECT(). - AssumeRole(gomock.Eq(&sts.AssumeRoleInput{ - RoleArn: aws.String(""), - DurationSeconds: aws.Int64(900), - RoleSessionName: aws.String("GrafanaSession"), - ExternalId: aws.String("external-id"), - })). - Return(&sts.AssumeRoleOutput{ - Credentials: &sts.Credentials{ - AccessKeyId: aws.String("id"), - SecretAccessKey: aws.String("secret"), - SessionToken: aws.String("token"), - }, - }, nil). - Times(1) - - creds, err := getCredentials(&datasourceInfo{ - AuthType: "arn", - ExternalID: "external-id", - }) - require.NoError(t, err) - require.NotNil(t, creds) - }) -} diff --git a/pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go b/pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go deleted file mode 100644 index f7937852a91..00000000000 --- a/pkg/tsdb/cloudwatch/mock_stsiface/stsapi.go +++ /dev/null @@ -1,436 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/aws/aws-sdk-go/service/sts/stsiface (interfaces: STSAPI) - -// Package mock_stsiface is a generated GoMock package. -package mock_stsiface - -import ( - context "context" - request "github.com/aws/aws-sdk-go/aws/request" - sts "github.com/aws/aws-sdk-go/service/sts" - gomock "github.com/golang/mock/gomock" - reflect "reflect" -) - -// MockSTSAPI is a mock of STSAPI interface -type MockSTSAPI struct { - ctrl *gomock.Controller - recorder *MockSTSAPIMockRecorder -} - -// MockSTSAPIMockRecorder is the mock recorder for MockSTSAPI -type MockSTSAPIMockRecorder struct { - mock *MockSTSAPI -} - -// NewMockSTSAPI creates a new mock instance -func NewMockSTSAPI(ctrl *gomock.Controller) *MockSTSAPI { - mock := &MockSTSAPI{ctrl: ctrl} - mock.recorder = &MockSTSAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockSTSAPI) EXPECT() *MockSTSAPIMockRecorder { - return m.recorder -} - -// AssumeRole mocks base method -func (m *MockSTSAPI) AssumeRole(arg0 *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssumeRole", arg0) - ret0, _ := ret[0].(*sts.AssumeRoleOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AssumeRole indicates an expected call of AssumeRole -func (mr *MockSTSAPIMockRecorder) AssumeRole(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRole", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRole), arg0) -} - -// AssumeRoleRequest mocks base method -func (m *MockSTSAPI) AssumeRoleRequest(arg0 *sts.AssumeRoleInput) (*request.Request, *sts.AssumeRoleOutput) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssumeRoleRequest", arg0) - ret0, _ := ret[0].(*request.Request) - ret1, _ := ret[1].(*sts.AssumeRoleOutput) - return ret0, ret1 -} - -// AssumeRoleRequest indicates an expected call of AssumeRoleRequest -func (mr *MockSTSAPIMockRecorder) AssumeRoleRequest(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleRequest), arg0) -} - -// AssumeRoleWithContext mocks base method -func (m *MockSTSAPI) AssumeRoleWithContext(arg0 context.Context, arg1 *sts.AssumeRoleInput, arg2 ...request.Option) (*sts.AssumeRoleOutput, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "AssumeRoleWithContext", varargs...) - ret0, _ := ret[0].(*sts.AssumeRoleOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AssumeRoleWithContext indicates an expected call of AssumeRoleWithContext -func (mr *MockSTSAPIMockRecorder) AssumeRoleWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithContext), varargs...) -} - -// AssumeRoleWithSAML mocks base method -func (m *MockSTSAPI) AssumeRoleWithSAML(arg0 *sts.AssumeRoleWithSAMLInput) (*sts.AssumeRoleWithSAMLOutput, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssumeRoleWithSAML", arg0) - ret0, _ := ret[0].(*sts.AssumeRoleWithSAMLOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AssumeRoleWithSAML indicates an expected call of AssumeRoleWithSAML -func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAML(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAML", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAML), arg0) -} - -// AssumeRoleWithSAMLRequest mocks base method -func (m *MockSTSAPI) AssumeRoleWithSAMLRequest(arg0 *sts.AssumeRoleWithSAMLInput) (*request.Request, *sts.AssumeRoleWithSAMLOutput) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssumeRoleWithSAMLRequest", arg0) - ret0, _ := ret[0].(*request.Request) - ret1, _ := ret[1].(*sts.AssumeRoleWithSAMLOutput) - return ret0, ret1 -} - -// AssumeRoleWithSAMLRequest indicates an expected call of AssumeRoleWithSAMLRequest -func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAMLRequest(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAMLRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAMLRequest), arg0) -} - -// AssumeRoleWithSAMLWithContext mocks base method -func (m *MockSTSAPI) AssumeRoleWithSAMLWithContext(arg0 context.Context, arg1 *sts.AssumeRoleWithSAMLInput, arg2 ...request.Option) (*sts.AssumeRoleWithSAMLOutput, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "AssumeRoleWithSAMLWithContext", varargs...) - ret0, _ := ret[0].(*sts.AssumeRoleWithSAMLOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AssumeRoleWithSAMLWithContext indicates an expected call of AssumeRoleWithSAMLWithContext -func (mr *MockSTSAPIMockRecorder) AssumeRoleWithSAMLWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithSAMLWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithSAMLWithContext), varargs...) -} - -// AssumeRoleWithWebIdentity mocks base method -func (m *MockSTSAPI) AssumeRoleWithWebIdentity(arg0 *sts.AssumeRoleWithWebIdentityInput) (*sts.AssumeRoleWithWebIdentityOutput, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentity", arg0) - ret0, _ := ret[0].(*sts.AssumeRoleWithWebIdentityOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AssumeRoleWithWebIdentity indicates an expected call of AssumeRoleWithWebIdentity -func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentity(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentity", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentity), arg0) -} - -// AssumeRoleWithWebIdentityRequest mocks base method -func (m *MockSTSAPI) AssumeRoleWithWebIdentityRequest(arg0 *sts.AssumeRoleWithWebIdentityInput) (*request.Request, *sts.AssumeRoleWithWebIdentityOutput) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentityRequest", arg0) - ret0, _ := ret[0].(*request.Request) - ret1, _ := ret[1].(*sts.AssumeRoleWithWebIdentityOutput) - return ret0, ret1 -} - -// AssumeRoleWithWebIdentityRequest indicates an expected call of AssumeRoleWithWebIdentityRequest -func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentityRequest(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentityRequest", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentityRequest), arg0) -} - -// AssumeRoleWithWebIdentityWithContext mocks base method -func (m *MockSTSAPI) AssumeRoleWithWebIdentityWithContext(arg0 context.Context, arg1 *sts.AssumeRoleWithWebIdentityInput, arg2 ...request.Option) (*sts.AssumeRoleWithWebIdentityOutput, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "AssumeRoleWithWebIdentityWithContext", varargs...) - ret0, _ := ret[0].(*sts.AssumeRoleWithWebIdentityOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AssumeRoleWithWebIdentityWithContext indicates an expected call of AssumeRoleWithWebIdentityWithContext -func (mr *MockSTSAPIMockRecorder) AssumeRoleWithWebIdentityWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssumeRoleWithWebIdentityWithContext", reflect.TypeOf((*MockSTSAPI)(nil).AssumeRoleWithWebIdentityWithContext), varargs...) -} - -// DecodeAuthorizationMessage mocks base method -func (m *MockSTSAPI) DecodeAuthorizationMessage(arg0 *sts.DecodeAuthorizationMessageInput) (*sts.DecodeAuthorizationMessageOutput, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DecodeAuthorizationMessage", arg0) - ret0, _ := ret[0].(*sts.DecodeAuthorizationMessageOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DecodeAuthorizationMessage indicates an expected call of DecodeAuthorizationMessage -func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessage(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessage", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessage), arg0) -} - -// DecodeAuthorizationMessageRequest mocks base method -func (m *MockSTSAPI) DecodeAuthorizationMessageRequest(arg0 *sts.DecodeAuthorizationMessageInput) (*request.Request, *sts.DecodeAuthorizationMessageOutput) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DecodeAuthorizationMessageRequest", arg0) - ret0, _ := ret[0].(*request.Request) - ret1, _ := ret[1].(*sts.DecodeAuthorizationMessageOutput) - return ret0, ret1 -} - -// DecodeAuthorizationMessageRequest indicates an expected call of DecodeAuthorizationMessageRequest -func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessageRequest(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessageRequest", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessageRequest), arg0) -} - -// DecodeAuthorizationMessageWithContext mocks base method -func (m *MockSTSAPI) DecodeAuthorizationMessageWithContext(arg0 context.Context, arg1 *sts.DecodeAuthorizationMessageInput, arg2 ...request.Option) (*sts.DecodeAuthorizationMessageOutput, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "DecodeAuthorizationMessageWithContext", varargs...) - ret0, _ := ret[0].(*sts.DecodeAuthorizationMessageOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DecodeAuthorizationMessageWithContext indicates an expected call of DecodeAuthorizationMessageWithContext -func (mr *MockSTSAPIMockRecorder) DecodeAuthorizationMessageWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeAuthorizationMessageWithContext", reflect.TypeOf((*MockSTSAPI)(nil).DecodeAuthorizationMessageWithContext), varargs...) -} - -// GetAccessKeyInfo mocks base method -func (m *MockSTSAPI) GetAccessKeyInfo(arg0 *sts.GetAccessKeyInfoInput) (*sts.GetAccessKeyInfoOutput, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccessKeyInfo", arg0) - ret0, _ := ret[0].(*sts.GetAccessKeyInfoOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAccessKeyInfo indicates an expected call of GetAccessKeyInfo -func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfo(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfo", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfo), arg0) -} - -// GetAccessKeyInfoRequest mocks base method -func (m *MockSTSAPI) GetAccessKeyInfoRequest(arg0 *sts.GetAccessKeyInfoInput) (*request.Request, *sts.GetAccessKeyInfoOutput) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccessKeyInfoRequest", arg0) - ret0, _ := ret[0].(*request.Request) - ret1, _ := ret[1].(*sts.GetAccessKeyInfoOutput) - return ret0, ret1 -} - -// GetAccessKeyInfoRequest indicates an expected call of GetAccessKeyInfoRequest -func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfoRequest(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfoRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfoRequest), arg0) -} - -// GetAccessKeyInfoWithContext mocks base method -func (m *MockSTSAPI) GetAccessKeyInfoWithContext(arg0 context.Context, arg1 *sts.GetAccessKeyInfoInput, arg2 ...request.Option) (*sts.GetAccessKeyInfoOutput, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetAccessKeyInfoWithContext", varargs...) - ret0, _ := ret[0].(*sts.GetAccessKeyInfoOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAccessKeyInfoWithContext indicates an expected call of GetAccessKeyInfoWithContext -func (mr *MockSTSAPIMockRecorder) GetAccessKeyInfoWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessKeyInfoWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetAccessKeyInfoWithContext), varargs...) -} - -// GetCallerIdentity mocks base method -func (m *MockSTSAPI) GetCallerIdentity(arg0 *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCallerIdentity", arg0) - ret0, _ := ret[0].(*sts.GetCallerIdentityOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCallerIdentity indicates an expected call of GetCallerIdentity -func (mr *MockSTSAPIMockRecorder) GetCallerIdentity(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentity", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentity), arg0) -} - -// GetCallerIdentityRequest mocks base method -func (m *MockSTSAPI) GetCallerIdentityRequest(arg0 *sts.GetCallerIdentityInput) (*request.Request, *sts.GetCallerIdentityOutput) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCallerIdentityRequest", arg0) - ret0, _ := ret[0].(*request.Request) - ret1, _ := ret[1].(*sts.GetCallerIdentityOutput) - return ret0, ret1 -} - -// GetCallerIdentityRequest indicates an expected call of GetCallerIdentityRequest -func (mr *MockSTSAPIMockRecorder) GetCallerIdentityRequest(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentityRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentityRequest), arg0) -} - -// GetCallerIdentityWithContext mocks base method -func (m *MockSTSAPI) GetCallerIdentityWithContext(arg0 context.Context, arg1 *sts.GetCallerIdentityInput, arg2 ...request.Option) (*sts.GetCallerIdentityOutput, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetCallerIdentityWithContext", varargs...) - ret0, _ := ret[0].(*sts.GetCallerIdentityOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCallerIdentityWithContext indicates an expected call of GetCallerIdentityWithContext -func (mr *MockSTSAPIMockRecorder) GetCallerIdentityWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentityWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetCallerIdentityWithContext), varargs...) -} - -// GetFederationToken mocks base method -func (m *MockSTSAPI) GetFederationToken(arg0 *sts.GetFederationTokenInput) (*sts.GetFederationTokenOutput, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFederationToken", arg0) - ret0, _ := ret[0].(*sts.GetFederationTokenOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetFederationToken indicates an expected call of GetFederationToken -func (mr *MockSTSAPIMockRecorder) GetFederationToken(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationToken", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationToken), arg0) -} - -// GetFederationTokenRequest mocks base method -func (m *MockSTSAPI) GetFederationTokenRequest(arg0 *sts.GetFederationTokenInput) (*request.Request, *sts.GetFederationTokenOutput) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFederationTokenRequest", arg0) - ret0, _ := ret[0].(*request.Request) - ret1, _ := ret[1].(*sts.GetFederationTokenOutput) - return ret0, ret1 -} - -// GetFederationTokenRequest indicates an expected call of GetFederationTokenRequest -func (mr *MockSTSAPIMockRecorder) GetFederationTokenRequest(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationTokenRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationTokenRequest), arg0) -} - -// GetFederationTokenWithContext mocks base method -func (m *MockSTSAPI) GetFederationTokenWithContext(arg0 context.Context, arg1 *sts.GetFederationTokenInput, arg2 ...request.Option) (*sts.GetFederationTokenOutput, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetFederationTokenWithContext", varargs...) - ret0, _ := ret[0].(*sts.GetFederationTokenOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetFederationTokenWithContext indicates an expected call of GetFederationTokenWithContext -func (mr *MockSTSAPIMockRecorder) GetFederationTokenWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFederationTokenWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetFederationTokenWithContext), varargs...) -} - -// GetSessionToken mocks base method -func (m *MockSTSAPI) GetSessionToken(arg0 *sts.GetSessionTokenInput) (*sts.GetSessionTokenOutput, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSessionToken", arg0) - ret0, _ := ret[0].(*sts.GetSessionTokenOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSessionToken indicates an expected call of GetSessionToken -func (mr *MockSTSAPIMockRecorder) GetSessionToken(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionToken", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionToken), arg0) -} - -// GetSessionTokenRequest mocks base method -func (m *MockSTSAPI) GetSessionTokenRequest(arg0 *sts.GetSessionTokenInput) (*request.Request, *sts.GetSessionTokenOutput) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSessionTokenRequest", arg0) - ret0, _ := ret[0].(*request.Request) - ret1, _ := ret[1].(*sts.GetSessionTokenOutput) - return ret0, ret1 -} - -// GetSessionTokenRequest indicates an expected call of GetSessionTokenRequest -func (mr *MockSTSAPIMockRecorder) GetSessionTokenRequest(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionTokenRequest", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionTokenRequest), arg0) -} - -// GetSessionTokenWithContext mocks base method -func (m *MockSTSAPI) GetSessionTokenWithContext(arg0 context.Context, arg1 *sts.GetSessionTokenInput, arg2 ...request.Option) (*sts.GetSessionTokenOutput, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetSessionTokenWithContext", varargs...) - ret0, _ := ret[0].(*sts.GetSessionTokenOutput) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSessionTokenWithContext indicates an expected call of GetSessionTokenWithContext -func (mr *MockSTSAPIMockRecorder) GetSessionTokenWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSessionTokenWithContext", reflect.TypeOf((*MockSTSAPI)(nil).GetSessionTokenWithContext), varargs...) -} diff --git a/pkg/tsdb/cloudwatch/session.go b/pkg/tsdb/cloudwatch/session.go new file mode 100644 index 00000000000..e93c25aa05c --- /dev/null +++ b/pkg/tsdb/cloudwatch/session.go @@ -0,0 +1,36 @@ +package cloudwatch + +import ( + "sync" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" +) + +type envelope struct { + session *session.Session + expiration time.Time +} + +var sessCache = map[string]envelope{} +var sessCacheLock sync.RWMutex + +// Session factory. +// Stubbable by tests. +//nolint:gocritic +var newSession = func(cfgs ...*aws.Config) (*session.Session, error) { + return session.NewSession(cfgs...) +} + +// STS credentials factory. +// Stubbable by tests. +//nolint:gocritic +var newSTSCredentials = stscreds.NewCredentials + +// EC2Metadata service factory. +// Stubbable by tests. +//nolint:gocritic +var newEC2Metadata = ec2metadata.New diff --git a/pkg/tsdb/cloudwatch/session_test.go b/pkg/tsdb/cloudwatch/session_test.go new file mode 100644 index 00000000000..8dac32b1bad --- /dev/null +++ b/pkg/tsdb/cloudwatch/session_test.go @@ -0,0 +1,107 @@ +package cloudwatch + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/client" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test cloudWatchExecutor.newSession with assumption of IAM role. +func TestNewSession_AssumeRole(t *testing.T) { + origNewSession := newSession + origNewSTSCredentials := newSTSCredentials + origNewEC2Metadata := newEC2Metadata + t.Cleanup(func() { + newSession = origNewSession + newSTSCredentials = origNewSTSCredentials + newEC2Metadata = origNewEC2Metadata + }) + newSession = func(cfgs ...*aws.Config) (*session.Session, error) { + cfg := aws.Config{} + cfg.MergeIn(cfgs...) + return &session.Session{ + Config: &cfg, + }, nil + } + newSTSCredentials = func(c client.ConfigProvider, roleARN string, + options ...func(*stscreds.AssumeRoleProvider)) *credentials.Credentials { + p := &stscreds.AssumeRoleProvider{ + RoleARN: roleARN, + } + for _, o := range options { + o(p) + } + + return credentials.NewCredentials(p) + } + newEC2Metadata = func(p client.ConfigProvider, cfgs ...*aws.Config) *ec2metadata.EC2Metadata { + return nil + } + + duration := stscreds.DefaultDuration + + t.Run("Without external ID", func(t *testing.T) { + t.Cleanup(func() { + sessCache = map[string]envelope{} + }) + + const roleARN = "test" + + e := newExecutor() + e.DataSource = fakeDataSource(fakeDataSourceCfg{ + assumeRoleARN: roleARN, + }) + + sess, err := e.newSession(defaultRegion) + require.NoError(t, err) + require.NotNil(t, sess) + + expCreds := credentials.NewCredentials(&stscreds.AssumeRoleProvider{ + RoleARN: roleARN, + Duration: duration, + }) + diff := cmp.Diff(expCreds, sess.Config.Credentials, cmp.Exporter(func(_ reflect.Type) bool { + return true + }), cmpopts.IgnoreFields(stscreds.AssumeRoleProvider{}, "Expiry")) + assert.Empty(t, diff) + }) + + t.Run("With external ID", func(t *testing.T) { + t.Cleanup(func() { + sessCache = map[string]envelope{} + }) + + const roleARN = "test" + const externalID = "external" + + e := newExecutor() + e.DataSource = fakeDataSource(fakeDataSourceCfg{ + assumeRoleARN: roleARN, + externalID: externalID, + }) + + sess, err := e.newSession(defaultRegion) + require.NoError(t, err) + require.NotNil(t, sess) + + expCreds := credentials.NewCredentials(&stscreds.AssumeRoleProvider{ + RoleARN: roleARN, + ExternalID: aws.String(externalID), + Duration: duration, + }) + diff := cmp.Diff(expCreds, sess.Config.Credentials, cmp.Exporter(func(_ reflect.Type) bool { + return true + }), cmpopts.IgnoreFields(stscreds.AssumeRoleProvider{}, "Expiry")) + assert.Empty(t, diff) + }) +} diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index 9f81e0b0207..7536634a2b5 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -18,9 +18,23 @@ import ( "github.com/grafana/grafana/pkg/models" ) -func fakeDataSource() *models.DataSource { +type fakeDataSourceCfg struct { + assumeRoleARN string + externalID string +} + +func fakeDataSource(cfgs ...fakeDataSourceCfg) *models.DataSource { jsonData := simplejson.New() - jsonData.Set("defaultRegion", "default") + jsonData.Set("defaultRegion", defaultRegion) + jsonData.Set("authType", "default") + for _, cfg := range cfgs { + if cfg.assumeRoleARN != "" { + jsonData.Set("assumeRoleArn", cfg.assumeRoleARN) + } + if cfg.externalID != "" { + jsonData.Set("externalId", cfg.externalID) + } + } return &models.DataSource{ Id: 1, Database: "default", diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx index 5f510208a8b..8fa5de1c4c5 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx @@ -78,7 +78,7 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); - it('should should show credentials profile name field', () => { + it('should show credentials profile name field', () => { const wrapper = setup({ jsonData: { authType: 'credentials', @@ -87,7 +87,7 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); - it('should should show access key and secret access key fields', () => { + it('should show access key and secret access key fields', () => { const wrapper = setup({ jsonData: { authType: 'keys', @@ -96,7 +96,7 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); - it('should should show arn role field', () => { + it('should show arn role field', () => { const wrapper = setup({ jsonData: { authType: 'arn', diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx index 7a3c1a9d72d..26cefc39785 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import { InlineFormLabel, LegacyForms, Button } from '@grafana/ui'; const { Select, Input } = LegacyForms; import { + AppEvents, DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOptionSelect, onUpdateDatasourceResetOption, @@ -13,11 +14,12 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { CloudWatchDatasource } from '../datasource'; import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types'; import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; +import { appEvents } from 'app/core/core'; const authProviderOptions = [ + { label: 'AWS SDK Default', value: 'default' }, { label: 'Access & secret key', value: 'keys' }, { label: 'Credentials file', value: 'credentials' }, - { label: 'ARN', value: 'arn' }, ] as SelectableValue[]; export type Props = DataSourcePluginOptionsEditorProps; @@ -44,6 +46,22 @@ export class ConfigEditor extends PureComponent { console.warn('Cloud Watch ConfigEditor has unmounted, initialization was canceled'); } }); + + if (this.props.options.jsonData.authType === 'arn') { + appEvents.emit(AppEvents.alertWarning, [ + 'Since grafana 7.3 authentication type "arn" is deprecated, falling back to default SDK provider', + ]); + } else if ( + this.props.options.jsonData.authType === 'credentials' && + !this.props.options.jsonData.profile && + !this.props.options.jsonData.database + ) { + appEvents.emit(AppEvents.alertWarning, [ + 'As of grafana 7.3 authentication type "credentials" should be used only for shared file credentials. \ + If you don\'t have a credentials file, switch to the default SDK provider for extracting credentials \ + from environment variables or IAM roles', + ]); + } } componentWillUnmount() { @@ -125,17 +143,18 @@ export class ConfigEditor extends PureComponent {
- Auth Provider + + Authentication Provider + -
+
+
+ + Assume Role ARN + +
+
+
+
{
- )} +
- Auth Provider + Authentication Provider +
+
+
+
+ + External ID + +
+ +
+
+
+
@@ -198,8 +247,9 @@ exports[`Render should render component 1`] = ` > - Auth Provider + Authentication Provider +
+ +
+
+ + External ID + +
+ +
+
+
+
@@ -360,7 +458,7 @@ exports[`Render should render component 1`] = ` `; -exports[`Render should should show access key and secret access key fields 1`] = ` +exports[`Render should show access key and secret access key fields 1`] = `

- Auth Provider + Authentication Provider +

+ +
+
+ + External ID + +
+ +
+
+
+
@@ -540,7 +687,7 @@ exports[`Render should should show access key and secret access key fields 1`] = `; -exports[`Render should should show arn role field 1`] = ` +exports[`Render should show arn role field 1`] = `

- Auth Provider + Authentication Provider +

+ +
+
+ + External ID + +
+ +
+
+
+
@@ -720,7 +916,7 @@ exports[`Render should should show arn role field 1`] = ` `; -exports[`Render should should show credentials profile name field 1`] = ` +exports[`Render should show credentials profile name field 1`] = `

- Auth Provider + Authentication Provider +

+ +
+
+ + External ID + +
+ +
+
+
+