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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user