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"
|
"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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -4,6 +4,7 @@ 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"
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user