Access control: Change data source permissions to be based on UID (#46741)

* Add ResourceAttribute

* Add ResourceAttribute option

* Set ResourceAttribute option

* Change resolvers to return uid based scopes

* update swagger to correct scope

* use ResourceAttribute for endpoint scope

* bump role version

* Add support for different attributes for access control metadata

* evaluate data source metadata based on uid

* Fix test

* uncomment benchmarks

* Use resourceID

* use evaluator for access control metadata

* update comment

* Set default permissions based on uid

* Add attribute to accesscontrol filter

* validate that scopes has correct attribute

* lint

* Update comment

* remove attribute parameter and extend prefix

* refactor to use scope prefix

* Get metadata with prefix

* fix test

* fix comparision

* remove unused type

* fix attribute index

* fix typo

* restructure logic

* Get metadata by uid

* fix imports

Co-authored-by: jguer <joao.guerreiro@grafana.com>
This commit is contained in:
Karl Persson 2022-03-24 12:21:26 +01:00 committed by GitHub
parent 758ccfb69e
commit cac6936015
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 47 additions and 44 deletions

View File

@ -9,8 +9,6 @@ import (
"sort" "sort"
"strconv" "strconv"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/datasource" "github.com/grafana/grafana/pkg/api/datasource"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
@ -18,6 +16,8 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@ -100,7 +100,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response
dto := convertModelToDtos(filtered[0]) dto := convertModelToDtos(filtered[0])
// Add accesscontrol metadata // Add accesscontrol metadata
dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, "datasources:id:", strconv.FormatInt(dto.Id, 10)) dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, datasources.ScopePrefix, dto.UID)
return response.JSON(200, &dto) return response.JSON(200, &dto)
} }
@ -159,7 +159,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response
dto := convertModelToDtos(filtered[0]) dto := convertModelToDtos(filtered[0])
// Add accesscontrol metadata // Add accesscontrol metadata
dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, "datasources:id:", strconv.FormatInt(dto.Id, 10)) dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, datasources.ScopePrefix, dto.UID)
return response.JSON(200, &dto) return response.JSON(200, &dto)
} }

View File

