mirror of
https://github.com/grafana/grafana.git
synced 2026-06-04 14:13:24 -04:00
Datasource Permissions: Allow sending in query param ds_type and add database migration (#121501)
This commit is contained in:
parent
84d26bde60
commit
8291111ea1
21 changed files with 231 additions and 57 deletions
|
|
@ -23,6 +23,14 @@
|
|||
# - action: 'users:write'
|
||||
# scope: 'global.users:*'
|
||||
# - action: 'users:create'
|
||||
# # Optional datasource plugin type for data source scoped permissions (`datasourceType`).
|
||||
# # If omitted and scope is `datasources:uid:<uid>`, the type is resolved from the data source when the role is provisioned.
|
||||
# # Provisioning fails if that UID if multiple data sources share the same UID, then you must set `datasourceType` explicitly.
|
||||
# - action: 'datasources:read'
|
||||
# scope: 'datasources:uid:cfh1oc3kvsw00a'
|
||||
# - action: 'datasources:read'
|
||||
# scope: 'datasources:uid:another-uid'
|
||||
# datasourceType: prometheus
|
||||
# - name: 'custom:global:users:reader'
|
||||
# # <bool> overwrite org id and creates a global role.
|
||||
# global: true
|
||||
|
|
|
|||
|
|
@ -120,6 +120,12 @@ roles:
|
|||
- action: 'users:write'
|
||||
scope: 'users:*'
|
||||
- action: 'users:create'
|
||||
# Optional `datasourceType` for scopes `datasources:uid:<DATASOURCE_UID>`.
|
||||
# If you omit it, Grafana resolves the plugin type from the data source when this file is provisioned.
|
||||
# It is required if there are two datasources with the same uid.
|
||||
- action: 'datasources:query'
|
||||
scope: 'datasources:uid:loki-uid-here'
|
||||
datasourceType: loki
|
||||
- name: 'custom:global:users:reader'
|
||||
# <bool> overwrite org id and creates a global role.
|
||||
global: true
|
||||
|
|
|
|||
|
|
@ -33,12 +33,18 @@ This API can be used to list, add and remove permissions for a data source.
|
|||
|
||||
Permissions can be set for a user, team, service account or a basic role (Admin, Editor, Viewer).
|
||||
|
||||
### Optional `ds_type` query parameter {#ds-type}
|
||||
|
||||
Every endpoint in this API accepts an optional query parameter `ds_type`. Set it to the data source **plugin type** (for example `prometheus` or `loki`). Use `ds_type` when more than one data source in the organization shares the same UID so Grafana can resolve the correct instance. If the UID is unique in the organization, you can omit `ds_type`.
|
||||
|
||||
## Get permissions for a data source
|
||||
|
||||
`GET /api/access-control/datasources/:uid`
|
||||
|
||||
Gets all existing permissions for the data source with the given `uid`.
|
||||
|
||||
Append `?ds_type=<TYPE>` when you need to disambiguate the UID; refer to [Optional `ds_type` query parameter](#ds-type).
|
||||
|
||||
**Required permissions**
|
||||
|
||||
See note in the [introduction](#data-source-permissions-api) for an explanation.
|
||||
|
|
@ -58,6 +64,15 @@ Content-Type: application/json
|
|||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example request (with `ds_type` when the UID is not unique):**
|
||||
|
||||
```http
|
||||
GET /api/access-control/datasources/my_datasource?ds_type=prometheus HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
|
||||
```http
|
||||
|
|
@ -131,6 +146,8 @@ Status codes:
|
|||
|
||||
Sets user permission for the data source with the given `uid`.
|
||||
|
||||
Append `?ds_type=<TYPE>` when you need to disambiguate the UID; refer to [Optional `ds_type` query parameter](#ds-type).
|
||||
|
||||
To add a permission, set the `permission` field to either `Query`, `Edit`, or `Admin`.
|
||||
To remove a permission, set the `permission` field to an empty string.
|
||||
|
||||
|
|
@ -203,6 +220,8 @@ Status codes:
|
|||
|
||||
Sets team permission for the data source with the given `uid`.
|
||||
|
||||
Append `?ds_type=<TYPE>` when you need to disambiguate the UID; refer to [Optional `ds_type` query parameter](#ds-type).
|
||||
|
||||
To add a permission, set the `permission` field to either `Query`, `Edit`, or `Admin`.
|
||||
To remove a permission, set the `permission` field to an empty string.
|
||||
|
||||
|
|
@ -275,6 +294,8 @@ Status codes:
|
|||
|
||||
Sets permission for the data source with the given `uid` to all users who have the specified basic role.
|
||||
|
||||
Append `?ds_type=<TYPE>` when you need to disambiguate the UID; refer to [Optional `ds_type` query parameter](#ds-type).
|
||||
|
||||
You can set permissions for the following basic roles: `Admin`, `Editor`, `Viewer`.
|
||||
|
||||
To add a permission, set the `permission` field to either `Query`, `Edit`, or `Admin`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package datasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
|
|
@ -11,8 +12,8 @@ import (
|
|||
dsV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
|
|
@ -30,19 +31,19 @@ type ConnectionClient interface {
|
|||
// as we cannot guarantee which resource the caller intended to get.
|
||||
//
|
||||
// Deprecated: Use /apis/<type>.datasource.grafana.app/v0alpha1/namespaces/{ns}/datasources/{uid} instead.
|
||||
GetConnectionByUID(c *contextmodel.ReqContext, uid string) (*dsV0.DataSourceConnectionList, error)
|
||||
GetConnectionByUID(ctx context.Context, orgID int64, uid string) (*dsV0.DataSourceConnectionList, error)
|
||||
}
|
||||
|
||||
var _ ConnectionClient = (*connectionClientImpl)(nil)
|
||||
|
||||
// connectionClientImpl implements the ConnectionClient interface.
|
||||
type connectionClientImpl struct {
|
||||
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
||||
clientConfigProvider grafanaapiserver.RestConfigProvider
|
||||
namespaceMapper request.NamespaceMapper
|
||||
}
|
||||
|
||||
// NewConnectionClient creates a new ConnectionClient that queries the connections endpoint in the query api group.
|
||||
func NewConnectionClient(cfg *setting.Cfg, provider grafanaapiserver.DirectRestConfigProvider) ConnectionClient {
|
||||
func NewConnectionClient(cfg *setting.Cfg, provider grafanaapiserver.RestConfigProvider) ConnectionClient {
|
||||
return &connectionClientImpl{
|
||||
clientConfigProvider: provider,
|
||||
namespaceMapper: request.GetNamespaceMapper(cfg),
|
||||
|
|
@ -51,19 +52,23 @@ func NewConnectionClient(cfg *setting.Cfg, provider grafanaapiserver.DirectRestC
|
|||
|
||||
// GetConnectionByUID queries GET /apis/datasource.grafana.app/v0alpha1/namespaces/{ns}/connections/{uid}
|
||||
// Deprecated: Use GetConnectionByTypeAndUID when type is known.
|
||||
func (cl *connectionClientImpl) GetConnectionByUID(c *contextmodel.ReqContext, uid string) (*dsV0.DataSourceConnectionList, error) {
|
||||
namespace := cl.namespaceMapper(c.OrgID)
|
||||
func (cl *connectionClientImpl) GetConnectionByUID(ctx context.Context, orgID int64, uid string) (*dsV0.DataSourceConnectionList, error) {
|
||||
namespace := cl.namespaceMapper(orgID)
|
||||
|
||||
cfg := cl.clientConfigProvider.GetDirectRestConfig(c)
|
||||
cfg = dynamic.ConfigFor(cfg) // This sets NegotiatedSerializer, required for RESTClientFor
|
||||
restCfg, err := cl.clientConfigProvider.GetRestConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rest config: %w", err)
|
||||
}
|
||||
|
||||
cfg := dynamic.ConfigFor(restCfg) // This sets NegotiatedSerializer, required for RESTClientFor
|
||||
cfg.GroupVersion = &dsV0.SchemeGroupVersion
|
||||
rest, err := rest.RESTClientFor(cfg)
|
||||
client, err := rest.RESTClientFor(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create rest client: %w", err)
|
||||
}
|
||||
|
||||
var statusCode int
|
||||
result := rest.Get().AbsPath("apis", dsV0.GROUP, dsV0.VERSION, "namespaces", namespace, "connections").Param("name", uid).Do(c.Req.Context()).StatusCode(&statusCode)
|
||||
result := client.Get().AbsPath("apis", dsV0.GROUP, dsV0.VERSION, "namespaces", namespace, "connections").Param("name", uid).Do(ctx).StatusCode(&statusCode)
|
||||
err = result.Error()
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
|
|
@ -89,25 +94,25 @@ func (cl *connectionClientImpl) GetConnectionByUID(c *contextmodel.ReqContext, u
|
|||
// datasource service just to get the datasource type, then forwarding the request
|
||||
// to the new APIs.
|
||||
type legacyConnectionClientImpl struct {
|
||||
datasourceService datasources.DataSourceService
|
||||
datasourceService datasourceservice.DataSourceRetriever
|
||||
}
|
||||
|
||||
var _ ConnectionClient = (*legacyConnectionClientImpl)(nil)
|
||||
|
||||
// NewLegacyConnectionClient creates a new ConnectionClient that relies on the legacy datasource service.
|
||||
func NewLegacyConnectionClient(datasourceService datasources.DataSourceService) ConnectionClient {
|
||||
func NewLegacyConnectionClient(datasourceService datasourceservice.DataSourceRetriever) ConnectionClient {
|
||||
return &legacyConnectionClientImpl{
|
||||
datasourceService: datasourceService,
|
||||
}
|
||||
}
|
||||
|
||||
func (cl *legacyConnectionClientImpl) GetConnectionByUID(c *contextmodel.ReqContext, uid string) (*dsV0.DataSourceConnectionList, error) {
|
||||
func (cl *legacyConnectionClientImpl) GetConnectionByUID(ctx context.Context, orgID int64, uid string) (*dsV0.DataSourceConnectionList, error) {
|
||||
query := datasources.GetDataSourceQuery{
|
||||
UID: uid,
|
||||
OrgID: c.OrgID,
|
||||
OrgID: orgID,
|
||||
}
|
||||
|
||||
conn, err := cl.datasourceService.GetDataSource(c.Req.Context(), &query)
|
||||
conn, err := cl.datasourceService.GetDataSource(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -16,28 +15,22 @@ import (
|
|||
clientrest "k8s.io/client-go/rest"
|
||||
|
||||
dsV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// implements grafanaapiserver.DirectRestConfigProvider
|
||||
type mockDirectRestConfigProvider struct {
|
||||
// implements grafanaapiserver.RestConfigProvider
|
||||
type mockRestConfigProvider struct {
|
||||
transport http.RoundTripper
|
||||
host string
|
||||
}
|
||||
|
||||
func (m *mockDirectRestConfigProvider) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config {
|
||||
func (m *mockRestConfigProvider) GetRestConfig(_ context.Context) (*clientrest.Config, error) {
|
||||
return &clientrest.Config{
|
||||
Host: m.host,
|
||||
Transport: m.transport,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockDirectRestConfigProvider) DirectlyServeHTTP(w http.ResponseWriter, r *http.Request) {}
|
||||
func (m *mockDirectRestConfigProvider) IsReady() bool { return true }
|
||||
|
||||
type mockRoundTripper struct {
|
||||
statusCode int
|
||||
responseBody []byte
|
||||
|
|
@ -100,7 +93,7 @@ func TestGetConnectionByUID(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &connectionClientImpl{
|
||||
clientConfigProvider: &mockDirectRestConfigProvider{
|
||||
clientConfigProvider: &mockRestConfigProvider{
|
||||
transport: &mockRoundTripper{
|
||||
statusCode: tt.statusCode,
|
||||
responseBody: tt.responseBody,
|
||||
|
|
@ -110,13 +103,7 @@ func TestGetConnectionByUID(t *testing.T) {
|
|||
namespaceMapper: func(orgID int64) string { return "default" },
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
reqCtx := &contextmodel.ReqContext{
|
||||
Context: &web.Context{Req: req},
|
||||
SignedInUser: &user.SignedInUser{OrgID: 1},
|
||||
}
|
||||
|
||||
result, err := client.GetConnectionByUID(reqCtx, tt.uid)
|
||||
result, err := client.GetConnectionByUID(context.Background(), 1, tt.uid)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
|
|
@ -187,13 +174,7 @@ func TestGetConnectionByUIDLegacy(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
reqCtx := &contextmodel.ReqContext{
|
||||
Context: &web.Context{Req: req},
|
||||
SignedInUser: &user.SignedInUser{OrgID: 1},
|
||||
}
|
||||
|
||||
conn, err := client.GetConnectionByUID(reqCtx, "uid")
|
||||
conn, err := client.GetConnectionByUID(context.Background(), 1, "uid")
|
||||
|
||||
if tt.expectedError != nil {
|
||||
require.Error(t, err)
|
||||
|
|
@ -215,6 +196,6 @@ type mockDataSourceService struct {
|
|||
error error
|
||||
}
|
||||
|
||||
func (m *mockDataSourceService) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
|
||||
func (m *mockDataSourceService) GetDataSource(_ context.Context, _ *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
|
||||
return m.response, m.error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ func (hs *HTTPServer) getK8sDataSourceByUIDHandler() web.Handler {
|
|||
uid := web.Params(c.Req)[":uid"]
|
||||
|
||||
// fetch the datasource type so we know which api group to call
|
||||
conns, err := hs.dsConnectionClient.GetConnectionByUID(c, uid) // nolint:staticcheck
|
||||
conns, err := hs.dsConnectionClient.GetConnectionByUID(c.Req.Context(), c.OrgID, uid) // nolint:staticcheck
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return response.Error(http.StatusNotFound, "Data source not found", nil)
|
||||
|
|
@ -158,7 +158,7 @@ func (hs *HTTPServer) callK8sDataSourceResourceHandler() web.Handler {
|
|||
// This uses the deprecated api on purpose because we need to get the connection details for the redirect.
|
||||
// /api/ we only have the UID so we cannot use the new api until the client updates to /apis/ which will not use this
|
||||
// redirect.
|
||||
conns, err := hs.dsConnectionClient.GetConnectionByUID(c, dsUID) //nolint:staticcheck
|
||||
conns, err := hs.dsConnectionClient.GetConnectionByUID(c.Req.Context(), c.OrgID, dsUID) //nolint:staticcheck
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
c.JsonApiErr(http.StatusNotFound, "Data source not found", nil)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
|
|
@ -59,7 +60,7 @@ type mockConnectionClient struct {
|
|||
err error
|
||||
}
|
||||
|
||||
func (m *mockConnectionClient) GetConnectionByUID(_ *contextmodel.ReqContext, _ string) (*queryV0.DataSourceConnectionList, error) {
|
||||
func (m *mockConnectionClient) GetConnectionByUID(_ context.Context, _ int64, _ string) (*queryV0.DataSourceConnectionList, error) {
|
||||
return m.result, m.err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
INSERT INTO {{ .Ident .PermissionTable }} (role_id, action, scope, created, updated, kind, attribute, identifier)
|
||||
INSERT INTO {{ .Ident .PermissionTable }} (role_id, action, scope, created, updated, kind, attribute, identifier, datasource_type)
|
||||
VALUES (
|
||||
{{ .Arg .RoleID }},
|
||||
{{ .Arg .Permission.Action }},
|
||||
|
|
@ -7,5 +7,6 @@ VALUES (
|
|||
{{ .Arg .Now }},
|
||||
{{ .Arg .Permission.Kind }},
|
||||
{{ .Arg .Permission.Attribute }},
|
||||
{{ .Arg .Permission.Identifier }}
|
||||
{{ .Arg .Permission.Identifier }},
|
||||
{{ .Arg .Permission.DatasourceType }}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
INSERT INTO `grafana`.`permission` (role_id, action, scope, created, updated, kind, attribute, identifier)
|
||||
INSERT INTO `grafana`.`permission` (role_id, action, scope, created, updated, kind, attribute, identifier, datasource_type)
|
||||
VALUES (
|
||||
23,
|
||||
'dashboards:view',
|
||||
|
|
@ -7,5 +7,6 @@ VALUES (
|
|||
'2025-08-27 21:35:00',
|
||||
'dashboard',
|
||||
'uid',
|
||||
'dash1'
|
||||
'dash1',
|
||||
''
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
INSERT INTO "grafana"."permission" (role_id, action, scope, created, updated, kind, attribute, identifier)
|
||||
INSERT INTO "grafana"."permission" (role_id, action, scope, created, updated, kind, attribute, identifier, datasource_type)
|
||||
VALUES (
|
||||
23,
|
||||
'dashboards:view',
|
||||
|
|
@ -7,5 +7,6 @@ VALUES (
|
|||
'2025-08-27 21:35:00',
|
||||
'dashboard',
|
||||
'uid',
|
||||
'dash1'
|
||||
'dash1',
|
||||
''
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
INSERT INTO "grafana"."permission" (role_id, action, scope, created, updated, kind, attribute, identifier)
|
||||
INSERT INTO "grafana"."permission" (role_id, action, scope, created, updated, kind, attribute, identifier, datasource_type)
|
||||
VALUES (
|
||||
23,
|
||||
'dashboards:view',
|
||||
|
|
@ -7,5 +7,6 @@ VALUES (
|
|||
'2025-08-27 21:35:00',
|
||||
'dashboard',
|
||||
'uid',
|
||||
'dash1'
|
||||
'dash1',
|
||||
''
|
||||
)
|
||||
|
|
|
|||
|
|
@ -209,9 +209,10 @@ type Permission struct {
|
|||
Action string `json:"action"`
|
||||
Scope string `json:"scope"`
|
||||
|
||||
Kind string `json:"-"`
|
||||
Attribute string `json:"-"`
|
||||
Identifier string `json:"-"`
|
||||
Kind string `json:"-"`
|
||||
Attribute string `json:"-"`
|
||||
Identifier string `json:"-"`
|
||||
DatasourceType string `json:"-" xorm:"datasource_type"`
|
||||
|
||||
Updated time.Time `json:"updated"`
|
||||
Created time.Time `json:"created"`
|
||||
|
|
|
|||
|
|
@ -207,6 +207,12 @@ func (a *api) getPermissions(c *contextmodel.ReqContext) response.Response {
|
|||
|
||||
resourceID := web.Params(c.Req)[":resourceID"]
|
||||
|
||||
if a.service.options.RequestValidator != nil {
|
||||
if _, err := a.service.options.RequestValidator(c.Req, c.GetOrgID(), resourceID); err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Teams-specific redirect: read team permissions from TeamBinding K8s API instead of
|
||||
// the generic resource permissions API. Falls back to legacy on failure.
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
|
|
@ -340,6 +346,16 @@ func (a *api) setUserPermission(c *contextmodel.ReqContext) response.Response {
|
|||
}
|
||||
resourceID := web.Params(c.Req)[":resourceID"]
|
||||
|
||||
if a.service.options.RequestValidator != nil {
|
||||
enrichedCtx, err := a.service.options.RequestValidator(c.Req, c.GetOrgID(), resourceID)
|
||||
if err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
if enrichedCtx != nil {
|
||||
c.Req = c.Req.WithContext(enrichedCtx)
|
||||
}
|
||||
}
|
||||
|
||||
resp := a.validateTeamResource(c, resourceID)
|
||||
if resp != nil {
|
||||
return resp
|
||||
|
|
@ -437,6 +453,16 @@ func (a *api) setTeamPermission(c *contextmodel.ReqContext) response.Response {
|
|||
}
|
||||
resourceID := web.Params(c.Req)[":resourceID"]
|
||||
|
||||
if a.service.options.RequestValidator != nil {
|
||||
enrichedCtx, err := a.service.options.RequestValidator(c.Req, c.GetOrgID(), resourceID)
|
||||
if err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
if enrichedCtx != nil {
|
||||
c.Req = c.Req.WithContext(enrichedCtx)
|
||||
}
|
||||
}
|
||||
|
||||
var cmd setPermissionCommand
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
|
|
@ -506,6 +532,16 @@ func (a *api) setBuiltinRolePermission(c *contextmodel.ReqContext) response.Resp
|
|||
builtInRole := web.Params(c.Req)[":builtInRole"]
|
||||
resourceID := web.Params(c.Req)[":resourceID"]
|
||||
|
||||
if a.service.options.RequestValidator != nil {
|
||||
enrichedCtx, err := a.service.options.RequestValidator(c.Req, c.GetOrgID(), resourceID)
|
||||
if err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
if enrichedCtx != nil {
|
||||
c.Req = c.Req.WithContext(enrichedCtx)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := setPermissionCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
|
|
@ -564,9 +600,20 @@ type SetResourcePermissionsParams struct {
|
|||
func (a *api) setPermissions(c *contextmodel.ReqContext) response.Response {
|
||||
ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.resourcepermissions.setPermissions")
|
||||
defer span.End()
|
||||
c.Req = c.Req.WithContext(ctx)
|
||||
|
||||
resourceID := web.Params(c.Req)[":resourceID"]
|
||||
|
||||
if a.service.options.RequestValidator != nil {
|
||||
enrichedCtx, err := a.service.options.RequestValidator(c.Req, c.GetOrgID(), resourceID)
|
||||
if err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
if enrichedCtx != nil {
|
||||
c.Req = c.Req.WithContext(enrichedCtx)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := setPermissionsCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Bad request data: "+err.Error(), err)
|
||||
|
|
@ -585,7 +632,7 @@ func (a *api) setPermissions(c *contextmodel.ReqContext) response.Response {
|
|||
}
|
||||
|
||||
metrics.MAccessResourcePermissionsBackend.WithLabelValues("legacy", "set_bulk", a.service.options.Resource, a.getFallbackStatus()).Inc()
|
||||
_, err := a.service.SetPermissions(ctx, c.GetOrgID(), resourceID, cmd.Permissions...)
|
||||
_, err := a.service.SetPermissions(c.Req.Context(), c.GetOrgID(), resourceID, cmd.Permissions...)
|
||||
if err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ type SetResourcePermissionCommand struct {
|
|||
ResourceID string
|
||||
ResourceAttribute string
|
||||
Permission string
|
||||
DatasourceType string
|
||||
}
|
||||
|
||||
type SetResourcePermissionsCommand struct {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package resourcepermissions
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
|
@ -65,6 +66,14 @@ type Options struct {
|
|||
LicenseMW web.Handler
|
||||
// RestConfigProvider if configured enables K8s API redirect for resource permissions
|
||||
RestConfigProvider apiserver.DirectRestConfigProvider
|
||||
// RequestValidator if configured is called before each handler. Return an error to abort the request.
|
||||
// The returned context, if non-nil, replaces the request context for subsequent processing.
|
||||
// This allows validators to cache resource metadata in the context so that downstream Set* methods
|
||||
// can read it without an additional DB lookup. Return (nil, nil) on success if no enrichment needed.
|
||||
RequestValidator func(r *http.Request, orgID int64, resourceID string) (context.Context, error)
|
||||
// DatasourceTypeResolver if configured resolves the datasource plugin type for a given resource UID.
|
||||
// Only set for datasource permission services. Used to populate datasource_type on new permission rows.
|
||||
DatasourceTypeResolver func(ctx context.Context, orgID int64, resourceID string) (string, error)
|
||||
}
|
||||
|
||||
// GetAction returns the permission action string for a given verb.
|
||||
|
|
|
|||
|
|
@ -225,12 +225,20 @@ func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user acces
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var datasourceType string
|
||||
if s.options.DatasourceTypeResolver != nil {
|
||||
if t, err := s.options.DatasourceTypeResolver(ctx, orgID, resourceID); err == nil {
|
||||
datasourceType = t
|
||||
}
|
||||
}
|
||||
|
||||
return s.store.SetUserResourcePermission(ctx, orgID, user, SetResourcePermissionCommand{
|
||||
Actions: actions,
|
||||
Permission: permission,
|
||||
Resource: s.scopeResource(),
|
||||
ResourceID: resourceID,
|
||||
ResourceAttribute: s.options.ResourceAttribute,
|
||||
DatasourceType: datasourceType,
|
||||
}, s.options.OnSetUser)
|
||||
}
|
||||
|
||||
|
|
@ -251,12 +259,20 @@ func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, re
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var datasourceType string
|
||||
if s.options.DatasourceTypeResolver != nil {
|
||||
if t, err := s.options.DatasourceTypeResolver(ctx, orgID, resourceID); err == nil {
|
||||
datasourceType = t
|
||||
}
|
||||
}
|
||||
|
||||
return s.store.SetTeamResourcePermission(ctx, orgID, teamID, SetResourcePermissionCommand{
|
||||
Actions: actions,
|
||||
Permission: permission,
|
||||
Resource: s.scopeResource(),
|
||||
ResourceID: resourceID,
|
||||
ResourceAttribute: s.options.ResourceAttribute,
|
||||
DatasourceType: datasourceType,
|
||||
}, s.options.OnSetTeam)
|
||||
}
|
||||
|
||||
|
|
@ -277,12 +293,20 @@ func (s *Service) SetBuiltInRolePermission(ctx context.Context, orgID int64, bui
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var datasourceType string
|
||||
if s.options.DatasourceTypeResolver != nil {
|
||||
if t, err := s.options.DatasourceTypeResolver(ctx, orgID, resourceID); err == nil {
|
||||
datasourceType = t
|
||||
}
|
||||
}
|
||||
|
||||
return s.store.SetBuiltInResourcePermission(ctx, orgID, builtInRole, SetResourcePermissionCommand{
|
||||
Actions: actions,
|
||||
Permission: permission,
|
||||
Resource: s.scopeResource(),
|
||||
ResourceID: resourceID,
|
||||
ResourceAttribute: s.options.ResourceAttribute,
|
||||
DatasourceType: datasourceType,
|
||||
}, s.options.OnSetBuiltInRole)
|
||||
}
|
||||
|
||||
|
|
@ -297,6 +321,13 @@ func (s *Service) SetPermissions(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var datasourceType string
|
||||
if s.options.DatasourceTypeResolver != nil {
|
||||
if t, err := s.options.DatasourceTypeResolver(ctx, orgID, resourceID); err == nil {
|
||||
datasourceType = t
|
||||
}
|
||||
}
|
||||
|
||||
dbCommands := make([]SetResourcePermissionsCommand, 0, len(commands))
|
||||
for _, cmd := range commands {
|
||||
if cmd.UserID != 0 {
|
||||
|
|
@ -328,6 +359,7 @@ func (s *Service) SetPermissions(
|
|||
ResourceID: resourceID,
|
||||
ResourceAttribute: s.options.ResourceAttribute,
|
||||
Permission: cmd.Permission,
|
||||
DatasourceType: datasourceType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -748,6 +748,7 @@ func (s *store) createPermissions(sess *db.Session, roleID int64, cmd SetResourc
|
|||
p.Created = time.Now()
|
||||
p.Updated = time.Now()
|
||||
p.Kind, p.Attribute, p.Identifier = p.SplitScope()
|
||||
p.DatasourceType = cmd.DatasourceType
|
||||
permissions = append(permissions, p)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -274,6 +274,10 @@ type GetDataSourceQuery struct {
|
|||
|
||||
// Required
|
||||
OrgID int64
|
||||
|
||||
// Type is the datasource plugin type (e.g. "prometheus", "loki").
|
||||
// When set alongside UID, it scopes the lookup to that specific type.
|
||||
Type string
|
||||
}
|
||||
|
||||
type DatasourcesPermissionFilterQuery struct {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ func (ss *SqlStore) getDataSource(_ context.Context, query *datasources.GetDataS
|
|||
UID: query.UID,
|
||||
Name: query.Name, // nolint:staticcheck
|
||||
ID: query.ID, // nolint:staticcheck
|
||||
Type: query.Type,
|
||||
}
|
||||
has, err := sess.Get(datasource)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package accesscontrol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
// DatasourceTypeMigrationID is the migration_log id for the datasource_type backfill.
|
||||
const DatasourceTypeMigrationID = "populate datasource_type in permission table for uid-scoped datasource permissions"
|
||||
|
||||
func AddDatasourceTypeMigration(mg *migrator.Migrator) {
|
||||
mg.AddMigration(DatasourceTypeMigrationID, &datasourceTypeMigrator{})
|
||||
}
|
||||
|
||||
type datasourceTypeMigrator struct {
|
||||
migrator.MigrationBase
|
||||
}
|
||||
|
||||
var _ migrator.CodeMigration = new(datasourceTypeMigrator)
|
||||
|
||||
func (m *datasourceTypeMigrator) SQL(_ migrator.Dialect) string {
|
||||
return CodeMigrationSQL
|
||||
}
|
||||
|
||||
const backfillDatasourceTypeSQL = `
|
||||
UPDATE permission AS p
|
||||
SET datasource_type = (
|
||||
SELECT ds.type
|
||||
FROM data_source AS ds
|
||||
INNER JOIN role AS r ON r.id = p.role_id
|
||||
WHERE ds.uid = p.identifier AND ds.org_id = r.org_id
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE p.kind = 'datasources' AND p.attribute = 'uid'`
|
||||
|
||||
// Exec populates the datasource_type column on permission rows that are scoped
|
||||
// to a specific datasource UID (kind='datasources', attribute='uid')
|
||||
func (m *datasourceTypeMigrator) Exec(sess *xorm.Session, _ *migrator.Migrator) error {
|
||||
if _, err := sess.Exec(backfillDatasourceTypeSQL); err != nil {
|
||||
return fmt.Errorf("failed to backfill permission.datasource_type: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -226,4 +226,10 @@ func AddMigration(mg *migrator.Migrator) {
|
|||
mg.AddMigration("alter permission.kind to length 80", migrator.NewRawSQLMigration("").
|
||||
Postgres("ALTER TABLE permission ALTER COLUMN kind TYPE VARCHAR(80);").
|
||||
Mysql("ALTER TABLE permission MODIFY kind VARCHAR(80);"))
|
||||
|
||||
mg.AddMigration("add datasource_type column to permission table", migrator.NewAddColumnMigration(permissionV1, &migrator.Column{
|
||||
Name: "datasource_type", Type: migrator.DB_NVarchar, Length: 255, Nullable: true,
|
||||
}))
|
||||
|
||||
AddDatasourceTypeMigration(mg)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue