zanzana/test-dashboard-search
Karl Persson 8 months ago
parent 5533b30135
commit 9f5581942f
  1. 4
      Makefile
  2. 13
      pkg/services/accesscontrol/acimpl/accesscontrol.go
  3. 10
      pkg/services/accesscontrol/dualwrite/reconciler.go
  4. 1
      pkg/services/authz/zanzana/client.go
  5. 41
      pkg/services/authz/zanzana/client/client.go
  6. 4
      pkg/services/authz/zanzana/client/noop.go
  7. 319
      pkg/services/authz/zanzana/proto/v1/extention.pb.go
  8. 21
      pkg/services/authz/zanzana/proto/v1/extention.proto
  9. 40
      pkg/services/authz/zanzana/proto/v1/extention_grpc.pb.go
  10. 1
      pkg/services/authz/zanzana/server/server.go
  11. 149
      pkg/services/authz/zanzana/server/server_batch_check.go
  12. 113
      pkg/services/authz/zanzana/server/server_batch_check_test.go
  13. 1
      pkg/services/authz/zanzana/server/server_list.go
  14. 4
      pkg/services/authz/zanzana/server/server_test.go
  15. 9
      pkg/services/authz/zanzana/translations.go
  16. 1
      pkg/services/authz/zanzana/zanzana.go
  17. 5
      pkg/services/dashboards/service/dashboard_service.go
  18. 288
      pkg/services/dashboards/service/zanzana.go
  19. 1
      pkg/services/dashboards/service/zanzana_integration_test.go

@ -179,7 +179,7 @@ update-workspace: gen-go
bash scripts/go-workspace/update-workspace.sh
.PHONY: build-go
build-go: gen-go update-workspace ## Build all Go binaries.
build-go:
@echo "build go files with updated workspace"
$(GO) run build.go $(GO_BUILD_FLAGS) build
@ -411,7 +411,7 @@ devenv-mysql:
.PHONY: protobuf
protobuf: ## Compile protobuf definitions
bash scripts/protobuf-check.sh
go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0
buf generate pkg/plugins/backendplugin/pluginextensionv2 --template pkg/plugins/backendplugin/pluginextensionv2/buf.gen.yaml
buf generate pkg/plugins/backendplugin/secretsmanagerplugin --template pkg/plugins/backendplugin/secretsmanagerplugin/buf.gen.yaml