@ -46,7 +46,7 @@ import (
// encrypted fields are listed under secureJsonFields section in the response. // encrypted fields are listed under secureJsonFields section in the response.
// //
// If you are running Grafana Enterprise and have Fine-grained access control enabled // If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source). // you need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).
// //
// Responses: // Responses:
// 200: createOrUpdateDatasourceResponse // 200: createOrUpdateDatasourceResponse
@ -59,7 +59,7 @@ import (
// Delete an existing data source by id. // Delete an existing data source by id.
// //
// If you are running Grafana Enterprise and have Fine-grained access control enabled // If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source). // you need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).
// //
// Responses: // Responses:
// 200: okResponse // 200: okResponse
@ -101,7 +101,7 @@ import (
// Get a single data source by Id. // Get a single data source by Id.
// //
// If you are running Grafana Enterprise and have Fine-grained access control enabled // If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source). // you need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).
// //
// Responses: // Responses:
// 200: getDatasourceResponse // 200: getDatasourceResponse

View File

@ -3,7 +3,8 @@ package datasources
import "github.com/grafana/grafana/pkg/services/accesscontrol" import "github.com/grafana/grafana/pkg/services/accesscontrol"
const ( const (
ScopeRoot = "datasources" ScopeRoot = "datasources"
ScopePrefix = ScopeRoot + ":uid:"
ActionRead = "datasources:read" ActionRead = "datasources:read"
ActionQuery = "datasources:query" ActionQuery = "datasources:query"

View File

@ -83,7 +83,7 @@ func ProvideService(
s.Bus.AddHandler(s.GetDefaultDataSource) s.Bus.AddHandler(s.GetDefaultDataSource)
ac.RegisterAttributeScopeResolver(NewNameScopeResolver(store)) ac.RegisterAttributeScopeResolver(NewNameScopeResolver(store))
ac.RegisterAttributeScopeResolver(NewUidScopeResolver(store)) ac.RegisterAttributeScopeResolver(NewIDScopeResolver(store))
return s return s
} }
@ -95,10 +95,10 @@ type DataSourceRetriever interface {
} }
// NewNameScopeResolver provides an AttributeScopeResolver able to // NewNameScopeResolver provides an AttributeScopeResolver able to
// translate a scope prefixed with "datasources:name:" into an id based scope. // translate a scope prefixed with "datasources:name:" into an uid based scope.
func NewNameScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) { func NewNameScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) {
prefix := datasources.ScopeProvider.GetResourceScopeName("") prefix := datasources.ScopeProvider.GetResourceScopeName("")
dsNameResolver := func(ctx context.Context, orgID int64, initialScope string) (string, error) { return prefix, func(ctx context.Context, orgID int64, initialScope string) (string, error) {
if !strings.HasPrefix(initialScope, prefix) { if !strings.HasPrefix(initialScope, prefix) {
return "", accesscontrol.ErrInvalidScope return "", accesscontrol.ErrInvalidScope
} }
@ -113,35 +113,36 @@ func NewNameScopeResolver(db DataSourceRetriever) (string, accesscontrol.Attribu
return "", err return "", err
} }
return datasources.ScopeProvider.GetResourceScope(strconv.FormatInt(query.Result.Id, 10)), nil return datasources.ScopeProvider.GetResourceScopeUID(query.Result.Uid), nil
} }
return prefix, dsNameResolver
} }
// NewUidScopeResolver provides an AttributeScopeResolver able to // NewIDScopeResolver provides an AttributeScopeResolver able to
// translate a scope prefixed with "datasources:uid:" into an id based scope. // translate a scope prefixed with "datasources:id:" into an uid based scope.
func NewUidScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) { func NewIDScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) {
prefix := datasources.ScopeProvider.GetResourceScopeUID("") prefix := datasources.ScopeProvider.GetResourceScope("")
dsUIDResolver := func(ctx context.Context, orgID int64, initialScope string) (string, error) { return prefix, func(ctx context.Context, orgID int64, initialScope string) (string, error) {
if !strings.HasPrefix(initialScope, prefix) { if !strings.HasPrefix(initialScope, prefix) {
return "", accesscontrol.ErrInvalidScope return "", accesscontrol.ErrInvalidScope
} }
dsUID := initialScope[len(prefix):] id := initialScope[len(prefix):]
if dsUID == "" { if id == "" {
return "", accesscontrol.ErrInvalidScope return "", accesscontrol.ErrInvalidScope
} }
query := models.GetDataSourceQuery{Uid: dsUID, OrgId: orgID} dsID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return "", accesscontrol.ErrInvalidScope
}
query := models.GetDataSourceQuery{Id: dsID, OrgId: orgID}
if err := db.GetDataSource(ctx, &query); err != nil { if err := db.GetDataSource(ctx, &query); err != nil {
return "", err return "", err
} }
return datasources.ScopeProvider.GetResourceScope(strconv.FormatInt(query.Result.Id, 10)), nil return datasources.ScopeProvider.GetResourceScopeUID(query.Result.Uid), nil
} }
return prefix, dsUIDResolver
} }
func (s *Service) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { func (s *Service) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
@ -180,7 +181,7 @@ func (s *Service) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCo
if cmd.UserId != 0 { if cmd.UserId != 0 {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{UserID: cmd.UserId, Permission: "Edit"}) permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{UserID: cmd.UserId, Permission: "Edit"})
} }
if _, err := s.permissionsService.SetPermissions(ctx, cmd.OrgId, strconv.FormatInt(cmd.Result.Id, 10), permissions...); err != nil { if _, err := s.permissionsService.SetPermissions(ctx, cmd.OrgId, cmd.Result.Uid, permissions...); err != nil {
return err return err
} }
} }

View File

