unistore: wire the authz client (#96632)

* unistore: wire the authz client

* rename dashboards.grafana.app into dashboard.grafana.app

* wire the authz client

* wire the authz client

* resuse the Standalone constructor

* configure default migration for resource folder

* add tests

* cleanup

* add logging
This commit is contained in:
Georges Chaudy
2024-11-19 15:13:30 +02:00
committed by GitHub
parent 6571451a57
commit e270412dbf
14 changed files with 192 additions and 35 deletions
+3 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/apiserver/options"
"github.com/grafana/grafana/pkg/services/authn/grpcutils"
"github.com/grafana/grafana/pkg/services/authz"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
@@ -32,6 +33,7 @@ func ProvideUnifiedStorageClient(
db infraDB.DB,
tracer tracing.Tracer,
reg prometheus.Registerer,
authzc authz.Client,
) (resource.ResourceClient, error) {
// See: apiserver.ApplyGrafanaConfig(cfg, features, o)
apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver")
@@ -95,7 +97,7 @@ func ProvideUnifiedStorageClient(
// Use the local SQL
default:
server, err := sql.NewResourceServer(ctx, db, cfg, features, tracer, reg)
server, err := sql.NewResourceServer(ctx, db, cfg, features, tracer, reg, authzc)
if err != nil {
return nil, err
}
+81
View File
@@ -2,6 +2,8 @@ package resource
import (
"context"
"log/slog"
"time"
"github.com/grafana/authlib/authz"
"github.com/grafana/authlib/claims"
@@ -24,3 +26,82 @@ func (c *staticAuthzClient) Compile(ctx context.Context, id claims.AuthInfo, req
}
var _ authz.AccessClient = &staticAuthzClient{}
type groupResource map[string]map[string]interface{}
// authzLimitedClient is a client that enforces RBAC for the limited number of groups and resources.
// This is a temporary solution until the authz service is fully implemented.
// The authz service will be responsible for enforcing RBAC.
// For now, it makes one call to the authz service for each list items. This is known to be inefficient.
type authzLimitedClient struct {
client authz.AccessChecker
// whitelist is a map of group to resources that are compatible with RBAC.
whitelist groupResource
logger *slog.Logger
}
// NewAuthzLimitedClient creates a new authzLimitedClient.
func NewAuthzLimitedClient(client authz.AccessChecker) authz.AccessClient {
logger := slog.Default().With("logger", "limited-authz-client")
return &authzLimitedClient{
client: client,
whitelist: groupResource{
"dashboard.grafana.app": map[string]interface{}{"dashboards": nil},
"folder.grafana.app": map[string]interface{}{"folders": nil},
},
logger: logger,
}
}
// Check implements authz.AccessClient.
func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) {
if !c.IsCompatibleWithRBAC(req.Group, req.Resource) {
c.logger.Debug("Check", "group", req.Group, "resource", req.Resource, "rbac", false, "allowed", true)
return authz.CheckResponse{Allowed: true}, nil
}
t := time.Now()
resp, err := c.client.Check(ctx, id, req)
if err != nil {
c.logger.Error("Check", "group", req.Group, "resource", req.Resource, "rbac", true, "error", err, "duration", time.Since(t))
return resp, err
}
c.logger.Debug("Check", "group", req.Group, "resource", req.Resource, "rbac", true, "allowed", resp.Allowed, "duration", time.Since(t))
return resp, nil
}
// Compile implements authz.AccessClient.
func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) {
return func(namespace string, name, folder string) bool {
// TODO: Implement For now we perform the check for each item.
if !c.IsCompatibleWithRBAC(req.Group, req.Resource) {
c.logger.Debug("Compile.Check", "group", req.Group, "resource", req.Resource, "namespace", namespace, "name", name, "folder", folder, "rbac", false, "allowed", true)
return true
}
t := time.Now()
r, err := c.client.Check(ctx, id, authz.CheckRequest{
Verb: "get",
Group: req.Group,
Resource: req.Resource,
Namespace: namespace,
Name: name,
Folder: folder,
})
if err != nil {
c.logger.Error("Compile.Check", "group", req.Group, "resource", req.Resource, "namespace", namespace, "name", name, "folder", folder, "rbac", true, "error", err, "duration", time.Since(t))
return false
}
c.logger.Debug("Compile.Check", "group", req.Group, "resource", req.Resource, "namespace", namespace, "name", name, "folder", folder, "rbac", true, "allowed", r.Allowed, "duration", time.Since(t))
return r.Allowed
}, nil
}
func (c authzLimitedClient) IsCompatibleWithRBAC(group, resource string) bool {
if _, ok := c.whitelist[group]; ok {
if _, ok := c.whitelist[group][resource]; ok {
return true
}
}
return false
}
var _ authz.AccessClient = &authzLimitedClient{}
@@ -0,0 +1,62 @@
package resource
import (
"context"
"testing"
"github.com/grafana/authlib/authz"
"github.com/stretchr/testify/assert"
)
func TestAuthzLimitedClient_Check(t *testing.T) {
mockClient := &staticAuthzClient{allowed: false}
client := NewAuthzLimitedClient(mockClient)
tests := []struct {
group string
resource string
expected bool
}{
{"dashboard.grafana.app", "dashboards", false},
{"folder.grafana.app", "folders", false},
{"unknown.group", "unknown.resource", true},
}
for _, test := range tests {
req := authz.CheckRequest{
Group: test.group,
Resource: test.resource,
}
resp, err := client.Check(context.Background(), nil, req)
assert.NoError(t, err)
assert.Equal(t, test.expected, resp.Allowed)
}
}
func TestAuthzLimitedClient_Compile(t *testing.T) {
mockClient := &staticAuthzClient{allowed: false}
client := NewAuthzLimitedClient(mockClient)
tests := []struct {
group string
resource string
expected bool
}{
{"dashboard.grafana.app", "dashboards", false},
{"folder.grafana.app", "folders", false},
{"unknown.group", "unknown.resource", true},
}
for _, test := range tests {
req := authz.ListRequest{
Group: test.group,
Resource: test.resource,
}
checker, err := client.Compile(context.Background(), nil, req)
assert.NoError(t, err)
assert.NotNil(t, checker)
result := checker("namespace", "name", "folder")
assert.Equal(t, test.expected, result)
}
}
+5 -2
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
infraDB "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/authz"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
@@ -18,7 +19,7 @@ import (
)
// Creates a new ResourceServer
func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer, reg prometheus.Registerer) (resource.ResourceServer, error) {
func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer, reg prometheus.Registerer, ac authz.Client) (resource.ResourceServer, error) {
apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver")
opts := resource.ResourceServerOptions{
Tracer: tracer,
@@ -27,7 +28,9 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg, fea
},
Reg: reg,
}
if ac != nil {
opts.AccessClient = resource.NewAuthzLimitedClient(ac)
}
// Support local file blob
if strings.HasPrefix(opts.Blob.URL, "./data/") {
dir := strings.Replace(opts.Blob.URL, "./data", cfg.DataPath, 1)
+7 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/modules"
"github.com/grafana/grafana/pkg/services/authn/grpcutils"
"github.com/grafana/grafana/pkg/services/authz"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/grpcserver"
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors"
@@ -93,7 +94,12 @@ func ProvideUnifiedStorageGrpcService(
}
func (s *service) start(ctx context.Context) error {
server, err := NewResourceServer(ctx, s.db, s.cfg, s.features, s.tracing, s.reg)
authzClient, err := authz.ProvideStandaloneAuthZClient(s.cfg, s.features, s.tracing)
if err != nil {
return err
}
server, err := NewResourceServer(ctx, s.db, s.cfg, s.features, s.tracing, s.reg, authzClient)
if err != nil {
return err
}