From 0be2177d15d0b7c287115a56183d42ab624a2915 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 10 Mar 2021 08:59:19 +0100 Subject: [PATCH] =?UTF-8?q?CloudWatch:=20Restrict=20auth=20provider=20and?= =?UTF-8?q?=20assume=20role=20usage=20according=20to=E2=80=A6=20(#31845)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CloudWatch: Restrict auth provider and assume role usage according to Grafana configuration (#31805) * restrict usage of providers and assume role according to grafana config * update docs * Update docs/sources/datasources/cloudwatch.md Co-authored-by: Arve Knudsen * Update pkg/tsdb/cloudwatch/cloudwatch.go Co-authored-by: Arve Knudsen * Update pkg/tsdb/cloudwatch/session_test.go Co-authored-by: Arve Knudsen * pr feedback * fix failing test Co-authored-by: Arve Knudsen (cherry picked from commit 2d660ee5025e6c317e60abb96b8ffc0014165673) * Update docs/sources/datasources/cloudwatch.md Co-authored-by: Arve Knudsen Co-authored-by: Arve Knudsen --- conf/defaults.ini | 2 +- conf/sample.ini | 2 +- docs/sources/administration/configuration.md | 2 +- docs/sources/datasources/cloudwatch.md | 12 +++++ pkg/tsdb/cloudwatch/cloudwatch.go | 25 +++++++-- pkg/tsdb/cloudwatch/log_actions_test.go | 14 ++--- pkg/tsdb/cloudwatch/metric_find_query_test.go | 12 ++--- pkg/tsdb/cloudwatch/query_transformer_test.go | 3 +- pkg/tsdb/cloudwatch/session_test.go | 51 +++++++++++++++++-- pkg/tsdb/cloudwatch/test_utils.go | 5 ++ pkg/tsdb/cloudwatch/time_series_query_test.go | 2 +- 11 files changed, 105 insertions(+), 25 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index dc1d72d96f3..eac091fec67 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -508,7 +508,7 @@ active_sync_enabled = true #################################### AWS ########################### [aws] # Enter a comma-separated list of allowed AWS authentication providers. -# Options are: default (AWS SDK Default), keys (Access && secret key), credentials (Credentials field), ec2_IAM_role (EC2 IAM Role) +# Options are: default (AWS SDK Default), keys (Access && secret key), credentials (Credentials field), ec2_iam_role (EC2 IAM Role) allowed_auth_providers = default,keys,credentials # Allow AWS users to assume a role using temporary security credentials. diff --git a/conf/sample.ini b/conf/sample.ini index 15a543b482d..770210fc5b2 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -498,7 +498,7 @@ #################################### AWS ########################### [aws] # Enter a comma-separated list of allowed AWS authentication providers. -# Options are: default (AWS SDK Default), keys (Access && secret key), credentials (Credentials field), ec2_IAM_role (EC2 IAM Role) +# Options are: default (AWS SDK Default), keys (Access && secret key), credentials (Credentials field), ec2_iam_role (EC2 IAM Role) ; allowed_auth_providers = default,keys,credentials # Allow AWS users to assume a role using temporary security credentials. diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index e8c69550e8d..ba437792658 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -780,7 +780,7 @@ You can configure core and external AWS plugins. Specify what authentication providers the AWS plugins allow. For a list of allowed providers, refer to the data-source configuration page for a given plugin. If you configure a plugin by provisioning, only providers that are specified in `allowed_auth_providers` are allowed. -Options: `default` (AWS SDK default), `keys` (Access and secret key), `credentials` (Credentials file), `ec2_IAM_role` (EC2 IAM role) +Options: `default` (AWS SDK default), `keys` (Access and secret key), `credentials` (Credentials file), `ec2_iam_role` (EC2 IAM role) ### assume_role_enabled diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index ed9597e511f..229287e672b 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -375,6 +375,18 @@ To request a quota increase, visit the [AWS Service Quotas console](https://cons Please see the AWS documentation for [Service Quotas](https://docs.aws.amazon.com/servicequotas/latest/userguide/intro.html) and [CloudWatch limits](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_limits.html) for more information. +## Configure the data source with grafana.ini + +In the [Grafana configuration](https://grafana.com/docs/grafana/latest/administration/configuration/#aws) there's an `AWS` section that allows you to customize the data source. + +### allowed_auth_providers + +Specify which authentication providers are allowed for the CloudWatch data source. The following providers are enabled by default in OSS Grafana: `default` (AWS SDK default), keys (Access and secret key), credentials (Credentials file), ec2_iam_role (EC2 IAM role). + +### assume_role_enabled + +Allows you to disable `assume role (ARN)` in the CloudWatch data source. By default, assume role (ARN) is enabled for OSS Grafana. + ## Configure the data source with provisioning It's now possible to configure data sources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for data sources on the [provisioning docs page]({{< relref "../administration/provisioning/#datasources" >}}) diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index ac8f0ed949c..99e201fef77 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -64,21 +64,22 @@ func init() { type CloudWatchService struct { LogsService *LogsService `inject:""` + Cfg *setting.Cfg `inject:""` } func (s *CloudWatchService) Init() error { plog.Debug("initing") - tsdb.RegisterTsdbQueryEndpoint("cloudwatch", func(ds *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - return newExecutor(s.LogsService), nil + return newExecutor(s.LogsService, s.Cfg), nil }) return nil } -func newExecutor(logsService *LogsService) *cloudWatchExecutor { +func newExecutor(logsService *LogsService, cfg *setting.Cfg) *cloudWatchExecutor { return &cloudWatchExecutor{ logsService: logsService, + cfg: cfg, } } @@ -90,11 +91,27 @@ type cloudWatchExecutor struct { rgtaClient resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI logsService *LogsService + cfg *setting.Cfg } func (e *cloudWatchExecutor) newSession(region string) (*session.Session, error) { dsInfo := e.getDSInfo(region) + authTypeAllowed := false + for _, provider := range e.cfg.AWSAllowedAuthProviders { + if provider == dsInfo.AuthType.String() { + authTypeAllowed = true + break + } + } + if !authTypeAllowed { + return nil, fmt.Errorf("attempting to use an auth type that is not allowed: %q", dsInfo.AuthType.String()) + } + + if dsInfo.AssumeRoleARN != "" && !e.cfg.AWSAssumeRoleEnabled { + return nil, fmt.Errorf("attempting to use assume role (ARN) which is disabled in grafana.ini") + } + bldr := strings.Builder{} for i, s := range []string{ dsInfo.AuthType.String(), dsInfo.AccessKey, dsInfo.Profile, dsInfo.AssumeRoleARN, region, dsInfo.Endpoint, @@ -166,7 +183,7 @@ func (e *cloudWatchExecutor) newSession(region string) (*session.Session, error) duration := stscreds.DefaultDuration expiration := time.Now().UTC().Add(duration) - if dsInfo.AssumeRoleARN != "" { + if dsInfo.AssumeRoleARN != "" && e.cfg.AWSAssumeRoleEnabled { // We should assume a role in AWS plog.Debug("Trying to assume role in AWS", "arn", dsInfo.AssumeRoleARN) diff --git a/pkg/tsdb/cloudwatch/log_actions_test.go b/pkg/tsdb/cloudwatch/log_actions_test.go index 58290e11d4b..d25d09ca5d5 100644 --- a/pkg/tsdb/cloudwatch/log_actions_test.go +++ b/pkg/tsdb/cloudwatch/log_actions_test.go @@ -47,7 +47,7 @@ func TestQuery_DescribeLogGroups(t *testing.T) { }, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -100,7 +100,7 @@ func TestQuery_DescribeLogGroups(t *testing.T) { }, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -170,7 +170,7 @@ func TestQuery_GetLogGroupFields(t *testing.T) { const refID = "A" - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -249,7 +249,7 @@ func TestQuery_StartQuery(t *testing.T) { To: "1584700643000", } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) _, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ TimeRange: timeRange, Queries: []*tsdb.Query{ @@ -295,7 +295,7 @@ func TestQuery_StartQuery(t *testing.T) { To: "1584873443000", } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ TimeRange: timeRange, Queries: []*tsdb.Query{ @@ -371,7 +371,7 @@ func TestQuery_StopQuery(t *testing.T) { To: "1584700643000", } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ TimeRange: timeRange, Queries: []*tsdb.Query{ @@ -458,7 +458,7 @@ func TestQuery_GetQueryResults(t *testing.T) { }, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { diff --git a/pkg/tsdb/cloudwatch/metric_find_query_test.go b/pkg/tsdb/cloudwatch/metric_find_query_test.go index 0a710b8a419..d1a4bbec9f5 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query_test.go +++ b/pkg/tsdb/cloudwatch/metric_find_query_test.go @@ -44,7 +44,7 @@ func TestQuery_Metrics(t *testing.T) { }, }, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -101,7 +101,7 @@ func TestQuery_Metrics(t *testing.T) { }, }, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -163,7 +163,7 @@ func TestQuery_Regions(t *testing.T) { cli = fakeEC2Client{ regions: []string{regionName}, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -245,7 +245,7 @@ func TestQuery_InstanceAttributes(t *testing.T) { }, }, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -348,7 +348,7 @@ func TestQuery_EBSVolumeIDs(t *testing.T) { }, }, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -448,7 +448,7 @@ func TestQuery_ResourceARNs(t *testing.T) { }, }, } - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) resp, err := executor.Query(context.Background(), fakeDataSource(), &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { diff --git a/pkg/tsdb/cloudwatch/query_transformer_test.go b/pkg/tsdb/cloudwatch/query_transformer_test.go index 90f64498042..451ef9ec364 100644 --- a/pkg/tsdb/cloudwatch/query_transformer_test.go +++ b/pkg/tsdb/cloudwatch/query_transformer_test.go @@ -6,12 +6,13 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestQueryTransformer(t *testing.T) { - executor := newExecutor(nil) + executor := newExecutor(nil, &setting.Cfg{}) t.Run("One cloudwatchQuery is generated when its request query has one stat", func(t *testing.T) { requestQueries := []*requestQuery{ { diff --git a/pkg/tsdb/cloudwatch/session_test.go b/pkg/tsdb/cloudwatch/session_test.go index ef501128fbd..b240c1df3d8 100644 --- a/pkg/tsdb/cloudwatch/session_test.go +++ b/pkg/tsdb/cloudwatch/session_test.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -58,7 +59,7 @@ func TestNewSession_AssumeRole(t *testing.T) { const roleARN = "test" - e := newExecutor(nil) + e := newExecutor(nil, newTestConfig()) e.DataSource = fakeDataSource(fakeDataSourceCfg{ assumeRoleARN: roleARN, }) @@ -85,7 +86,7 @@ func TestNewSession_AssumeRole(t *testing.T) { const roleARN = "test" const externalID = "external" - e := newExecutor(nil) + e := newExecutor(nil, newTestConfig()) e.DataSource = fakeDataSource(fakeDataSourceCfg{ assumeRoleARN: roleARN, externalID: externalID, @@ -105,6 +106,50 @@ func TestNewSession_AssumeRole(t *testing.T) { }), cmpopts.IgnoreFields(stscreds.AssumeRoleProvider{}, "Expiry")) assert.Empty(t, diff) }) + + t.Run("Assume role not enabled", func(t *testing.T) { + t.Cleanup(func() { + sessCache = map[string]envelope{} + }) + + const roleARN = "test" + + e := newExecutor(nil, &setting.Cfg{AWSAllowedAuthProviders: []string{"default"}, AWSAssumeRoleEnabled: false}) + e.DataSource = fakeDataSource(fakeDataSourceCfg{ + assumeRoleARN: roleARN, + }) + + sess, err := e.newSession(defaultRegion) + require.Error(t, err) + require.Nil(t, sess) + + expectedError := "attempting to use assume role (ARN) which is disabled in grafana.ini" + assert.Equal(t, expectedError, err.Error()) + }) +} + +func TestNewSession_AllowedAuthProviders(t *testing.T) { + t.Run("Not allowed auth type is used", func(t *testing.T) { + e := newExecutor(nil, &setting.Cfg{AWSAllowedAuthProviders: []string{"keys"}}) + e.DataSource = fakeDataSource() + e.DataSource.JsonData.Set("authType", "default") + + sess, err := e.newSession(defaultRegion) + require.Error(t, err) + require.Nil(t, sess) + + assert.Equal(t, `attempting to use an auth type that is not allowed: "default"`, err.Error()) + }) + + t.Run("Allowed auth type is used", func(t *testing.T) { + e := newExecutor(nil, &setting.Cfg{AWSAllowedAuthProviders: []string{"keys"}}) + e.DataSource = fakeDataSource() + e.DataSource.JsonData.Set("authType", "keys") + + sess, err := e.newSession(defaultRegion) + require.NoError(t, err) + require.NotNil(t, sess) + }) } func TestNewSession_EC2IAMRole(t *testing.T) { @@ -123,7 +168,7 @@ func TestNewSession_EC2IAMRole(t *testing.T) { } t.Run("Credentials are created", func(t *testing.T) { - e := newExecutor(nil) + e := newExecutor(nil, &setting.Cfg{AWSAllowedAuthProviders: []string{"ec2_iam_role"}, AWSAssumeRoleEnabled: true}) e.DataSource = fakeDataSource() e.DataSource.JsonData.Set("authType", "ec2_iam_role") diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index 193aeddad9f..8f5725a4e90 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" ) type fakeDataSourceCfg struct { @@ -145,3 +146,7 @@ func (c fakeRGTAClient) GetResourcesPages(in *resourcegroupstaggingapi.GetResour }, true) return nil } + +func newTestConfig() *setting.Cfg { + return &setting.Cfg{AWSAllowedAuthProviders: []string{"default"}, AWSAssumeRoleEnabled: true} +} diff --git a/pkg/tsdb/cloudwatch/time_series_query_test.go b/pkg/tsdb/cloudwatch/time_series_query_test.go index 8afdf3bfa11..a8160aee37e 100644 --- a/pkg/tsdb/cloudwatch/time_series_query_test.go +++ b/pkg/tsdb/cloudwatch/time_series_query_test.go @@ -9,7 +9,7 @@ import ( ) func TestTimeSeriesQuery(t *testing.T) { - executor := newExecutor(nil) + executor := newExecutor(nil, newTestConfig()) t.Run("End time before start time should result in error", func(t *testing.T) { _, err := executor.executeTimeSeriesQuery(context.TODO(), &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-2h")})