@ -75,9 +75,10 @@ type dataSourceMockRetriever struct {
func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
for _, datasource := range d.res { for _, datasource := range d.res {
nameMatch := query.Name != "" && query.Name == datasource.Name idMatch := query.Id != 0 && query.Id == datasource.Id
uidMatch := query.Uid != "" && query.Uid == datasource.Uid uidMatch := query.Uid != "" && query.Uid == datasource.Uid
if nameMatch || uidMatch { nameMatch := query.Name != "" && query.Name == datasource.Name
if idMatch || nameMatch || uidMatch {
query.Result = datasource query.Result = datasource
return nil return nil
@ -88,10 +89,10 @@ func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *mode
func TestService_NameScopeResolver(t *testing.T) { func TestService_NameScopeResolver(t *testing.T) {
retriever := &dataSourceMockRetriever{[]*models.DataSource{ retriever := &dataSourceMockRetriever{[]*models.DataSource{
{Id: 1, Name: "test-datasource"}, {Name: "test-datasource", Uid: "1"},
{Id: 2, Name: "*"}, {Name: "*", Uid: "2"},
{Id: 3, Name: ":/*"}, {Name: ":/*", Uid: "3"},
{Id: 4, Name: ":"}, {Name: ":", Uid: "4"},
}} }}
type testCaseResolver struct { type testCaseResolver struct {
@ -105,25 +106,25 @@ func TestService_NameScopeResolver(t *testing.T) {
{ {
desc: "correct", desc: "correct",
given: "datasources:name:test-datasource", given: "datasources:name:test-datasource",
want: "datasources:id:1", want: "datasources:uid:1",
wantErr: nil, wantErr: nil,
}, },
{ {
desc: "asterisk in name", desc: "asterisk in name",
given: "datasources:name:*", given: "datasources:name:*",
want: "datasources:id:2", want: "datasources:uid:2",
wantErr: nil, wantErr: nil,
}, },
{ {
desc: "complex name", desc: "complex name",
given: "datasources:name::/*", given: "datasources:name::/*",
want: "datasources:id:3", want: "datasources:uid:3",
wantErr: nil, wantErr: nil,
}, },
{ {
desc: "colon in name", desc: "colon in name",
given: "datasources:name::", given: "datasources:name::",
want: "datasources:id:4", want: "datasources:uid:4",
wantErr: nil, wantErr: nil,
}, },
{ {
@ -162,7 +163,7 @@ func TestService_NameScopeResolver(t *testing.T) {
} }
} }
func TestService_UIDScopeResolver(t *testing.T) { func TestService_IDScopeResolver(t *testing.T) {
retriever := &dataSourceMockRetriever{[]*models.DataSource{ retriever := &dataSourceMockRetriever{[]*models.DataSource{
{Id: 1, Uid: "NnftN9Lnz"}, {Id: 1, Uid: "NnftN9Lnz"},
}} }}
@ -177,15 +178,15 @@ func TestService_UIDScopeResolver(t *testing.T) {
testCases := []testCaseResolver{ testCases := []testCaseResolver{
{ {
desc: "correct", desc: "correct",
given: "datasources:uid:NnftN9Lnz", given: "datasources:id:1",
want: "datasources:id:1", want: "datasources:uid:NnftN9Lnz",
wantErr: nil, wantErr: nil,
}, },
{ {
desc: "unknown datasource", desc: "unknown datasource",
given: "datasources:uid:unknown", given: "datasources:id:unknown",
want: "", want: "",
wantErr: models.ErrDataSourceNotFound, wantErr: accesscontrol.ErrInvalidScope,
}, },
{ {
desc: "malformed scope", desc: "malformed scope",
@ -195,13 +196,13 @@ func TestService_UIDScopeResolver(t *testing.T) {
}, },
{ {
desc: "empty uid scope", desc: "empty uid scope",
given: "datasources:uid:", given: "datasources:id:",
want: "", want: "",
wantErr: accesscontrol.ErrInvalidScope, wantErr: accesscontrol.ErrInvalidScope,
}, },
} }
prefix, resolver := NewUidScopeResolver(retriever) prefix, resolver := NewIDScopeResolver(retriever)
require.Equal(t, "datasources:uid:", prefix) require.Equal(t, "datasources:id:", prefix)
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {