AccessControl: Resolve attribute based scopes to id based scopes (#40742)

* AccessControl: POC scope attribute resolution

Refactor based on ScopeMutators

test errors and calls to cache

Add comments to tests

Rename logger

Create keywordMutator only once

* AccessControl: Add AttributeScopeResolver registration

Co-authored-by: gamab <gabriel.mabille@grafana.com>

* AccessControl: Add AttributeScopeResolver to datasources

Co-authored-by: gamab <gabriel.mabille@grafana.com>

* Test evaluation with translation

* fix imports

* AccessControl: Test attribute resolver

* Fix trailing white space

* Make ScopeResolver public for enterprise redefine

* Handle wildcard

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

Co-authored-by: jguer <joao.guerreiro@grafana.com>
This commit is contained in:
Gabriel MABILLE
2022-01-18 17:34:35 +01:00
committed by GitHub
co-authored by gamab Jguer
parent 7a622422a9
commit 54280fc9d7
14 changed files with 548 additions and 110 deletions
+35 -1
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
@@ -49,7 +51,7 @@ type cachedDecryptedJSON struct {
json map[string]string
}
func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, secretsService secrets.Service) *Service {
func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, secretsService secrets.Service, ac accesscontrol.AccessControl) *Service {
s := &Service{
Bus: bus,
SQLStore: store,
@@ -70,9 +72,41 @@ func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, secretsService secret
s.Bus.AddHandler(s.UpdateDataSource)
s.Bus.AddHandler(s.GetDefaultDataSource)
ac.RegisterAttributeScopeResolver(NewNameScopeResolver(store))
return s
}
type DataSourceRetriever interface {
GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error
}
// NewNameScopeResolver provides an AttributeScopeResolver able to
// translate a scope prefixed with "datasources:name:" into an id based scope.
func NewNameScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) {
dsNameResolver := func(ctx context.Context, orgID int64, initialScope string) (string, error) {
dsNames := strings.Split(initialScope, ":")
if dsNames[0] != "datasources" || len(dsNames) != 3 {
return "", accesscontrol.ErrInvalidScope
}
dsName := dsNames[2]
// Special wildcard case
if dsName == "*" {
return accesscontrol.Scope("datasources", "id", "*"), nil
}
query := models.GetDataSourceQuery{Name: dsName, OrgId: orgID}
if err := db.GetDataSource(ctx, &query); err != nil {
return "", err
}
return accesscontrol.Scope("datasources", "id", fmt.Sprintf("%v", query.Result.Id)), nil
}
return "datasources:name:", dsNameResolver
}
func (s *Service) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
return s.SQLStore.GetDataSource(ctx, query)
}
+86 -18
View File
@@ -13,6 +13,8 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/database"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
@@ -34,7 +36,7 @@ func TestService(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
s := ProvideService(bus.New(), sqlStore, secretsService)
s := ProvideService(bus.New(), sqlStore, secretsService, &acmock.Mock{})
var ds *models.DataSource
@@ -66,6 +68,72 @@ func TestService(t *testing.T) {
})
}
type dataSourceMockRetriever struct {
res *models.DataSource
}
func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
if query.Name == d.res.Name {
query.Result = d.res
return nil
}
return models.ErrDataSourceNotFound
}
func TestService_NameScopeResolver(t *testing.T) {
type testCaseResolver struct {
desc string
given string
want string
wantErr error
}
testCases := []testCaseResolver{
{
desc: "correct",
given: "datasources:name:test-datasource",
want: "datasources:id:1",
wantErr: nil,
},
{
desc: "correct",
given: "datasources:name:*",
want: "datasources:id:*",
wantErr: nil,
},
{
desc: "unknown datasource",
given: "datasources:name:unknown-datasource",
want: "",
wantErr: models.ErrDataSourceNotFound,
},
{
desc: "malformed scope",
given: "datasources:unknown-datasource",
want: "",
wantErr: accesscontrol.ErrInvalidScope,
},
}
testDataSource := &models.DataSource{Id: 1, Name: "test-datasource"}
prefix, resolver := NewNameScopeResolver(&dataSourceMockRetriever{testDataSource})
require.Equal(t, "datasources:name:", prefix)
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
resolved, err := resolver(context.Background(), 1, tc.given)
if tc.wantErr != nil {
require.Error(t, err)
require.Equal(t, tc.wantErr, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.want, resolved)
}
})
}
}
//nolint:goconst
func TestService_GetHttpTransport(t *testing.T) {
t.Run("Should use cached proxy", func(t *testing.T) {
@@ -83,7 +151,7 @@ func TestService_GetHttpTransport(t *testing.T) {
}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
rt1, err := dsService.GetHTTPTransport(&ds, provider)
require.NoError(t, err)
@@ -116,7 +184,7 @@ func TestService_GetHttpTransport(t *testing.T) {
json.Set("tlsAuthWithCACert", true)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
tlsCaCert, err := secretsService.Encrypt(context.Background(), []byte(caCert), secrets.WithoutScope())
require.NoError(t, err)
@@ -166,7 +234,7 @@ func TestService_GetHttpTransport(t *testing.T) {
json.Set("tlsAuth", true)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
tlsClientCert, err := secretsService.Encrypt(context.Background(), []byte(clientCert), secrets.WithoutScope())
require.NoError(t, err)
@@ -209,7 +277,7 @@ func TestService_GetHttpTransport(t *testing.T) {
json.Set("serverName", "server-name")
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
tlsCaCert, err := secretsService.Encrypt(context.Background(), []byte(caCert), secrets.WithoutScope())
require.NoError(t, err)
@@ -246,7 +314,7 @@ func TestService_GetHttpTransport(t *testing.T) {
json.Set("tlsSkipVerify", true)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
ds := models.DataSource{
Id: 1,
@@ -277,7 +345,7 @@ func TestService_GetHttpTransport(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
encryptedData, err := secretsService.Encrypt(context.Background(), []byte(`Bearer xf5yhfkpsnmgo`), secrets.WithoutScope())
require.NoError(t, err)
@@ -336,7 +404,7 @@ func TestService_GetHttpTransport(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
ds := models.DataSource{
Id: 1,
@@ -369,7 +437,7 @@ func TestService_GetHttpTransport(t *testing.T) {
require.NoError(t, err)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
ds := models.DataSource{
Type: models.DS_ES,
@@ -403,7 +471,7 @@ func TestService_getTimeout(t *testing.T) {
}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
for _, tc := range testCases {
ds := &models.DataSource{
@@ -416,7 +484,7 @@ func TestService_getTimeout(t *testing.T) {
func TestService_DecryptedValue(t *testing.T) {
t.Run("When datasource hasn't been updated, encrypted JSON should be fetched from cache", func(t *testing.T) {
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
encryptedJsonData, err := secretsService.EncryptJsonData(
context.Background(),
@@ -470,7 +538,7 @@ func TestService_DecryptedValue(t *testing.T) {
SecureJsonData: encryptedJsonData,
}
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
// Populate cache
password, ok := dsService.DecryptedValue(&ds, "password")
@@ -506,7 +574,7 @@ func TestService_HTTPClientOptions(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
@@ -523,7 +591,7 @@ func TestService_HTTPClientOptions(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
@@ -543,7 +611,7 @@ func TestService_HTTPClientOptions(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
@@ -567,7 +635,7 @@ func TestService_HTTPClientOptions(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
@@ -585,7 +653,7 @@ func TestService_HTTPClientOptions(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
_, err := dsService.httpClientOptions(&ds)
assert.Error(t, err)
@@ -599,7 +667,7 @@ func TestService_HTTPClientOptions(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService)
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)