@ -3,6 +3,7 @@ package acimpl
import (
"context"
"errors"
"fmt"
"strconv"
"time"
@ -10,9 +11,11 @@ import (
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel"
"github.com/grafana/authlib/authz"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@ -35,6 +38,16 @@ func ProvideAccessControl(features featuremgmt.FeatureToggles, zclient zanzana.C
m = initMetrics()
}
res, _ := zclient.Check(context.TODO(), &identity.StaticRequester{Type: claims.TypeUser, UserUID: "ce2ficppysl4wd"}, authz.CheckRequest{
Verb: utils.VerbGet,
Group: "dashboard.grafana.app",
Resource: "dashboards",
Name: "gen-e274d5ad-8314-4e3d-b035-7c236f901af4",
Folder: "gen-5fb43981-c102-4fbe-896f-d21fb5ef494a",
})
fmt.Println("Check result for user 4", res.Allowed)
return &AccessControl{
features,
logger,

@ -57,7 +57,7 @@ func NewZanzanaReconciler(client zanzana.Client, store db.DB, lock *serverlock.S
client: client,
lock: lock,
log: log.New("zanzana.reconciler"),
collectors: collectors,
collectors: []TupleCollector{},
reconcilers: []resourceReconciler{
newResourceReconciler(
"team memberships",
@ -112,7 +112,7 @@ func (r *ZanzanaReconciler) Sync(ctx context.Context) error {
}
}
r.reconcile(ctx)
// r.reconcile(ctx)
return nil
}
@ -140,14 +140,18 @@ func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
run := func(ctx context.Context) {
now := time.Now()
r.log.Info("Start reconciliation", "ts", now)
for _, reconciler := range r.reconcilers {
if err := reconciler.reconcile(ctx); err != nil {
r.log.Warn("Failed to perform reconciliation for resource", "err", err)
}
}
r.log.Debug("Finished reconciliation", "elapsed", time.Since(now))
r.log.Info("Finished reconciliation", "elapsed", time.Since(now))
}
run(ctx)
return
if r.lock == nil {
run(ctx)
return

@ -18,6 +18,7 @@ import (
// Client is a wrapper around [openfgav1.OpenFGAServiceClient]
type Client interface {
authz.AccessClient
BatchCheck(ctx context.Context, id claims.AuthInfo, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error)
List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error)
CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error)

@ -85,7 +85,6 @@ func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption)
return c, nil
}
// Check implements authz.AccessClient.
func (c *Client) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) {
ctx, span := tracer.Start(ctx, "authz.zanzana.client.Check")
defer span.End()
@ -109,11 +108,19 @@ func (c *Client) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckR
return authz.CheckResponse{Allowed: res.GetAllowed()}, nil
}
func (c *Client) BatchCheck(ctx context.Context, id claims.AuthInfo, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
ctx, span := tracer.Start(ctx, "authz.zanzana.client.BatchCheck")
defer span.End()
req.Subject = id.GetUID()
return c.authzext.BatchCheck(ctx, req)
}
func (c *Client) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) {
ctx, span := tracer.Start(ctx, "authz.zanzana.client.Compile")
defer span.End()
_, err := c.authzext.List(ctx, &authzextv1.ListRequest{
res, err := c.authzext.List(ctx, &authzextv1.ListRequest{
Subject: id.GetUID(),
Group: req.Group,
Verb: utils.VerbList,
@ -125,8 +132,7 @@ func (c *Client) Compile(ctx context.Context, id claims.AuthInfo, req authz.List
return nil, err
}
// FIXME: implement checker
return func(namespace, name, folder string) bool { return false }, nil
return newItemChecker(res), nil
}
func (c *Client) List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) {
@ -227,3 +233,30 @@ func (c *Client) loadModel(ctx context.Context, storeID string) (string, error)
return res.AuthorizationModels[0].GetId(), nil
}
func newItemChecker(res *authzextv1.ListResponse) authz.ItemChecker {
// if we can see all resource of this type we can just return a function that always return true
if res.GetAll() {
return func(_, _, _ string) bool { return true }
}
folders := make(map[string]struct{}, len(res.Folders))
for _, f := range res.Folders {
folders[f] = struct{}{}
}
items := make(map[string]struct{}, len(res.Items))
for _, i := range res.Items {
items[i] = struct{}{}
}
return func(namespace, name, folder string) bool {
if _, ok := items[name]; ok {
return true
}
if _, ok := folders[folder]; ok {
return true
}
return false
}
}

@ -22,6 +22,10 @@ func (nc *NoopClient) Check(ctx context.Context, id claims.AuthInfo, req authz.C
return authz.CheckResponse{}, nil
}
func (nc *NoopClient) BatchCheck(ctx context.Context, id claims.AuthInfo, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
return nil, nil
}
func (nc *NoopClient) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) {
return nil, nil
}

@ -162,6 +162,203 @@ func (x *ListResponse) GetItems() []string {
return nil
}
type BatchCheckRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"`
Verb string `protobuf:"bytes,2,opt,name=verb,proto3" json:"verb,omitempty"`
Group string `protobuf:"bytes,3,opt,name=group,proto3" json:"group,omitempty"`
Resource string `protobuf:"bytes,4,opt,name=resource,proto3" json:"resource,omitempty"`
Namespace string `protobuf:"bytes,5,opt,name=namespace,proto3" json:"namespace,omitempty"`
Items []*BatchCheckItem `protobuf:"bytes,6,rep,name=items,proto3" json:"items,omitempty"`
}
func (x *BatchCheckRequest) Reset() {
*x = BatchCheckRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_extention_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BatchCheckRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchCheckRequest) ProtoMessage() {}
func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message {
mi := &file_extention_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead.
func (*BatchCheckRequest) Descriptor() ([]byte, []int) {
return file_extention_proto_rawDescGZIP(), []int{2}
}
func (x *BatchCheckRequest) GetSubject() string {
if x != nil {
return x.Subject
}
return ""
}
func (x *BatchCheckRequest) GetVerb() string {
if x != nil {
return x.Verb
}
return ""
}
func (x *BatchCheckRequest) GetGroup() string {
if x != nil {
return x.Group
}
return ""
}
func (x *BatchCheckRequest) GetResource() string {
if x != nil {
return x.Resource
}
return ""
}
func (x *BatchCheckRequest) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *BatchCheckRequest) GetItems() []*BatchCheckItem {
if x != nil {
return x.Items
}
return nil
}
type BatchCheckItem struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Folder string `protobuf:"bytes,2,opt,name=folder,proto3" json:"folder,omitempty"`
}
func (x *BatchCheckItem) Reset() {
*x = BatchCheckItem{}
if protoimpl.UnsafeEnabled {
mi := &file_extention_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BatchCheckItem) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchCheckItem) ProtoMessage() {}
func (x *BatchCheckItem) ProtoReflect() protoreflect.Message {
mi := &file_extention_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead.
func (*BatchCheckItem) Descriptor() ([]byte, []int) {
return file_extention_proto_rawDescGZIP(), []int{3}
}
func (x *BatchCheckItem) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *BatchCheckItem) GetFolder() string {
if x != nil {
return x.Folder
}
return ""
}
type BatchCheckResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
All bool `protobuf:"varint,1,opt,name=all,proto3" json:"all,omitempty"`
Items map[string]bool `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}
func (x *BatchCheckResponse) Reset() {
*x = BatchCheckResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_extention_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BatchCheckResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BatchCheckResponse) ProtoMessage() {}
func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message {
mi := &file_extention_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead.
func (*BatchCheckResponse) Descriptor() ([]byte, []int) {
return file_extention_proto_rawDescGZIP(), []int{4}
}
func (x *BatchCheckResponse) GetAll() bool {
if x != nil {
return x.All
}
return false
}
func (x *BatchCheckResponse) GetItems() map[string]bool {
if x != nil {
return x.Items
}
return nil
}
var File_extention_proto protoreflect.FileDescriptor
var file_extention_proto_rawDesc = []byte{
@ -181,17 +378,51 @@ var file_extention_proto_rawDesc = []byte{
0x52, 0x03, 0x61, 0x6c, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x73,
0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x12,
0x14, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05,
0x69, 0x74, 0x65, 0x6d, 0x73, 0x32, 0x62, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78,
0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49,
0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65,
0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e,
0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f,
0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xcb, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43,
0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73,
0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75,
0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f,
0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12,
0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65,
0x6d, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a,
0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61,
0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74,
0x65, 0x6d, 0x73, 0x22, 0x3c, 0x0a, 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63,
0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c,
0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65,
0x72, 0x22, 0xa9, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18,
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x12, 0x47, 0x0a, 0x05, 0x69, 0x74,
0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x61, 0x75, 0x74, 0x68,
0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42,
0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x74,
0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0xbf, 0x01,
0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12,
0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f,
0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69,
0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b,
0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69,
0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e,
0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74,
0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72,
0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b,
0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var (
@ -206,19 +437,27 @@ func file_extention_proto_rawDescGZIP() []byte {
return file_extention_proto_rawDescData
}
var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_extention_proto_goTypes = []any{
(*ListRequest)(nil), // 0: authz.extention.v1.ListRequest
(*ListResponse)(nil), // 1: authz.extention.v1.ListResponse
(*ListRequest)(nil), // 0: authz.extention.v1.ListRequest
(*ListResponse)(nil), // 1: authz.extention.v1.ListResponse
(*BatchCheckRequest)(nil), // 2: authz.extention.v1.BatchCheckRequest
(*BatchCheckItem)(nil), // 3: authz.extention.v1.BatchCheckItem
(*BatchCheckResponse)(nil), // 4: authz.extention.v1.BatchCheckResponse
nil, // 5: authz.extention.v1.BatchCheckResponse.ItemsEntry
}
var file_extention_proto_depIdxs = []int32{
0, // 0: authz.extention.v1.AuthzExtentionService.List:input_type -> authz.extention.v1.ListRequest
1, // 1: authz.extention.v1.AuthzExtentionService.List:output_type -> authz.extention.v1.ListResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
3, // 0: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem
5, // 1: authz.extention.v1.BatchCheckResponse.items:type_name -> authz.extention.v1.BatchCheckResponse.ItemsEntry
0, // 2: authz.extention.v1.AuthzExtentionService.List:input_type -> authz.extention.v1.ListRequest
2, // 3: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest
1, // 4: authz.extention.v1.AuthzExtentionService.List:output_type -> authz.extention.v1.ListResponse
4, // 5: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse
4, // [4:6] is the sub-list for method output_type
2, // [2:4] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_extention_proto_init() }
@ -251,6 +490,42 @@ func file_extention_proto_init() {
return nil
}
}
file_extention_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*BatchCheckRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_extention_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*BatchCheckItem); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_extention_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*BatchCheckResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@ -258,7 +533,7 @@ func file_extention_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_extention_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},

@ -6,6 +6,8 @@ package authz.extention.v1;
service AuthzExtentionService {
rpc List(ListRequest) returns (ListResponse);
rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse);
}
message ListRequest {
@ -21,3 +23,22 @@ message ListResponse {
repeated string folders = 2;
repeated string items = 3;
}
message BatchCheckRequest {
string subject = 1;
string verb = 2;
string group = 3;
string resource = 4;
string namespace = 5;
repeated BatchCheckItem items = 6;
}
message BatchCheckItem {
string name = 1;
string folder = 2;
}
message BatchCheckResponse {
bool all = 1;
map<string, bool> items = 2;
}

@ -19,7 +19,8 @@ import (
const _ = grpc.SupportPackageIsVersion8
const (
AuthzExtentionService_List_FullMethodName = "/authz.extention.v1.AuthzExtentionService/List"
AuthzExtentionService_List_FullMethodName = "/authz.extention.v1.AuthzExtentionService/List"
AuthzExtentionService_BatchCheck_FullMethodName = "/authz.extention.v1.AuthzExtentionService/BatchCheck"
)
// AuthzExtentionServiceClient is the client API for AuthzExtentionService service.
@ -27,6 +28,7 @@ const (
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AuthzExtentionServiceClient interface {
List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error)
}
type authzExtentionServiceClient struct {
@ -47,11 +49,22 @@ func (c *authzExtentionServiceClient) List(ctx context.Context, in *ListRequest,
return out, nil
}
func (c *authzExtentionServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchCheckResponse)
err := c.cc.Invoke(ctx, AuthzExtentionService_BatchCheck_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthzExtentionServiceServer is the server API for AuthzExtentionService service.
// All implementations should embed UnimplementedAuthzExtentionServiceServer
// for forward compatibility
type AuthzExtentionServiceServer interface {
List(context.Context, *ListRequest) (*ListResponse, error)
BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error)
}
// UnimplementedAuthzExtentionServiceServer should be embedded to have forward compatible implementations.
@ -61,6 +74,9 @@ type UnimplementedAuthzExtentionServiceServer struct {
func (UnimplementedAuthzExtentionServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedAuthzExtentionServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BatchCheck not implemented")
}
// UnsafeAuthzExtentionServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AuthzExtentionServiceServer will
@ -91,6 +107,24 @@ func _AuthzExtentionService_List_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler)
}
func _AuthzExtentionService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchCheckRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthzExtentionService_BatchCheck_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest))
}
return interceptor(ctx, in, info, handler)
}
// AuthzExtentionService_ServiceDesc is the grpc.ServiceDesc for AuthzExtentionService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -102,6 +136,10 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{
MethodName: "List",
Handler: _AuthzExtentionService_List_Handler,
},
{
MethodName: "BatchCheck",
Handler: _AuthzExtentionService_BatchCheck_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "extention.proto",

@ -31,7 +31,6 @@ var errStoreNotFound = errors.New("store not found")
type Server struct {
authzv1.UnimplementedAuthzServiceServer
authzextv1.UnimplementedAuthzExtentionServiceServer
openfga openfgav1.OpenFGAServiceServer

@ -0,0 +1,149 @@
package server
import (
"context"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"google.golang.org/protobuf/types/known/structpb"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1"
)
func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok {
return s.batchCheckTyped(ctx, r, info)
}
return s.batchCheckGeneric(ctx, r)
}
func (s *Server) batchCheckTyped(ctx context.Context, r *authzextv1.BatchCheckRequest, info common.TypeInfo) (*authzextv1.BatchCheckResponse, error) {
relation := common.VerbMapping[r.GetVerb()]
// 1. check if subject has access through namespace
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: s.storeID,
AuthorizationModelId: s.modelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
if err != nil {
return nil, err
}
if res.GetAllowed() {
return &authzextv1.BatchCheckResponse{All: true}, nil
}
BatchRes := &authzextv1.BatchCheckResponse{
Items: make(map[string]bool, len(r.Items)),
}
// 2. check if subject has direct access to resources
for _, i := range r.Items {
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: s.storeID,
AuthorizationModelId: s.modelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: common.NewTypedIdent(info.Type, i.GetName()),
},
})
if err != nil {
return nil, err
}
BatchRes.Items[i.GetName()] = res.GetAllowed()
}
return BatchRes, nil
}
func (s *Server) batchCheckGeneric(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
relation := common.VerbMapping[r.GetVerb()]
// 1. check if subject has access through namespace
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: s.storeID,
AuthorizationModelId: s.modelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
if err != nil {
return nil, err
}
if res.GetAllowed() {
return &authzextv1.BatchCheckResponse{All: true}, nil
}
BatchRes := &authzextv1.BatchCheckResponse{
Items: make(map[string]bool, len(r.Items)),
}
for _, i := range r.Items {
// 2. check if subject has direct access to resource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: s.storeID,
AuthorizationModelId: s.modelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: common.NewResourceIdent(r.GetGroup(), r.GetResource(), i.GetName()),
},
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())),
},
},
})
if err != nil {
return nil, err
}
if res.GetAllowed() {
BatchRes.Items[i.GetName()] = true
continue
}
if i.Folder == "" {
BatchRes.Items[i.GetName()] = false
continue
}
// 3. check if subject has access as a sub resource for the folder
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: s.storeID,
AuthorizationModelId: s.modelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: common.NewFolderResourceIdent(r.GetGroup(), r.GetResource(), i.GetFolder()),
},
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())),
},
},
})
if err != nil {
return nil, err
}
BatchRes.Items[i.GetName()] = res.GetAllowed()
}
return BatchRes, nil
}

@ -0,0 +1,113 @@
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/utils"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1"
)
func testBatchCheck(t *testing.T, server *Server) {
newBatch := func(subject, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
return &authzextv1.BatchCheckRequest{
// FIXME: namespace should map to store
// Namespace: storeID,
Subject: subject,
Verb: utils.VerbGet,
Group: group,
Resource: resource,
Items: items,
}
}
t.Run("user:1 should only be able to read resource:dashboards.grafana.app/dashboards/1", func(t *testing.T) {
res, err := server.BatchCheck(context.Background(), newBatch("user:1", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
require.NoError(t, err)
assert.False(t, res.All)
require.Len(t, res.Items, 2)
assert.True(t, res.Items["1"])
assert.False(t, res.Items["2"])
})
t.Run("user:2 should be able to read resource:dashboards.grafana.app/dashboards/{1,2} through namespace", func(t *testing.T) {
res, err := server.BatchCheck(context.Background(), newBatch("user:2", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
require.NoError(t, err)
assert.True(t, res.All)
assert.Len(t, res.Items, 0)
})
t.Run("user:3 should be able to read resource:dashboards.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
res, err := server.BatchCheck(context.Background(), newBatch("user:3", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
require.NoError(t, err)
assert.False(t, res.All)
require.Len(t, res.Items, 2)
assert.True(t, res.Items["1"])
assert.False(t, res.Items["2"])
})
t.Run("user:4 should be able to read all dashboards.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
res, err := server.BatchCheck(context.Background(), newBatch("user:4", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "3"},
{Name: "3", Folder: "2"},
}))
require.NoError(t, err)
assert.False(t, res.All)
require.Len(t, res.Items, 3)
assert.True(t, res.Items["1"])
assert.True(t, res.Items["2"])
assert.False(t, res.Items["3"])
})
t.Run("user:5 should be able to read resource:dashboards.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
res, err := server.BatchCheck(context.Background(), newBatch("user:5", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
require.NoError(t, err)
assert.False(t, res.All)
require.Len(t, res.Items, 2)
assert.True(t, res.Items["1"])
assert.False(t, res.Items["2"])
})
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
res, err := server.BatchCheck(context.Background(), newBatch("user:6", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
require.NoError(t, err)
assert.False(t, res.All)
require.Len(t, res.Items, 2)
assert.True(t, res.Items["1"])
assert.False(t, res.Items["2"])
})
t.Run("user:7 should be able to read folder {1,2} through namespace access", func(t *testing.T) {
res, err := server.BatchCheck(context.Background(), newBatch("user:7", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
require.NoError(t, err)
assert.True(t, res.All)
require.Len(t, res.Items, 0)
})
}

@ -22,6 +22,7 @@ func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzext
return s.listGeneric(ctx, r)
}
func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info common.TypeInfo) (*authzextv1.ListResponse, error) {
relation := common.VerbMapping[r.GetVerb()]

@ -49,6 +49,10 @@ func TestIntegrationServer(t *testing.T) {
t.Run("test list", func(t *testing.T) {
testList(t, srv)
})
t.Run("test bulkCheck", func(t *testing.T) {
testBulkCheck(t, srv)
})
}
func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {

@ -155,6 +155,7 @@ var resourceTranslations = map[string]resourceTranslation{
"dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource),
"dashboards.permissions:read": newScopedMapping(RelationPermissionsRead, dashboardGroup, dashboardResource),
"dashboards.permissions:write": newScopedMapping(RelationPermissionsWrite, dashboardGroup, dashboardResource),
"alert.rules:read": newScopedMapping(RelationRead, "alerting.grafana.app", "rules"),
},
},
KindDashboards: {
@ -170,4 +171,12 @@ var resourceTranslations = map[string]resourceTranslation{
"dashboards.permissions:write": newMapping(RelationPermissionsWrite),
},
},
KindAlertRules: {
typ: TypeResource,
group: "alerting.grafna.app",
resource: "rules",
mapping: map[string]actionMappig{
"alert.rules:read": newMapping(RelationRead),
},
},
}

@ -42,6 +42,7 @@ var ResourceRelations = []string{RelationRead, RelationWrite, RelationCreate, Re
const (
KindOrg string = "org"
KindDashboards string = "dashboards"
KindAlertRules string = "alert.rules"
KindFolders string = "folders"
)

@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/datasources"
@ -59,6 +60,7 @@ type DashboardServiceImpl struct {
folderPermissions accesscontrol.FolderPermissionsService
dashboardPermissions accesscontrol.DashboardPermissionsService
ac accesscontrol.AccessControl
zclient zanzana.Client
metrics *dashboardsMetrics
}
@ -67,7 +69,7 @@ func ProvideDashboardServiceImpl(
cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore,
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl,
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer,
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, zclient zanzana.Client,
) (*DashboardServiceImpl, error) {
dashSvc := &DashboardServiceImpl{
cfg: cfg,
@ -80,6 +82,7 @@ func ProvideDashboardServiceImpl(
folderStore: folderStore,
folderService: folderSvc,
metrics: newDashboardsMetrics(r),
zclient: zclient,
}
ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, fStore))

@ -10,10 +10,16 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/authlib/authz"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1"
"github.com/grafana/grafana/pkg/services/dashboards"
)
@ -86,16 +92,47 @@ func (dr *DashboardServiceImpl) findDashboardsZanzanaCompare(ctx context.Context
"zanana_result_len", len(second.result),
"grafana_duration", first.duration,
"zanzana_duration", second.duration,
"title", query.Title,
"missing", findMissing(first.result, second.result),
)
} else {
dr.metrics.searchRequestStatusTotal.WithLabelValues("success").Inc()
dr.log.Debug("zanzana search is correct", "result_len", len(first.result), "grafana_duration", first.duration, "zanzana_duration", second.duration)
dr.log.Info("zanzana search is correct", "grafana_len", len(first.result), "zanzana_len", len(first.result), "grafana_duration", first.duration, "zanzana_duration", second.duration)
}
return first.result, first.err
}
func findMissing(a, b []dashboards.DashboardSearchProjection) []string {
lookup := make(map[string]struct{}, 0)
for _, s := range b {
lookup[s.UID] = struct{}{}
}
var missing []string
for _, s := range a {
if _, ok := lookup[s.UID]; !ok {
uid := fmt.Sprintf("folder:%s", s.UID)
if !s.IsFolder {
uid = fmt.Sprintf("dashboard:%s:parent:%s", s.UID, s.FolderUID)
}
missing = append(missing, uid)
}
}
return missing
}
func (dr *DashboardServiceImpl) findDashboardsZanzana(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
if query.Limit < 1 {
query.Limit = 1000
}
if query.Page < 1 {
query.Page = 1
}
findDashboards := dr.getFindDashboardsFn(query)
return findDashboards(ctx, query)
}
@ -104,19 +141,252 @@ type findDashboardsFn func(ctx context.Context, query dashboards.FindPersistedDa
// getFindDashboardsFn makes a decision which search method should be used
func (dr *DashboardServiceImpl) getFindDashboardsFn(query dashboards.FindPersistedDashboardsQuery) findDashboardsFn {
if query.Limit > 0 && query.Limit < listQueryLimitThreshold && len(query.Title) > 0 {
/*
if query.Limit > 0 && query.Limit < listQueryLimitThreshold && len(query.Title) > 0 {
return dr.findDashboardsZanzanaCheck
}
if len(query.DashboardUIDs) > 0 || len(query.DashboardIds) > 0 {
return dr.findDashboardsZanzanaCheck
}
if len(query.FolderUIDs) > 0 {
return dr.findDashboardsZanzanaCheck
}
if len(query.Title) <= listQueryLengthThreshold {
return dr.findDashboardsZanzanaList
}
return dr.findDashboardsZanzanaCheck
*/
if false {
return dr.findDashboardsZanzanaGeneric
} else if false {
return dr.findDashboardsZanzanaGenericCheck
} else {
return dr.findDashboardsZanzanaGenericBatchCheck
}
if len(query.DashboardUIDs) > 0 || len(query.DashboardIds) > 0 {
return dr.findDashboardsZanzanaCheck
}
func (dr *DashboardServiceImpl) findDashboardsZanzanaGeneric(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
dashCheck, err := dr.zclient.Compile(ctx, query.SignedInUser, authz.ListRequest{
Group: dashboardalpha1.DashboardResourceInfo.GroupResource().Group,
Resource: dashboardalpha1.DashboardResourceInfo.GroupResource().Resource,
})
if err != nil {
return nil, err
}
if len(query.FolderUIDs) > 0 {
return dr.findDashboardsZanzanaCheck
folderCheck, err := dr.zclient.Compile(ctx, query.SignedInUser, authz.ListRequest{
Group: folderalpha1.FolderResourceInfo.GroupResource().Group,
Resource: folderalpha1.FolderResourceInfo.GroupResource().Resource,
})
if err != nil {
return nil, err
}
if query.Page < 1 {
query.Page = 1
}
// Remember initial query limit
limit := query.Limit
// Set limit to default to prevent pagination issues
query.Limit = defaultQueryLimit
query.SkipAccessControlFilter = true
result := make([]dashboards.DashboardSearchProjection, 0, query.Limit)
outer:
for len(result) < int(limit) {
findRes, err := dr.dashboardStore.FindDashboards(ctx, &query)
if err != nil {
return nil, err
}
if len(findRes) == 0 {
break outer
}
if err != nil {
return nil, err
}
for _, r := range findRes {
if len(result) == int(limit) {
break outer
}
if r.IsFolder {
if folderCheck("", r.UID, "") {
result = append(result, r)
}
} else {
if dashCheck("", r.UID, r.FolderUID) {
result = append(result, r)
}
}
}
query.Page += 1
}
return result, nil
}
func (dr *DashboardServiceImpl) findDashboardsZanzanaGenericBatchCheck(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
if query.Page < 1 {
query.Page = 1
}
// Remember initial query limit
limit := query.Limit
// Set limit to default to prevent pagination issues
query.Limit = defaultQueryLimit
query.SkipAccessControlFilter = true
result := make([]dashboards.DashboardSearchProjection, 0, query.Limit)
outer:
for len(result) < int(limit) {
findRes, err := dr.dashboardStore.FindDashboards(ctx, &query)
if err != nil {
return nil, err
}
if len(findRes) == 0 {
break outer
}
if err != nil {
return nil, err
}
foldersCheck := &authzextv1.BatchCheckRequest{
Verb: utils.VerbGet,
Group: folderalpha1.FolderResourceInfo.GroupResource().Group,
Resource: folderalpha1.FolderResourceInfo.GroupResource().Resource,
}
dashboardsCheck := &authzextv1.BatchCheckRequest{
Verb: utils.VerbGet,
Group: dashboardalpha1.DashboardResourceInfo.GroupResource().Group,
Resource: dashboardalpha1.DashboardResourceInfo.GroupResource().Resource,
}
for _, r := range findRes {
if r.IsFolder {
foldersCheck.Items = append(foldersCheck.Items, &authzextv1.BatchCheckItem{Name: r.UID})
} else {
dashboardsCheck.Items = append(dashboardsCheck.Items, &authzextv1.BatchCheckItem{Name: r.UID, Folder: r.FolderUID})
}
}
foldersRes, err := dr.zclient.BatchCheck(ctx, query.SignedInUser, foldersCheck)
if err != nil {
return nil, err
}
dashboardsRes, err := dr.zclient.BatchCheck(ctx, query.SignedInUser, dashboardsCheck)
if err != nil {
return nil, err
}
for _, r := range findRes {
if len(result) == int(limit) {
break outer
}
if r.IsFolder {
if foldersRes.Items[r.UID] {
result = append(result, r)
}
} else {
if dashboardsRes.Items[r.UID] {
result = append(result, r)
}
}
}
query.Page += 1
}
if len(query.Title) <= listQueryLengthThreshold {
return dr.findDashboardsZanzanaList
return result, nil
}
func (dr *DashboardServiceImpl) findDashboardsZanzanaGenericCheck(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
if query.Page < 1 {
query.Page = 1
}
return dr.findDashboardsZanzanaCheck
// Remember initial query limit
limit := query.Limit
// Set limit to default to prevent pagination issues
query.Limit = defaultQueryLimit
query.SkipAccessControlFilter = true
result := make([]dashboards.DashboardSearchProjection, 0, query.Limit)
outer:
for len(result) < int(limit) {
findRes, err := dr.dashboardStore.FindDashboards(ctx, &query)
if err != nil {
return nil, err
}
if len(findRes) == 0 {
break outer
}
if err != nil {
return nil, err
}
for _, r := range findRes {
if len(result) == int(limit) {
break outer
}
if r.IsFolder {
res, err := dr.zclient.Check(ctx, query.SignedInUser, authz.CheckRequest{
Verb: utils.VerbGet,
Group: folderalpha1.FolderResourceInfo.GroupResource().Group,
Resource: folderalpha1.FolderResourceInfo.GroupResource().Resource,
Name: r.UID,
})
if err != nil {
return nil, err
}
if res.Allowed {
result = append(result, r)
}
} else {
res, err := dr.zclient.Check(ctx, query.SignedInUser, authz.CheckRequest{
Verb: utils.VerbGet,
Name: r.UID,
Group: dashboardalpha1.DashboardResourceInfo.GroupResource().Group,
Resource: dashboardalpha1.DashboardResourceInfo.GroupResource().Resource,
Folder: r.FolderUID,
})
if err != nil {
return nil, err
}
if res.Allowed {
result = append(result, r)
}
}
}
query.Page += 1
}
return result, nil
}
// findDashboardsZanzanaCheck implements "Search, then check" strategy. It first performs search query, then filters out results

@ -70,6 +70,7 @@ func TestIntegrationDashboardServiceZanzana(t *testing.T) {
foldertest.NewFakeService(),
fStore,
nil,
zclient,
)
require.NoError(t, err)

Loading…
Cancel
Save