mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
758ccfb69e
commit
cac6936015
@ -9,8 +9,6 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/datasources/permissions"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/api/datasource"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
@ -18,6 +16,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"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/web"
|
||||
)
|
||||
@ -100,7 +100,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response
|
||||
dto := convertModelToDtos(filtered[0])
|
||||
|
||||
// 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)
|
||||
}
|
||||
@ -159,7 +159,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response
|
||||
dto := convertModelToDtos(filtered[0])
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ import (
|
||||
// encrypted fields are listed under secureJsonFields section in the response.
|
||||
//
|
||||
// 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:
|
||||
// 200: createOrUpdateDatasourceResponse
|
||||
@ -59,7 +59,7 @@ import (
|
||||
// Delete an existing data source by id.
|
||||
//
|
||||
// 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:
|
||||
// 200: okResponse
|
||||
@ -101,7 +101,7 @@ import (
|
||||
// Get a single data source by Id.
|
||||
//
|
||||
// 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:
|
||||
// 200: getDatasourceResponse
|
||||
|
@ -4,6 +4,7 @@ import "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
|
||||
const (
|
||||
ScopeRoot = "datasources"
|
||||
ScopePrefix = ScopeRoot + ":uid:"
|
||||
|
||||
ActionRead = "datasources:read"
|
||||
ActionQuery = "datasources:query"
|
||||
|
@ -83,7 +83,7 @@ func ProvideService(
|
||||
s.Bus.AddHandler(s.GetDefaultDataSource)
|
||||
|
||||
ac.RegisterAttributeScopeResolver(NewNameScopeResolver(store))
|
||||
ac.RegisterAttributeScopeResolver(NewUidScopeResolver(store))
|
||||
ac.RegisterAttributeScopeResolver(NewIDScopeResolver(store))
|
||||
|
||||
return s
|
||||
}
|
||||
@ -95,10 +95,10 @@ type DataSourceRetriever interface {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
return "", accesscontrol.ErrInvalidScope
|
||||
}
|
||||
@ -113,35 +113,36 @@ func NewNameScopeResolver(db DataSourceRetriever) (string, accesscontrol.Attribu
|
||||
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
|
||||
// translate a scope prefixed with "datasources:uid:" into an id based scope.
|
||||
func NewUidScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) {
|
||||
prefix := datasources.ScopeProvider.GetResourceScopeUID("")
|
||||
dsUIDResolver := func(ctx context.Context, orgID int64, initialScope string) (string, error) {
|
||||
// NewIDScopeResolver provides an AttributeScopeResolver able to
|
||||
// translate a scope prefixed with "datasources:id:" into an uid based scope.
|
||||
func NewIDScopeResolver(db DataSourceRetriever) (string, accesscontrol.AttributeScopeResolveFunc) {
|
||||
prefix := datasources.ScopeProvider.GetResourceScope("")
|
||||
return prefix, func(ctx context.Context, orgID int64, initialScope string) (string, error) {
|
||||
if !strings.HasPrefix(initialScope, prefix) {
|
||||
return "", accesscontrol.ErrInvalidScope
|
||||
}
|
||||
|
||||
dsUID := initialScope[len(prefix):]
|
||||
if dsUID == "" {
|
||||
id := initialScope[len(prefix):]
|
||||
if id == "" {
|
||||
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 {
|
||||
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 {
|
||||
@ -180,7 +181,7 @@ func (s *Service) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCo
|
||||
if cmd.UserId != 0 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -75,9 +75,10 @@ type dataSourceMockRetriever struct {
|
||||
|
||||
func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
|
||||
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
|
||||
if nameMatch || uidMatch {
|
||||
nameMatch := query.Name != "" && query.Name == datasource.Name
|
||||
if idMatch || nameMatch || uidMatch {
|
||||
query.Result = datasource
|
||||
|
||||
return nil
|
||||
@ -88,10 +89,10 @@ func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *mode
|
||||
|
||||
func TestService_NameScopeResolver(t *testing.T) {
|
||||
retriever := &dataSourceMockRetriever{[]*models.DataSource{
|
||||
{Id: 1, Name: "test-datasource"},
|
||||
{Id: 2, Name: "*"},
|
||||
{Id: 3, Name: ":/*"},
|
||||
{Id: 4, Name: ":"},
|
||||
{Name: "test-datasource", Uid: "1"},
|
||||
{Name: "*", Uid: "2"},
|
||||
{Name: ":/*", Uid: "3"},
|
||||
{Name: ":", Uid: "4"},
|
||||
}}
|
||||
|
||||
type testCaseResolver struct {
|
||||
@ -105,25 +106,25 @@ func TestService_NameScopeResolver(t *testing.T) {
|
||||
{
|
||||
desc: "correct",
|
||||
given: "datasources:name:test-datasource",
|
||||
want: "datasources:id:1",
|
||||
want: "datasources:uid:1",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "asterisk in name",
|
||||
given: "datasources:name:*",
|
||||
want: "datasources:id:2",
|
||||
want: "datasources:uid:2",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "complex name",
|
||||
given: "datasources:name::/*",
|
||||
want: "datasources:id:3",
|
||||
want: "datasources:uid:3",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "colon in name",
|
||||
given: "datasources:name::",
|
||||
want: "datasources:id:4",
|
||||
want: "datasources:uid:4",
|
||||
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{
|
||||
{Id: 1, Uid: "NnftN9Lnz"},
|
||||
}}
|
||||
@ -177,15 +178,15 @@ func TestService_UIDScopeResolver(t *testing.T) {
|
||||
testCases := []testCaseResolver{
|
||||
{
|
||||
desc: "correct",
|
||||
given: "datasources:uid:NnftN9Lnz",
|
||||
want: "datasources:id:1",
|
||||
given: "datasources:id:1",
|
||||
want: "datasources:uid:NnftN9Lnz",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "unknown datasource",
|
||||
given: "datasources:uid:unknown",
|
||||
given: "datasources:id:unknown",
|
||||
want: "",
|
||||
wantErr: models.ErrDataSourceNotFound,
|
||||
wantErr: accesscontrol.ErrInvalidScope,
|
||||
},
|
||||
{
|
||||
desc: "malformed scope",
|
||||
@ -195,13 +196,13 @@ func TestService_UIDScopeResolver(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "empty uid scope",
|
||||
given: "datasources:uid:",
|
||||
given: "datasources:id:",
|
||||
want: "",
|
||||
wantErr: accesscontrol.ErrInvalidScope,
|
||||
},
|
||||
}
|
||||
prefix, resolver := NewUidScopeResolver(retriever)
|
||||
require.Equal(t, "datasources:uid:", prefix)
|
||||
prefix, resolver := NewIDScopeResolver(retriever)
|
||||
require.Equal(t, "datasources:id:", prefix)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user