From 7a7fd45bdd32ccb5969bd0dc17e435b6547ac44e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 28 Oct 2025 11:22:13 +0100 Subject: [PATCH] Zanzana: app platform style write APIs (#112812) * refactor zanzana client instantiation * refactor client imports * POC write API (Mutate) * fix linter * delete exisitng folder parents * refactor common functions * minor refactor * groupd operations by type * atomic folder operations * use deleteExisting for deletes * Add tests for folders * more tests * resource permissions tests * add more tests * fix mock zanzana client * fix linter * fix linter * re-use types from apps * add some comments to the protobuf --- apps/iam/pkg/reconcilers/folder_reconciler.go | 11 +- pkg/server/wire_gen.go | 4 +- .../dualwrite/collectors_test.go | 4 + pkg/services/authz/proto/v1/extention.pb.go | 1125 +++++++++++++---- pkg/services/authz/proto/v1/extention.proto | 64 + .../authz/proto/v1/extention_grpc.pb.go | 38 + pkg/services/authz/rbac.go | 5 +- pkg/services/authz/wireset.go | 2 +- pkg/services/authz/zanzana.go | 42 +- pkg/services/authz/zanzana/client.go | 16 +- pkg/services/authz/zanzana/client/client.go | 18 + pkg/services/authz/zanzana/client/noop.go | 8 +- .../authz/zanzana/client/shadow_client.go | 5 +- .../zanzana/{ => common}/translations.go | 61 +- pkg/services/authz/zanzana/common/tuple.go | 98 +- pkg/services/authz/zanzana/server.go | 30 - .../authz/zanzana/server/server_mutate.go | 90 ++ .../zanzana/server/server_mutate_folder.go | 142 +++ .../server/server_mutate_folder_test.go | 164 +++ .../server_mutate_resourcepermissions.go | 173 +++ .../server_mutate_resourcepermissions_test.go | 115 ++ .../zanzana/server/server_mutate_test.go | 135 ++ .../authz/zanzana/server/server_test.go | 97 +- pkg/services/authz/zanzana/store.go | 18 - pkg/services/authz/zanzana/zanzana.go | 145 +-- 25 files changed, 2108 insertions(+), 502 deletions(-) rename pkg/services/authz/zanzana/{ => common}/translations.go (67%) create mode 100644 pkg/services/authz/zanzana/server/server_mutate.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_folder.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_folder_test.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_test.go delete mode 100644 pkg/services/authz/zanzana/store.go diff --git a/apps/iam/pkg/reconcilers/folder_reconciler.go b/apps/iam/pkg/reconcilers/folder_reconciler.go index 59385b3b3a3..066637ee1e6 100644 --- a/apps/iam/pkg/reconcilers/folder_reconciler.go +++ b/apps/iam/pkg/reconcilers/folder_reconciler.go @@ -5,15 +5,16 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/operator" foldersKind "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/services/authz" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" ) // PermissionStore interface for managing folder permissions @@ -36,7 +37,7 @@ type FolderReconciler struct { func NewFolderReconciler(cfg ReconcilerConfig) (operator.Reconciler, error) { // Create Zanzana client - zanzanaClient, err := authz.NewZanzanaClient("*", cfg.ZanzanaCfg) + zanzanaClient, err := authz.NewRemoteZanzanaClient("*", cfg.ZanzanaCfg) if err != nil { return nil, fmt.Errorf("unable to create zanzana client: %w", err) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index feed7394400..ac23bdda879 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -442,7 +442,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - zanzanaClient, err := authz.ProvideZanzana(cfg, sqlStore, tracingService, featureToggles, registerer) + zanzanaClient, err := authz.ProvideZanzanaClient(cfg, sqlStore, tracingService, featureToggles, registerer) if err != nil { return nil, err } @@ -1061,7 +1061,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - zanzanaClient, err := authz.ProvideZanzana(cfg, sqlStore, tracingService, featureToggles, registerer) + zanzanaClient, err := authz.ProvideZanzanaClient(cfg, sqlStore, tracingService, featureToggles, registerer) if err != nil { return nil, err } diff --git a/pkg/services/accesscontrol/dualwrite/collectors_test.go b/pkg/services/accesscontrol/dualwrite/collectors_test.go index 2c0ac88a689..32e021c4b1c 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors_test.go +++ b/pkg/services/accesscontrol/dualwrite/collectors_test.go @@ -242,6 +242,10 @@ func (m *mockZanzanaClient) Compile(ctx context.Context, id authlib.AuthInfo, re return args.Get(0).(authlib.ItemChecker), args.Get(1).(authlib.Zookie), args.Error(2) } +func (m *mockZanzanaClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { + return nil +} + func TestIntegrationTeamMembershipCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) diff --git a/pkg/services/authz/proto/v1/extention.pb.go b/pkg/services/authz/proto/v1/extention.pb.go index 9f155187ab5..9b197a15d94 100644 --- a/pkg/services/authz/proto/v1/extention.pb.go +++ b/pkg/services/authz/proto/v1/extention.pb.go @@ -24,6 +24,564 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type MutateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Operations []*MutateOperation `protobuf:"bytes,2,rep,name=operations,proto3" json:"operations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutateRequest) Reset() { + *x = MutateRequest{} + mi := &file_extention_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutateRequest) ProtoMessage() {} + +func (x *MutateRequest) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutateRequest.ProtoReflect.Descriptor instead. +func (*MutateRequest) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{0} +} + +func (x *MutateRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *MutateRequest) GetOperations() []*MutateOperation { + if x != nil { + return x.Operations + } + return nil +} + +type MutateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutateResponse) Reset() { + *x = MutateResponse{} + mi := &file_extention_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutateResponse) ProtoMessage() {} + +func (x *MutateResponse) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutateResponse.ProtoReflect.Descriptor instead. +func (*MutateResponse) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{1} +} + +type MutateOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Operation: + // + // *MutateOperation_SetFolderParent + // *MutateOperation_DeleteFolder + // *MutateOperation_CreatePermission + // *MutateOperation_DeletePermission + Operation isMutateOperation_Operation `protobuf_oneof:"operation"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutateOperation) Reset() { + *x = MutateOperation{} + mi := &file_extention_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutateOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutateOperation) ProtoMessage() {} + +func (x *MutateOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutateOperation.ProtoReflect.Descriptor instead. +func (*MutateOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{2} +} + +func (x *MutateOperation) GetOperation() isMutateOperation_Operation { + if x != nil { + return x.Operation + } + return nil +} + +func (x *MutateOperation) GetSetFolderParent() *SetFolderParentOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_SetFolderParent); ok { + return x.SetFolderParent + } + } + return nil +} + +func (x *MutateOperation) GetDeleteFolder() *DeleteFolderOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeleteFolder); ok { + return x.DeleteFolder + } + } + return nil +} + +func (x *MutateOperation) GetCreatePermission() *CreatePermissionOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_CreatePermission); ok { + return x.CreatePermission + } + } + return nil +} + +func (x *MutateOperation) GetDeletePermission() *DeletePermissionOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeletePermission); ok { + return x.DeletePermission + } + } + return nil +} + +type isMutateOperation_Operation interface { + isMutateOperation_Operation() +} + +type MutateOperation_SetFolderParent struct { + SetFolderParent *SetFolderParentOperation `protobuf:"bytes,1,opt,name=set_folder_parent,json=setFolderParent,proto3,oneof"` +} + +type MutateOperation_DeleteFolder struct { + DeleteFolder *DeleteFolderOperation `protobuf:"bytes,2,opt,name=delete_folder,json=deleteFolder,proto3,oneof"` +} + +type MutateOperation_CreatePermission struct { + CreatePermission *CreatePermissionOperation `protobuf:"bytes,3,opt,name=create_permission,json=createPermission,proto3,oneof"` +} + +type MutateOperation_DeletePermission struct { + DeletePermission *DeletePermissionOperation `protobuf:"bytes,4,opt,name=delete_permission,json=deletePermission,proto3,oneof"` +} + +func (*MutateOperation_SetFolderParent) isMutateOperation_Operation() {} + +func (*MutateOperation_DeleteFolder) isMutateOperation_Operation() {} + +func (*MutateOperation_CreatePermission) isMutateOperation_Operation() {} + +func (*MutateOperation_DeletePermission) isMutateOperation_Operation() {} + +type SetFolderParentOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UID of the folder + Folder string `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder,omitempty"` + // UID of the parent folder + Parent string `protobuf:"bytes,2,opt,name=parent,proto3" json:"parent,omitempty"` + // If true, delete all existing parent relations associated with the folder + DeleteExisting bool `protobuf:"varint,3,opt,name=delete_existing,json=deleteExisting,proto3" json:"delete_existing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetFolderParentOperation) Reset() { + *x = SetFolderParentOperation{} + mi := &file_extention_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetFolderParentOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetFolderParentOperation) ProtoMessage() {} + +func (x *SetFolderParentOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetFolderParentOperation.ProtoReflect.Descriptor instead. +func (*SetFolderParentOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{3} +} + +func (x *SetFolderParentOperation) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +func (x *SetFolderParentOperation) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *SetFolderParentOperation) GetDeleteExisting() bool { + if x != nil { + return x.DeleteExisting + } + return false +} + +type DeleteFolderOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UID of the folder to delete + Folder string `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder,omitempty"` + // UID of the parent folder + Parent string `protobuf:"bytes,2,opt,name=parent,proto3" json:"parent,omitempty"` + // If true, delete all existing parent relations associated with the folder + DeleteExisting bool `protobuf:"varint,3,opt,name=delete_existing,json=deleteExisting,proto3" json:"delete_existing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteFolderOperation) Reset() { + *x = DeleteFolderOperation{} + mi := &file_extention_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteFolderOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteFolderOperation) ProtoMessage() {} + +func (x *DeleteFolderOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteFolderOperation.ProtoReflect.Descriptor instead. +func (*DeleteFolderOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{4} +} + +func (x *DeleteFolderOperation) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +func (x *DeleteFolderOperation) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *DeleteFolderOperation) GetDeleteExisting() bool { + if x != nil { + return x.DeleteExisting + } + return false +} + +type CreatePermissionOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Resource *Resource `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + Permission *Permission `protobuf:"bytes,2,opt,name=permission,proto3" json:"permission,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreatePermissionOperation) Reset() { + *x = CreatePermissionOperation{} + mi := &file_extention_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreatePermissionOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePermissionOperation) ProtoMessage() {} + +func (x *CreatePermissionOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreatePermissionOperation.ProtoReflect.Descriptor instead. +func (*CreatePermissionOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{5} +} + +func (x *CreatePermissionOperation) GetResource() *Resource { + if x != nil { + return x.Resource + } + return nil +} + +func (x *CreatePermissionOperation) GetPermission() *Permission { + if x != nil { + return x.Permission + } + return nil +} + +type DeletePermissionOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Resource *Resource `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + Permission *Permission `protobuf:"bytes,2,opt,name=permission,proto3" json:"permission,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletePermissionOperation) Reset() { + *x = DeletePermissionOperation{} + mi := &file_extention_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletePermissionOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletePermissionOperation) ProtoMessage() {} + +func (x *DeletePermissionOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeletePermissionOperation.ProtoReflect.Descriptor instead. +func (*DeletePermissionOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{6} +} + +func (x *DeletePermissionOperation) GetResource() *Resource { + if x != nil { + return x.Resource + } + return nil +} + +func (x *DeletePermissionOperation) GetPermission() *Permission { + if x != nil { + return x.Permission + } + return nil +} + +type Resource struct { + state protoimpl.MessageState `protogen:"open.v1"` + // group of the resource (e.g: "dashboard.grafana.app") + Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` + // kind of the resource (e.g: "dashboards") + Resource string `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + // uid of the resource + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Resource) Reset() { + *x = Resource{} + mi := &file_extention_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Resource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Resource) ProtoMessage() {} + +func (x *Resource) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Resource.ProtoReflect.Descriptor instead. +func (*Resource) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{7} +} + +func (x *Resource) GetGroup() string { + if x != nil { + return x.Group + } + return "" +} + +func (x *Resource) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +func (x *Resource) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type Permission struct { + state protoimpl.MessageState `protogen:"open.v1"` + // kind of the identity getting the permission (e.g: "user", "team", "serviceaccount") + Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` + // uid of the identity getting the permission + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // action set granted to the user (e.g. "admin" or "edit", "view") + Verb string `protobuf:"bytes,3,opt,name=verb,proto3" json:"verb,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Permission) Reset() { + *x = Permission{} + mi := &file_extention_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Permission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Permission) ProtoMessage() {} + +func (x *Permission) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Permission.ProtoReflect.Descriptor instead. +func (*Permission) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{8} +} + +func (x *Permission) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *Permission) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Permission) GetVerb() string { + if x != nil { + return x.Verb + } + return "" +} + type TupleKey struct { state protoimpl.MessageState `protogen:"open.v1"` User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` @@ -36,7 +594,7 @@ type TupleKey struct { func (x *TupleKey) Reset() { *x = TupleKey{} - mi := &file_extention_proto_msgTypes[0] + mi := &file_extention_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -48,7 +606,7 @@ func (x *TupleKey) String() string { func (*TupleKey) ProtoMessage() {} func (x *TupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[0] + mi := &file_extention_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -61,7 +619,7 @@ func (x *TupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKey.ProtoReflect.Descriptor instead. func (*TupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{0} + return file_extention_proto_rawDescGZIP(), []int{9} } func (x *TupleKey) GetUser() string { @@ -102,7 +660,7 @@ type Tuple struct { func (x *Tuple) Reset() { *x = Tuple{} - mi := &file_extention_proto_msgTypes[1] + mi := &file_extention_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -114,7 +672,7 @@ func (x *Tuple) String() string { func (*Tuple) ProtoMessage() {} func (x *Tuple) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[1] + mi := &file_extention_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -127,7 +685,7 @@ func (x *Tuple) ProtoReflect() protoreflect.Message { // Deprecated: Use Tuple.ProtoReflect.Descriptor instead. func (*Tuple) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{1} + return file_extention_proto_rawDescGZIP(), []int{10} } func (x *Tuple) GetKey() *TupleKey { @@ -155,7 +713,7 @@ type TupleKeyWithoutCondition struct { func (x *TupleKeyWithoutCondition) Reset() { *x = TupleKeyWithoutCondition{} - mi := &file_extention_proto_msgTypes[2] + mi := &file_extention_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -167,7 +725,7 @@ func (x *TupleKeyWithoutCondition) String() string { func (*TupleKeyWithoutCondition) ProtoMessage() {} func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[2] + mi := &file_extention_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -180,7 +738,7 @@ func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKeyWithoutCondition.ProtoReflect.Descriptor instead. func (*TupleKeyWithoutCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{2} + return file_extention_proto_rawDescGZIP(), []int{11} } func (x *TupleKeyWithoutCondition) GetUser() string { @@ -214,7 +772,7 @@ type RelationshipCondition struct { func (x *RelationshipCondition) Reset() { *x = RelationshipCondition{} - mi := &file_extention_proto_msgTypes[3] + mi := &file_extention_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -226,7 +784,7 @@ func (x *RelationshipCondition) String() string { func (*RelationshipCondition) ProtoMessage() {} func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[3] + mi := &file_extention_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -239,7 +797,7 @@ func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use RelationshipCondition.ProtoReflect.Descriptor instead. func (*RelationshipCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{3} + return file_extention_proto_rawDescGZIP(), []int{12} } func (x *RelationshipCondition) GetName() string { @@ -268,7 +826,7 @@ type ReadRequest struct { func (x *ReadRequest) Reset() { *x = ReadRequest{} - mi := &file_extention_proto_msgTypes[4] + mi := &file_extention_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -280,7 +838,7 @@ func (x *ReadRequest) String() string { func (*ReadRequest) ProtoMessage() {} func (x *ReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[4] + mi := &file_extention_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -293,7 +851,7 @@ func (x *ReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. func (*ReadRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{4} + return file_extention_proto_rawDescGZIP(), []int{13} } func (x *ReadRequest) GetNamespace() string { @@ -335,7 +893,7 @@ type ReadRequestTupleKey struct { func (x *ReadRequestTupleKey) Reset() { *x = ReadRequestTupleKey{} - mi := &file_extention_proto_msgTypes[5] + mi := &file_extention_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -347,7 +905,7 @@ func (x *ReadRequestTupleKey) String() string { func (*ReadRequestTupleKey) ProtoMessage() {} func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[5] + mi := &file_extention_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -360,7 +918,7 @@ func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequestTupleKey.ProtoReflect.Descriptor instead. func (*ReadRequestTupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{5} + return file_extention_proto_rawDescGZIP(), []int{14} } func (x *ReadRequestTupleKey) GetUser() string { @@ -394,7 +952,7 @@ type ReadResponse struct { func (x *ReadResponse) Reset() { *x = ReadResponse{} - mi := &file_extention_proto_msgTypes[6] + mi := &file_extention_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -406,7 +964,7 @@ func (x *ReadResponse) String() string { func (*ReadResponse) ProtoMessage() {} func (x *ReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[6] + mi := &file_extention_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -419,7 +977,7 @@ func (x *ReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. func (*ReadResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{6} + return file_extention_proto_rawDescGZIP(), []int{15} } func (x *ReadResponse) GetTuples() []*Tuple { @@ -445,7 +1003,7 @@ type WriteRequestWrites struct { func (x *WriteRequestWrites) Reset() { *x = WriteRequestWrites{} - mi := &file_extention_proto_msgTypes[7] + mi := &file_extention_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -457,7 +1015,7 @@ func (x *WriteRequestWrites) String() string { func (*WriteRequestWrites) ProtoMessage() {} func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[7] + mi := &file_extention_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -470,7 +1028,7 @@ func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestWrites.ProtoReflect.Descriptor instead. func (*WriteRequestWrites) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{7} + return file_extention_proto_rawDescGZIP(), []int{16} } func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey { @@ -489,7 +1047,7 @@ type WriteRequestDeletes struct { func (x *WriteRequestDeletes) Reset() { *x = WriteRequestDeletes{} - mi := &file_extention_proto_msgTypes[8] + mi := &file_extention_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -501,7 +1059,7 @@ func (x *WriteRequestDeletes) String() string { func (*WriteRequestDeletes) ProtoMessage() {} func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[8] + mi := &file_extention_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -514,7 +1072,7 @@ func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestDeletes.ProtoReflect.Descriptor instead. func (*WriteRequestDeletes) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{8} + return file_extention_proto_rawDescGZIP(), []int{17} } func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition { @@ -535,7 +1093,7 @@ type WriteRequest struct { func (x *WriteRequest) Reset() { *x = WriteRequest{} - mi := &file_extention_proto_msgTypes[9] + mi := &file_extention_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -547,7 +1105,7 @@ func (x *WriteRequest) String() string { func (*WriteRequest) ProtoMessage() {} func (x *WriteRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[9] + mi := &file_extention_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -560,7 +1118,7 @@ func (x *WriteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead. func (*WriteRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{9} + return file_extention_proto_rawDescGZIP(), []int{18} } func (x *WriteRequest) GetNamespace() string { @@ -592,7 +1150,7 @@ type WriteResponse struct { func (x *WriteResponse) Reset() { *x = WriteResponse{} - mi := &file_extention_proto_msgTypes[10] + mi := &file_extention_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -604,7 +1162,7 @@ func (x *WriteResponse) String() string { func (*WriteResponse) ProtoMessage() {} func (x *WriteResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[10] + mi := &file_extention_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -617,7 +1175,7 @@ func (x *WriteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteResponse.ProtoReflect.Descriptor instead. func (*WriteResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{10} + return file_extention_proto_rawDescGZIP(), []int{19} } type BatchCheckRequest struct { @@ -631,7 +1189,7 @@ type BatchCheckRequest struct { func (x *BatchCheckRequest) Reset() { *x = BatchCheckRequest{} - mi := &file_extention_proto_msgTypes[11] + mi := &file_extention_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -643,7 +1201,7 @@ func (x *BatchCheckRequest) String() string { func (*BatchCheckRequest) ProtoMessage() {} func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[11] + mi := &file_extention_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -656,7 +1214,7 @@ func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead. func (*BatchCheckRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{11} + return file_extention_proto_rawDescGZIP(), []int{20} } func (x *BatchCheckRequest) GetSubject() string { @@ -694,7 +1252,7 @@ type BatchCheckItem struct { func (x *BatchCheckItem) Reset() { *x = BatchCheckItem{} - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -706,7 +1264,7 @@ func (x *BatchCheckItem) String() string { func (*BatchCheckItem) ProtoMessage() {} func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -719,7 +1277,7 @@ func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead. func (*BatchCheckItem) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{12} + return file_extention_proto_rawDescGZIP(), []int{21} } func (x *BatchCheckItem) GetVerb() string { @@ -773,7 +1331,7 @@ type BatchCheckResponse struct { func (x *BatchCheckResponse) Reset() { *x = BatchCheckResponse{} - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -785,7 +1343,7 @@ func (x *BatchCheckResponse) String() string { func (*BatchCheckResponse) ProtoMessage() {} func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -798,7 +1356,7 @@ func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead. func (*BatchCheckResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{13} + return file_extention_proto_rawDescGZIP(), []int{22} } func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource { @@ -817,7 +1375,7 @@ type BatchCheckGroupResource struct { func (x *BatchCheckGroupResource) Reset() { *x = BatchCheckGroupResource{} - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -829,7 +1387,7 @@ func (x *BatchCheckGroupResource) String() string { func (*BatchCheckGroupResource) ProtoMessage() {} func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -842,7 +1400,7 @@ func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckGroupResource.ProtoReflect.Descriptor instead. func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{14} + return file_extention_proto_rawDescGZIP(), []int{23} } func (x *BatchCheckGroupResource) GetItems() map[string]bool { @@ -863,150 +1421,231 @@ var file_extention_proto_rawDesc = string([]byte{ 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x72, 0x0a, 0x0d, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, + 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4d, 0x75, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x88, 0x03, 0x0a, 0x0f, 0x4d, + 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, + 0x0a, 0x11, 0x73, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x65, 0x74, 0x46, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x50, 0x0a, 0x0d, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0c, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x5c, 0x0a, 0x11, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x10, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x5c, 0x0a, 0x11, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, + 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x70, 0x0a, 0x15, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, + 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x95, 0x01, 0x0a, + 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x50, 0x0a, 0x08, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, + 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x48, + 0x0a, 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, + 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, 0x70, + 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, 0x0a, + 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, + 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x12, + 0x2e, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, + 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, 0x0a, + 0x15, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, 0x01, + 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x74, + 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, + 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, + 0x79, 0x12, 0x38, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, + 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, 0x65, + 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x63, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, - 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, - 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, 0x0a, 0x15, 0x52, 0x65, 0x6c, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, 0x01, 0x0a, 0x0b, 0x52, 0x65, - 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, + 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, 0x61, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, 0x70, + 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, + 0x75, 0x70, 0x6c, 0x65, 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, + 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, + 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, 0x57, + 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, + 0x73, 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, + 0x4b, 0x65, 0x79, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x62, + 0x0a, 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, + 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, + 0x79, 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, + 0x73, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, 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, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x02, 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, 0x03, 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, 0xa4, 0x01, + 0x0a, 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, + 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x76, 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, + 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 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, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, + 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, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 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, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0xa1, 0x01, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x38, 0x0a, - 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x70, - 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x69, - 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, - 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, - 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, - 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, 0x57, 0x72, 0x69, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x3b, 0x0a, - 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, - 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x62, 0x0a, 0x13, 0x57, 0x72, - 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, - 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0xaf, - 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x3e, 0x0a, - 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, - 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x41, 0x0a, - 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 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, 0xde, 0x02, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 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, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, + 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 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, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, - 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x85, 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, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, - 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, 0x03, 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, 0xa4, 0x01, 0x0a, 0x0e, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, - 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, - 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, - 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 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, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x67, 0x72, 0x6f, - 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 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, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2b, 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, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa1, 0x01, 0x0a, 0x17, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 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, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 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, - 0x8d, 0x02, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 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, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 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, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 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, + 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 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 ( @@ -1021,56 +1660,76 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_extention_proto_goTypes = []any{ - (*TupleKey)(nil), // 0: authz.extention.v1.TupleKey - (*Tuple)(nil), // 1: authz.extention.v1.Tuple - (*TupleKeyWithoutCondition)(nil), // 2: authz.extention.v1.TupleKeyWithoutCondition - (*RelationshipCondition)(nil), // 3: authz.extention.v1.RelationshipCondition - (*ReadRequest)(nil), // 4: authz.extention.v1.ReadRequest - (*ReadRequestTupleKey)(nil), // 5: authz.extention.v1.ReadRequestTupleKey - (*ReadResponse)(nil), // 6: authz.extention.v1.ReadResponse - (*WriteRequestWrites)(nil), // 7: authz.extention.v1.WriteRequestWrites - (*WriteRequestDeletes)(nil), // 8: authz.extention.v1.WriteRequestDeletes - (*WriteRequest)(nil), // 9: authz.extention.v1.WriteRequest - (*WriteResponse)(nil), // 10: authz.extention.v1.WriteResponse - (*BatchCheckRequest)(nil), // 11: authz.extention.v1.BatchCheckRequest - (*BatchCheckItem)(nil), // 12: authz.extention.v1.BatchCheckItem - (*BatchCheckResponse)(nil), // 13: authz.extention.v1.BatchCheckResponse - (*BatchCheckGroupResource)(nil), // 14: authz.extention.v1.BatchCheckGroupResource - nil, // 15: authz.extention.v1.BatchCheckResponse.GroupsEntry - nil, // 16: authz.extention.v1.BatchCheckGroupResource.ItemsEntry - (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 18: google.protobuf.Struct - (*wrapperspb.Int32Value)(nil), // 19: google.protobuf.Int32Value + (*MutateRequest)(nil), // 0: authz.extention.v1.MutateRequest + (*MutateResponse)(nil), // 1: authz.extention.v1.MutateResponse + (*MutateOperation)(nil), // 2: authz.extention.v1.MutateOperation + (*SetFolderParentOperation)(nil), // 3: authz.extention.v1.SetFolderParentOperation + (*DeleteFolderOperation)(nil), // 4: authz.extention.v1.DeleteFolderOperation + (*CreatePermissionOperation)(nil), // 5: authz.extention.v1.CreatePermissionOperation + (*DeletePermissionOperation)(nil), // 6: authz.extention.v1.DeletePermissionOperation + (*Resource)(nil), // 7: authz.extention.v1.Resource + (*Permission)(nil), // 8: authz.extention.v1.Permission + (*TupleKey)(nil), // 9: authz.extention.v1.TupleKey + (*Tuple)(nil), // 10: authz.extention.v1.Tuple + (*TupleKeyWithoutCondition)(nil), // 11: authz.extention.v1.TupleKeyWithoutCondition + (*RelationshipCondition)(nil), // 12: authz.extention.v1.RelationshipCondition + (*ReadRequest)(nil), // 13: authz.extention.v1.ReadRequest + (*ReadRequestTupleKey)(nil), // 14: authz.extention.v1.ReadRequestTupleKey + (*ReadResponse)(nil), // 15: authz.extention.v1.ReadResponse + (*WriteRequestWrites)(nil), // 16: authz.extention.v1.WriteRequestWrites + (*WriteRequestDeletes)(nil), // 17: authz.extention.v1.WriteRequestDeletes + (*WriteRequest)(nil), // 18: authz.extention.v1.WriteRequest + (*WriteResponse)(nil), // 19: authz.extention.v1.WriteResponse + (*BatchCheckRequest)(nil), // 20: authz.extention.v1.BatchCheckRequest + (*BatchCheckItem)(nil), // 21: authz.extention.v1.BatchCheckItem + (*BatchCheckResponse)(nil), // 22: authz.extention.v1.BatchCheckResponse + (*BatchCheckGroupResource)(nil), // 23: authz.extention.v1.BatchCheckGroupResource + nil, // 24: authz.extention.v1.BatchCheckResponse.GroupsEntry + nil, // 25: authz.extention.v1.BatchCheckGroupResource.ItemsEntry + (*timestamppb.Timestamp)(nil), // 26: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 27: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 28: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ - 3, // 0: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition - 0, // 1: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey - 17, // 2: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp - 18, // 3: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct - 5, // 4: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey - 19, // 5: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value - 1, // 6: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple - 0, // 7: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey - 2, // 8: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition - 7, // 9: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites - 8, // 10: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes - 12, // 11: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem - 15, // 12: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry - 16, // 13: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry - 14, // 14: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource - 11, // 15: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest - 4, // 16: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest - 9, // 17: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest - 13, // 18: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse - 6, // 19: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse - 10, // 20: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse - 18, // [18:21] is the sub-list for method output_type - 15, // [15:18] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 2, // 0: authz.extention.v1.MutateRequest.operations:type_name -> authz.extention.v1.MutateOperation + 3, // 1: authz.extention.v1.MutateOperation.set_folder_parent:type_name -> authz.extention.v1.SetFolderParentOperation + 4, // 2: authz.extention.v1.MutateOperation.delete_folder:type_name -> authz.extention.v1.DeleteFolderOperation + 5, // 3: authz.extention.v1.MutateOperation.create_permission:type_name -> authz.extention.v1.CreatePermissionOperation + 6, // 4: authz.extention.v1.MutateOperation.delete_permission:type_name -> authz.extention.v1.DeletePermissionOperation + 7, // 5: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 8, // 6: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 7, // 7: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 8, // 8: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 12, // 9: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition + 9, // 10: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey + 26, // 11: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 27, // 12: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 14, // 13: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey + 28, // 14: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 10, // 15: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple + 9, // 16: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey + 11, // 17: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition + 16, // 18: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites + 17, // 19: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes + 21, // 20: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem + 24, // 21: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry + 25, // 22: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry + 23, // 23: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource + 20, // 24: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest + 13, // 25: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 18, // 26: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 0, // 27: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest + 22, // 28: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse + 15, // 29: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 19, // 30: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 1, // 31: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse + 28, // [28:32] is the sub-list for method output_type + 24, // [24:28] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_extention_proto_init() } @@ -1078,13 +1737,19 @@ func file_extention_proto_init() { if File_extention_proto != nil { return } + file_extention_proto_msgTypes[2].OneofWrappers = []any{ + (*MutateOperation_SetFolderParent)(nil), + (*MutateOperation_DeleteFolder)(nil), + (*MutateOperation_CreatePermission)(nil), + (*MutateOperation_DeletePermission)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_extention_proto_rawDesc), len(file_extention_proto_rawDesc)), NumEnums: 0, - NumMessages: 17, + NumMessages: 26, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index 9d64c718b56..a7e52a99627 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -13,6 +13,70 @@ service AuthzExtentionService { rpc Read(ReadRequest) returns (ReadResponse); rpc Write(WriteRequest) returns (WriteResponse); + + rpc Mutate(MutateRequest) returns (MutateResponse); +} + +message MutateRequest { + string namespace = 1; + repeated MutateOperation operations = 2; +} + +message MutateResponse {} + +message MutateOperation { + oneof operation { + SetFolderParentOperation set_folder_parent = 1; + DeleteFolderOperation delete_folder = 2; + CreatePermissionOperation create_permission = 3; + DeletePermissionOperation delete_permission = 4; + } +} + +message SetFolderParentOperation { + // UID of the folder + string folder = 1; + // UID of the parent folder + string parent = 2; + // If true, delete all existing parent relations associated with the folder + bool delete_existing = 3; +} + +message DeleteFolderOperation { + // UID of the folder to delete + string folder = 1; + // UID of the parent folder + string parent = 2; + // If true, delete all existing parent relations associated with the folder + bool delete_existing = 3; +} + +message CreatePermissionOperation { + Resource resource = 1; + Permission permission = 2; +} + +message DeletePermissionOperation { + Resource resource = 1; + Permission permission = 2; +} + +message Resource { + // group of the resource (e.g: "dashboard.grafana.app") + string group = 1; + // kind of the resource (e.g: "dashboards") + string resource = 2; + // uid of the resource + string name = 3; +} + +message Permission { + // kind of the identity getting the permission (e.g: "user", "team", "serviceaccount") + string kind = 1; + // uid of the identity getting the permission + string name = 2; + // action set granted to the user (e.g. "admin" or "edit", "view") + string verb = 3; } message TupleKey { diff --git a/pkg/services/authz/proto/v1/extention_grpc.pb.go b/pkg/services/authz/proto/v1/extention_grpc.pb.go index 78eb6e30b58..f83b14c1c8d 100644 --- a/pkg/services/authz/proto/v1/extention_grpc.pb.go +++ b/pkg/services/authz/proto/v1/extention_grpc.pb.go @@ -22,6 +22,7 @@ const ( AuthzExtentionService_BatchCheck_FullMethodName = "/authz.extention.v1.AuthzExtentionService/BatchCheck" AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read" AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write" + AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate" ) // AuthzExtentionServiceClient is the client API for AuthzExtentionService service. @@ -31,6 +32,7 @@ type AuthzExtentionServiceClient interface { BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error) + Mutate(ctx context.Context, in *MutateRequest, opts ...grpc.CallOption) (*MutateResponse, error) } type authzExtentionServiceClient struct { @@ -71,6 +73,16 @@ func (c *authzExtentionServiceClient) Write(ctx context.Context, in *WriteReques return out, nil } +func (c *authzExtentionServiceClient) Mutate(ctx context.Context, in *MutateRequest, opts ...grpc.CallOption) (*MutateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MutateResponse) + err := c.cc.Invoke(ctx, AuthzExtentionService_Mutate_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 @@ -78,6 +90,7 @@ type AuthzExtentionServiceServer interface { BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) Read(context.Context, *ReadRequest) (*ReadResponse, error) Write(context.Context, *WriteRequest) (*WriteResponse, error) + Mutate(context.Context, *MutateRequest) (*MutateResponse, error) } // UnimplementedAuthzExtentionServiceServer should be embedded to have forward compatible implementations. @@ -93,6 +106,9 @@ func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadReque func (UnimplementedAuthzExtentionServiceServer) Write(context.Context, *WriteRequest) (*WriteResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Write not implemented") } +func (UnimplementedAuthzExtentionServiceServer) Mutate(context.Context, *MutateRequest) (*MutateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Mutate 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 @@ -159,6 +175,24 @@ func _AuthzExtentionService_Write_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _AuthzExtentionService_Mutate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthzExtentionServiceServer).Mutate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthzExtentionService_Mutate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthzExtentionServiceServer).Mutate(ctx, req.(*MutateRequest)) + } + 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) @@ -178,6 +212,10 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Write", Handler: _AuthzExtentionService_Write_Handler, }, + { + MethodName: "Mutate", + Handler: _AuthzExtentionService_Mutate_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "extention.proto", diff --git a/pkg/services/authz/rbac.go b/pkg/services/authz/rbac.go index ed58f7ed686..349fd0d4013 100644 --- a/pkg/services/authz/rbac.go +++ b/pkg/services/authz/rbac.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/services/authz/rbac" "github.com/grafana/grafana/pkg/services/authz/rbac/store" "github.com/grafana/grafana/pkg/services/authz/zanzana" + zClient "github.com/grafana/grafana/pkg/services/authz/zanzana/client" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/setting" @@ -84,7 +85,7 @@ func ProvideAuthZClient( case clientModeCloud: rbacClient, err := newRemoteRBACClient(authCfg, tracer, reg) if zanzanaEnabled { - return zanzana.WithShadowClient(rbacClient, zanzanaClient, reg) + return zClient.WithShadowClient(rbacClient, zanzanaClient, reg) } return rbacClient, err default: @@ -131,7 +132,7 @@ func ProvideAuthZClient( ) if zanzanaEnabled { - return zanzana.WithShadowClient(rbacClient, zanzanaClient, reg) + return zClient.WithShadowClient(rbacClient, zanzanaClient, reg) } return rbacClient, nil diff --git a/pkg/services/authz/wireset.go b/pkg/services/authz/wireset.go index 4a46f15ea3b..6540413325b 100644 --- a/pkg/services/authz/wireset.go +++ b/pkg/services/authz/wireset.go @@ -6,5 +6,5 @@ import ( var WireSet = wire.NewSet( ProvideAuthZClient, - ProvideZanzana, + ProvideZanzanaClient, ) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 36abf4779c1..37fa8c7b59d 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -24,26 +24,26 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana" + zClient "github.com/grafana/grafana/pkg/services/authz/zanzana/client" + zServer "github.com/grafana/grafana/pkg/services/authz/zanzana/server" + zStore "github.com/grafana/grafana/pkg/services/authz/zanzana/store" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/setting" ) -// ProvideZanzana used to register ZanzanaClient. +// ProvideZanzanaClient used to register ZanzanaClient. // It will also start an embedded ZanzanaSever if mode is set to "embedded". -func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (zanzana.Client, error) { +func ProvideZanzanaClient(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (zanzana.Client, error) { //nolint:staticcheck // not yet migrated to OpenFeature if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) { - return zanzana.NewNoopClient(), nil + return zClient.NewNoopClient(), nil } - logger := log.New("zanzana.server") - - var client zanzana.Client switch cfg.ZanzanaClient.Mode { case setting.ZanzanaModeClient: - return NewZanzanaClient( + return NewRemoteZanzanaClient( fmt.Sprintf("stacks-%s", cfg.StackID), ZanzanaClientConfig{ URL: cfg.ZanzanaClient.Addr, @@ -51,18 +51,20 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL, ServerCertFile: cfg.ZanzanaClient.ServerCertFile, }) + case setting.ZanzanaModeEmbedded: - store, err := zanzana.NewEmbeddedStore(cfg, db, logger) + logger := log.New("zanzana.server") + store, err := zStore.NewEmbeddedStore(cfg, db, logger) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } - openfga, err := zanzana.NewOpenFGAServer(cfg.ZanzanaServer, store) + openfga, err := zServer.NewOpenFGAServer(cfg.ZanzanaServer, store) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } - srv, err := zanzana.NewServer(cfg.ZanzanaServer, openfga, logger, tracer, reg) + srv, err := zServer.NewServer(cfg.ZanzanaServer, openfga, logger, tracer, reg) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } @@ -82,16 +84,15 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features authzv1.RegisterAuthzServiceServer(channel, srv) authzextv1.RegisterAuthzExtentionServiceServer(channel, srv) - client, err = zanzana.NewClient(channel) + client, err := zClient.New(channel) if err != nil { return nil, fmt.Errorf("failed to initialize zanzana client: %w", err) } + return client, nil default: return nil, fmt.Errorf("unsupported zanzana mode: %s", cfg.ZanzanaClient.Mode) } - - return client, nil } type ZanzanaClientConfig struct { @@ -101,7 +102,8 @@ type ZanzanaClientConfig struct { ServerCertFile string } -func NewZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client, error) { +// NewRemoteZanzanaClient creates a new Zanzana client that connects to remote Zanzana server. +func NewRemoteZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client, error) { tokenClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{ Token: cfg.Token, TokenExchangeURL: cfg.TokenExchangeURL, @@ -134,7 +136,7 @@ func NewZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err) } - client, err := zanzana.NewClient(conn) + client, err := zClient.New(conn) if err != nil { return nil, fmt.Errorf("failed to initialize zanzana client: %w", err) } @@ -186,17 +188,17 @@ func (z *Zanzana) start(ctx context.Context) error { return err } - store, err := zanzana.NewStore(z.cfg, z.logger) + store, err := zStore.NewStore(z.cfg, z.logger) if err != nil { return fmt.Errorf("failed to initilize zanana store: %w", err) } - openfgaServer, err := zanzana.NewOpenFGAServer(z.cfg.ZanzanaServer, store) + openfgaServer, err := zServer.NewOpenFGAServer(z.cfg.ZanzanaServer, store) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } - zanzanaServer, err := zanzana.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger, tracer, z.reg) + zanzanaServer, err := zServer.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger, tracer, z.reg) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } @@ -240,7 +242,7 @@ func (z *Zanzana) start(ctx context.Context) error { authzextv1.RegisterAuthzExtentionServiceServer(grpcServer, zanzanaServer) // register grpc health server - healthServer := zanzana.NewHealthServer(zanzanaServer) + healthServer := zServer.NewHealthServer(zanzanaServer) healthv1pb.RegisterHealthServer(grpcServer, healthServer) if _, err := grpcserver.ProvideReflectionService(z.cfg, z.handle); err != nil { @@ -253,7 +255,7 @@ func (z *Zanzana) start(ctx context.Context) error { func (z *Zanzana) running(ctx context.Context) error { if z.cfg.Env == setting.Dev && z.cfg.ZanzanaServer.OpenFGAHttpAddr != "" { go func() { - srv, err := zanzana.NewOpenFGAHttpServer(z.cfg.ZanzanaServer, z.handle) + srv, err := zServer.NewOpenFGAHttpServer(z.cfg.ZanzanaServer, z.handle) if err != nil { z.logger.Error("failed to create OpenFGA HTTP server", "error", err) } else { diff --git a/pkg/services/authz/zanzana/client.go b/pkg/services/authz/zanzana/client.go index 40b672bf759..a95ead698cb 100644 --- a/pkg/services/authz/zanzana/client.go +++ b/pkg/services/authz/zanzana/client.go @@ -3,13 +3,9 @@ package zanzana import ( "context" - "google.golang.org/grpc" - authlib "github.com/grafana/authlib/types" - "github.com/prometheus/client_golang/prometheus" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana/client" ) // Client is a wrapper around [openfgav1.OpenFGAServiceClient] @@ -18,16 +14,6 @@ type Client interface { Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) Write(ctx context.Context, req *authzextv1.WriteRequest) error BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) -} -func NewClient(cc grpc.ClientConnInterface) (*client.Client, error) { - return client.New(cc) -} - -func WithShadowClient(accessClient authlib.AccessClient, zanzanaClient authlib.AccessClient, reg prometheus.Registerer) (authlib.AccessClient, error) { - return client.WithShadowClient(accessClient, zanzanaClient, reg), nil -} - -func NewNoopClient() *client.NoopClient { - return client.NewNoop() + Mutate(ctx context.Context, req *authzextv1.MutateRequest) error } diff --git a/pkg/services/authz/zanzana/client/client.go b/pkg/services/authz/zanzana/client/client.go index 05266afba83..68809a84fa8 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -12,9 +12,11 @@ import ( "github.com/grafana/grafana/pkg/infra/log" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" ) var _ authlib.AccessClient = (*Client)(nil) +var _ zanzana.Client = (*Client)(nil) var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/client") @@ -72,3 +74,19 @@ func (c *Client) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckReque return c.authzext.BatchCheck(ctx, req) } + +func (c *Client) WriteNew(ctx context.Context, req *authzextv1.WriteRequest) error { + ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Write") + defer span.End() + + _, err := c.authzext.Write(ctx, req) + return err +} + +func (c *Client) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { + ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Mutate") + defer span.End() + + _, err := c.authzext.Mutate(ctx, req) + return err +} diff --git a/pkg/services/authz/zanzana/client/noop.go b/pkg/services/authz/zanzana/client/noop.go index 419a9b73201..d0397740b5e 100644 --- a/pkg/services/authz/zanzana/client/noop.go +++ b/pkg/services/authz/zanzana/client/noop.go @@ -6,11 +6,13 @@ import ( authlib "github.com/grafana/authlib/types" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" ) var _ authlib.AccessClient = (*NoopClient)(nil) +var _ zanzana.Client = (*NoopClient)(nil) -func NewNoop() *NoopClient { +func NewNoopClient() *NoopClient { return &NoopClient{} } @@ -35,3 +37,7 @@ func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) er func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { return nil, nil } + +func (nc NoopClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { + return nil +} diff --git a/pkg/services/authz/zanzana/client/shadow_client.go b/pkg/services/authz/zanzana/client/shadow_client.go index 4fb4c545ff0..f2f0ec7d4f1 100644 --- a/pkg/services/authz/zanzana/client/shadow_client.go +++ b/pkg/services/authz/zanzana/client/shadow_client.go @@ -6,6 +6,7 @@ import ( "github.com/prometheus/client_golang/prometheus" authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/log" ) @@ -19,14 +20,14 @@ type ShadowClient struct { } // WithShadowClient returns a new access client that runs zanzana checks in the background. -func WithShadowClient(accessClient authlib.AccessClient, zanzanaClient authlib.AccessClient, reg prometheus.Registerer) authlib.AccessClient { +func WithShadowClient(accessClient authlib.AccessClient, zanzanaClient authlib.AccessClient, reg prometheus.Registerer) (authlib.AccessClient, error) { client := &ShadowClient{ logger: log.New("zanzana-shadow-client"), accessClient: accessClient, zanzanaClient: zanzanaClient, metrics: newShadowClientMetrics(reg), } - return client + return client, nil } func (c *ShadowClient) Check(ctx context.Context, id authlib.AuthInfo, req authlib.CheckRequest, folder string) (authlib.CheckResponse, error) { diff --git a/pkg/services/authz/zanzana/translations.go b/pkg/services/authz/zanzana/common/translations.go similarity index 67% rename from pkg/services/authz/zanzana/translations.go rename to pkg/services/authz/zanzana/common/translations.go index 0abefcf2e29..ce5625d698b 100644 --- a/pkg/services/authz/zanzana/translations.go +++ b/pkg/services/authz/zanzana/common/translations.go @@ -1,6 +1,8 @@ -package zanzana +package common import ( + authlib "github.com/grafana/authlib/types" + dashboards "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" ) @@ -79,3 +81,60 @@ var resourceTranslations = map[string]resourceTranslation{ }, }, } + +func TranslateToCheckRequest(namespace, action, kind, name string) (*authlib.CheckRequest, bool) { + translation, ok := resourceTranslations[kind] + + if !ok { + return nil, false + } + + m, ok := translation.mapping[action] + if !ok { + return nil, false + } + + verb, ok := RelationToVerbMapping[m.relation] + if !ok { + return nil, false + } + + req := &authlib.CheckRequest{ + Namespace: namespace, + Verb: verb, + Group: translation.group, + Resource: translation.resource, + Name: name, + } + + return req, true +} + +func TranslateToListRequest(namespace, action, kind string) (*authlib.ListRequest, bool) { + translation, ok := resourceTranslations[kind] + + if !ok { + return nil, false + } + + // FIXME: support different verbs + req := &authlib.ListRequest{ + Namespace: namespace, + Group: translation.group, + Resource: translation.resource, + } + + return req, true +} + +func TranslateToGroupResource(kind string) string { + translation, ok := resourceTranslations[kind] + if !ok { + return "" + } + return FormatGroupResource(translation.group, translation.resource, "") +} + +func TranslateBasicRole(name string) string { + return basicRolesTranslations[name] +} diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index 41ed7c2a85b..9349453054c 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -1,12 +1,14 @@ package common import ( + "fmt" "strings" openfgav1 "github.com/openfga/api/proto/openfga/v1" "google.golang.org/protobuf/types/known/structpb" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) @@ -33,6 +35,11 @@ const ( TypeTeamPrefix string = TypeTeam + ":" ) +const ( + KindDashboards string = dashboardV1.DASHBOARD_RESOURCE + KindFolders string = folderV1.RESOURCE +) + const ( RelationTeamMember string = "member" RelationTeamAdmin string = "admin" @@ -144,6 +151,10 @@ func isValidRelation(relation string, valid []string) bool { return false } +func IsFolderResourceTuple(t *openfgav1.TupleKey) bool { + return strings.HasPrefix(t.Object, TypeFolder) && strings.HasPrefix(t.Relation, "resource_") +} + func SubresourceRelation(relation string) string { return TypeResource + "_" + relation } @@ -178,6 +189,69 @@ func FormatGroupResource(group, resource, subresource string) string { return b.String() } +// NewTupleEntry constructs new openfga entry type:name[#relation]. +// Relation allows to specify group of users (subjects) related to type:name +// (for example, team:devs#member refers to users which are members of team devs) +func NewTupleEntry(objectType, name, relation string) string { + obj := fmt.Sprintf("%s:%s", objectType, name) + if relation != "" { + obj = fmt.Sprintf("%s#%s", obj, relation) + } + return obj +} + +func NewObjectEntry(objectType, group, resource, subresource, name string) string { + if objectType == TypeFolder { + return TypeFolder + ":" + name + } + + obj := fmt.Sprintf("%s:%s/%s", objectType, group, resource) + if subresource != "" { + obj = fmt.Sprintf("%s/%s", obj, subresource) + } + if name != "" { + obj = fmt.Sprintf("%s/%s", obj, name) + } + return obj +} + +func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) { + translation, ok := resourceTranslations[kind] + + if !ok { + return nil, false + } + + m, ok := translation.mapping[action] + if !ok { + return nil, false + } + + if name == "*" { + return NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource), true + } + + if translation.typ == TypeResource { + return NewResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource, name), true + } + + if translation.typ == TypeFolder { + if m.group != "" && m.resource != "" { + return NewFolderResourceTuple(subject, m.relation, m.group, m.resource, m.subresource, name), true + } + + return NewFolderTuple(subject, m.relation, name), true + } + + return NewTypedTuple(translation.typ, subject, m.relation, name), true +} + +func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) { + va := a.Condition.Context.Fields["subresources"] + vb := b.Condition.Context.Fields["subresources"] + va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...) +} + func NewResourceTuple(subject, relation, group, resource, subresource, name string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ User: subject, @@ -200,6 +274,18 @@ func isSubresourceRelationSet(relation string) bool { relation == RelationSubresourceSetAdmin } +func NewFolderParentTuple(folder, parent string) *openfgav1.TupleKey { + return &openfgav1.TupleKey{ + Object: NewFolderIdent(folder), + Relation: RelationParent, + User: NewFolderIdent(parent), + } +} + +func NewFolderTuple(subject, relation, name string) *openfgav1.TupleKey { + return NewTypedTuple(TypeFolder, subject, relation, name) +} + func NewFolderResourceTuple(subject, relation, group, resource, subresource, folder string) *openfgav1.TupleKey { relation = SubresourceRelation(relation) var condition *openfgav1.RelationshipCondition @@ -256,18 +342,6 @@ func NewGroupResourceTuple(subject, relation, group, resource, subresource strin } } -func NewFolderParentTuple(folder, parent string) *openfgav1.TupleKey { - return &openfgav1.TupleKey{ - Object: NewFolderIdent(folder), - Relation: RelationParent, - User: NewFolderIdent(parent), - } -} - -func NewFolderTuple(subject, relation, name string) *openfgav1.TupleKey { - return NewTypedTuple(TypeFolder, subject, relation, name) -} - func NewTypedTuple(typ, subject, relation, name string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ User: subject, diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go index 91a9800efbd..3505f0dcec1 100644 --- a/pkg/services/authz/zanzana/server.go +++ b/pkg/services/authz/zanzana/server.go @@ -1,31 +1 @@ package zanzana - -import ( - "net/http" - - openfgaserver "github.com/openfga/openfga/pkg/server" - openfgastorage "github.com/openfga/openfga/pkg/storage" - "github.com/prometheus/client_golang/prometheus" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/authz/zanzana/server" - "github.com/grafana/grafana/pkg/services/grpcserver" - "github.com/grafana/grafana/pkg/setting" -) - -func NewServer(cfg setting.ZanzanaServerSettings, openfga server.OpenFGAServer, logger log.Logger, tracer tracing.Tracer, reg prometheus.Registerer) (*server.Server, error) { - return server.NewServer(cfg, openfga, logger, tracer, reg) -} - -func NewHealthServer(target server.DiagnosticServer) *server.HealthServer { - return server.NewHealthServer(target) -} - -func NewOpenFGAServer(cfg setting.ZanzanaServerSettings, store openfgastorage.OpenFGADatastore) (*openfgaserver.Server, error) { - return server.NewOpenFGAServer(cfg, store) -} - -func NewOpenFGAHttpServer(cfg setting.ZanzanaServerSettings, srv grpcserver.Provider) (*http.Server, error) { - return server.NewOpenFGAHttpServer(cfg, srv) -} diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go new file mode 100644 index 00000000000..348d9512e72 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -0,0 +1,90 @@ +package server + +import ( + "context" + "errors" + "fmt" + "time" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +type OperationGroup string + +const ( + OperationGroupFolder OperationGroup = "folder" + OperationGroupPermission OperationGroup = "permission" +) + +func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { + ctx, span := s.tracer.Start(ctx, "server.Mutate") + defer span.End() + + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.Mutate", req.GetNamespace()).Observe(time.Since(t).Seconds()) + }(time.Now()) + + res, err := s.mutate(ctx, req) + if err != nil { + s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace()) + return nil, errors.New("failed to perform mutate request") + } + + return res, nil +} + +func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { + if err := authorize(ctx, req.GetNamespace(), s.cfg); err != nil { + return nil, err + } + + storeInf, err := s.getStoreInfo(ctx, req.Namespace) + if err != nil { + return nil, fmt.Errorf("failed to get openfga store: %w", err) + } + + groupedOperations, err := groupByOperation(req.GetOperations()) + if err != nil { + return nil, fmt.Errorf("failed to group operations: %w", err) + } + + for operationGroup, operations := range groupedOperations { + switch operationGroup { + case OperationGroupFolder: + if err := s.mutateFolders(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate folder: %w", err) + } + case OperationGroupPermission: + if err := s.mutateResourcePermissions(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate resource permissions: %w", err) + } + default: + s.logger.Warn("unsupported operation group", "operationGroup", operationGroup) + } + } + + return &authzextv1.MutateResponse{}, nil +} + +func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, error) { + switch operation.Operation.(type) { + case *authzextv1.MutateOperation_SetFolderParent, *authzextv1.MutateOperation_DeleteFolder: + return OperationGroupFolder, nil + case *authzextv1.MutateOperation_CreatePermission, *authzextv1.MutateOperation_DeletePermission: + return OperationGroupPermission, nil + } + return OperationGroup(""), errors.New("unsupported mutate operation type") +} + +func groupByOperation(operations []*authzextv1.MutateOperation) (map[OperationGroup][]*authzextv1.MutateOperation, error) { + grouped := make(map[OperationGroup][]*authzextv1.MutateOperation) + for _, operation := range operations { + operationGroup, err := getOperationGroup(operation) + if err != nil { + return nil, err + } + grouped[operationGroup] = append(grouped[operationGroup], operation) + } + + return grouped, nil +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_folder.go b/pkg/services/authz/zanzana/server/server_mutate_folder.go new file mode 100644 index 00000000000..3d92347f404 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_folder.go @@ -0,0 +1,142 @@ +package server + +import ( + "context" + "fmt" + "strings" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func (s *Server) mutateFolders(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateFolder") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_SetFolderParent: + tuple, err := s.getFolderWriteTuple(ctx, store, op.SetFolderParent) + if err != nil { + return err + } + if tuple != nil { + writeTuples = append(writeTuples, tuple) + } + + // Delete existing parent tuples + if op.SetFolderParent.GetDeleteExisting() { + tuples, err := s.getFolderDeleteTuples(ctx, store, op.SetFolderParent.GetFolder(), op.SetFolderParent.GetParent(), true) + if err != nil { + return err + } + deleteTuples = append(deleteTuples, tuples...) + } + case *authzextv1.MutateOperation_DeleteFolder: + tuples, err := s.getFolderDeleteTuples(ctx, store, op.DeleteFolder.GetFolder(), op.DeleteFolder.GetParent(), op.DeleteFolder.GetDeleteExisting()) + if err != nil { + return err + } + deleteTuples = append(deleteTuples, tuples...) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + if len(writeTuples) == 0 && len(deleteTuples) == 0 { + return nil + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write folder tuples", "error", err) + return err + } + + return nil +} + +func (s *Server) getFolderWriteTuple(ctx context.Context, store *storeInfo, req *authzextv1.SetFolderParentOperation) (*openfgav1.TupleKey, error) { + // Folder is at the root level + if req.GetParent() == "" { + return nil, nil + } + + if strings.ContainsAny(req.GetFolder(), "#:") { + return nil, fmt.Errorf("folder UID contains invalid characters: %s", req.GetFolder()) + } + + tuple := zanzana.NewFolderParentTuple(req.GetFolder(), req.GetParent()) + return tuple, nil +} + +func (s *Server) getFolderDeleteTuples(ctx context.Context, store *storeInfo, folderUID string, parentUID string, deleteExisting bool) ([]*openfgav1.TupleKeyWithoutCondition, error) { + tupleKeysToDelete := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + if folderUID != "" && parentUID != "" && !deleteExisting { + tuple := zanzana.NewFolderParentTuple(folderUID, parentUID) + tupleKeysToDelete = append(tupleKeysToDelete, &openfgav1.TupleKeyWithoutCondition{ + User: tuple.GetUser(), + Relation: tuple.GetRelation(), + Object: tuple.GetObject(), + }) + } + + if deleteExisting { + parentTuples, err := s.listFolderParents(ctx, store, folderUID) + if err != nil { + return nil, fmt.Errorf("failed to list folder parents: %w", err) + } + + for _, tuple := range parentTuples { + tupleKeysToDelete = append(tupleKeysToDelete, &openfgav1.TupleKeyWithoutCondition{ + User: tuple.Key.User, + Relation: tuple.Key.Relation, + Object: tuple.Key.Object, + }) + } + } + + return tupleKeysToDelete, nil +} + +func (s *Server) listFolderParents(ctx context.Context, store *storeInfo, folderUID string) ([]*openfgav1.Tuple, error) { + ctx, span := s.tracer.Start(ctx, "server.listFolderParents") + defer span.End() + + object := zanzana.NewFolderIdent(folderUID) + resp, err := s.openfga.Read(ctx, &openfgav1.ReadRequest{ + StoreId: store.ID, + TupleKey: &openfgav1.ReadRequestTupleKey{ + Object: object, + Relation: zanzana.RelationParent, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to list folder parents: %w", err) + } + + return resp.Tuples, nil +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_folder_test.go b/pkg/services/authz/zanzana/server/server_mutate_folder_test.go new file mode 100644 index 00000000000..a01c3b08740 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_folder_test.go @@ -0,0 +1,164 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateFolders(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewFolderParentTuple("11", "1"), + common.NewFolderParentTuple("12", "1"), + common.NewFolderParentTuple("111", "11"), + common.NewFolderParentTuple("112", "11"), + common.NewFolderParentTuple("broken", "foo"), + common.NewFolderParentTuple("broken", "bar"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateFolders(t *testing.T, srv *Server) { + setupMutateFolders(t, srv) + + t.Run("should create new folder parent relation", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_SetFolderParent{ + SetFolderParent: &v1.SetFolderParentOperation{ + Folder: "new-folder", + Parent: "1", + DeleteExisting: false, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:new-folder", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "folder:new-folder", res.Tuples[0].Key.Object) + require.Equal(t, "parent", res.Tuples[0].Key.Relation) + require.Equal(t, "folder:1", res.Tuples[0].Key.User) + }) + + t.Run("should delete folder parent relation", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeleteFolder{ + DeleteFolder: &v1.DeleteFolderOperation{ + Folder: "11", + Parent: "1", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:11", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) + + t.Run("should clean up all parent relations", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeleteFolder{ + DeleteFolder: &v1.DeleteFolderOperation{ + Folder: "broken", + DeleteExisting: true, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:broken", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) + + t.Run("should perform batch mutate if multiple operations are provided", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_SetFolderParent{ + SetFolderParent: &v1.SetFolderParentOperation{ + Folder: "new-folder-2", + Parent: "1", + }, + }, + }, + { + Operation: &v1.MutateOperation_DeleteFolder{ + DeleteFolder: &v1.DeleteFolderOperation{ + Folder: "12", + Parent: "1", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:new-folder-2", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "folder:new-folder-2", res.Tuples[0].Key.Object) + require.Equal(t, "parent", res.Tuples[0].Key.Relation) + require.Equal(t, "folder:1", res.Tuples[0].Key.User) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:12", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go new file mode 100644 index 00000000000..fa8b5467235 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go @@ -0,0 +1,173 @@ +package server + +import ( + "context" + "errors" + "fmt" + "strings" + + "google.golang.org/protobuf/types/known/structpb" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +var ( + errEmptyName = errors.New("name cannot be empty") + errInvalidBasicRole = errors.New("invalid basic role") + errUnknownKind = errors.New("unknown permission kind") +) + +func (s *Server) mutateResourcePermissions(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateResourcePermissions") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_CreatePermission: + tuple, err := s.getPermissionWriteTuple(ctx, op.CreatePermission) + if err != nil { + return err + } + writeTuples = append(writeTuples, tuple) + case *authzextv1.MutateOperation_DeletePermission: + tuple, err := s.getPermissionDeleteTuple(ctx, op.DeletePermission) + if err != nil { + return err + } + deleteTuples = append(deleteTuples, tuple) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write resource permission tuples", "error", err) + return err + } + + return nil +} + +func (s *Server) getPermissionWriteTuple(ctx context.Context, req *authzextv1.CreatePermissionOperation) (*openfgav1.TupleKey, error) { + resource := req.GetResource() + permission := req.GetPermission() + object := zanzana.NewObjectEntry(toZanzanaType(resource.GetGroup()), resource.GetGroup(), resource.GetResource(), "", resource.GetName()) + tuple, err := NewResourceTuple(object, resource, permission) + if err != nil { + return nil, err + } + + return tuple, nil +} + +func (s *Server) getPermissionDeleteTuple(ctx context.Context, req *authzextv1.DeletePermissionOperation) (*openfgav1.TupleKeyWithoutCondition, error) { + resource := req.GetResource() + permission := req.GetPermission() + object := zanzana.NewObjectEntry(toZanzanaType(resource.GetGroup()), resource.GetGroup(), resource.GetResource(), "", resource.GetName()) + tuple, err := NewResourceTuple(object, resource, permission) + if err != nil { + return nil, err + } + + return &openfgav1.TupleKeyWithoutCondition{ + User: tuple.GetUser(), + Relation: tuple.GetRelation(), + Object: tuple.GetObject(), + }, nil +} + +func toZanzanaType(apiGroup string) string { + if apiGroup == "folder.grafana.app" { + return zanzana.TypeFolder + } + return zanzana.TypeResource +} + +func NewResourceTuple(object string, resource *authzextv1.Resource, perm *authzextv1.Permission) (*openfgav1.TupleKey, error) { + // Typ is "folder" or "resource" + typ := toZanzanaType(resource.Group) + + // subject + subject, err := toZanzanaSubject(perm.GetKind(), perm.GetName()) + if err != nil { + return nil, err + } + + key := &openfgav1.TupleKey{ + // e.g. "user:{uid}", "serviceaccount:{uid}", "team:{uid}", "basicrole:{viewer|editor|admin}" + User: subject, + // "view", "edit", "admin" + Relation: strings.ToLower(perm.Verb), + // e.g. "folder:{name}" or "resource:{apiGroup}/{resource}/{name}" + Object: object, + } + + // For resources we add a condition to filter by apiGroup/resource + // e.g "group_filter": {"group_resource": "dashboards.grafana.app/dashboards"} + if typ == zanzana.TypeResource { + key.Condition = &openfgav1.RelationshipCondition{ + Name: "group_filter", + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "group_resource": structpb.NewStringValue( + resource.GetGroup() + "/" + resource.GetResource(), + ), + }, + }, + } + } + + return key, nil +} + +func toZanzanaSubject(kind string, name string) (string, error) { + if name == "" { + return "", errEmptyName + } + iamKind := iamv0.ResourcePermissionSpecPermissionKind(kind) + switch iamKind { + case iamv0.ResourcePermissionSpecPermissionKindUser: + return zanzana.NewTupleEntry(zanzana.TypeUser, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindServiceAccount: + return zanzana.NewTupleEntry(zanzana.TypeServiceAccount, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindTeam: + return zanzana.NewTupleEntry(zanzana.TypeTeam, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindBasicRole: + basicRole := zanzana.TranslateBasicRole(name) + if basicRole == "" { + return "", fmt.Errorf("%w: %s", errInvalidBasicRole, name) + } + + // e.g role:basic_viewer#assignee + return zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, zanzana.RelationAssignee), nil + } + + // should not happen since we are after create + // validation webhook should have caught invalid kinds + return "", errUnknownKind +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go new file mode 100644 index 00000000000..3336d9d813b --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go @@ -0,0 +1,115 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateResourcePermissions(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), + common.NewTypedResourceTuple("user:2", common.RelationGet, common.TypeFolder, folderGroup, folderResource, "", "1"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateResourcePermissions(t *testing.T, srv *Server) { + setupMutateResourcePermissions(t, srv) + + t.Run("should create new resource permission", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "foo", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "bar", + Verb: common.RelationGet, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationGet, + Object: "resource:dashboard.grafana.app/dashboards/foo", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:bar", res.Tuples[0].Key.User) + require.Equal(t, common.RelationGet, res.Tuples[0].Key.Relation) + require.Equal(t, "resource:dashboard.grafana.app/dashboards/foo", res.Tuples[0].Key.Object) + }) + + t.Run("should delete resource permission", func(t *testing.T) { + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + User: "user:1", + Object: "resource:dashboard.grafana.app/dashboards/1", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 2) + + _, err = srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "1", + Verb: common.RelationUpdate, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationGet, + Object: "resource:dashboard.grafana.app/dashboards/1", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:1", res.Tuples[0].Key.User) + require.Equal(t, common.RelationGet, res.Tuples[0].Key.Relation) + require.Equal(t, "resource:dashboard.grafana.app/dashboards/1", res.Tuples[0].Key.Object) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_test.go b/pkg/services/authz/zanzana/server/server_mutate_test.go new file mode 100644 index 00000000000..70dc1ea2fb8 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_test.go @@ -0,0 +1,135 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutate(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewFolderParentTuple("11", "1"), + common.NewFolderParentTuple("12", "1"), + common.NewFolderParentTuple("111", "11"), + common.NewFolderParentTuple("112", "11"), + common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), + common.NewTypedResourceTuple("user:2", common.RelationGet, common.TypeFolder, folderGroup, folderResource, "", "1"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutate(t *testing.T, srv *Server) { + setupMutate(t, srv) + + t.Run("should perform multiple mutate operations", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_SetFolderParent{ + SetFolderParent: &v1.SetFolderParentOperation{ + Folder: "new-folder", + Parent: "1", + DeleteExisting: false, + }, + }, + }, + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "foo", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "bar", + Verb: common.RelationGet, + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "1", + Verb: common.RelationUpdate, + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "1", + Verb: common.RelationGet, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:new-folder", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "folder:new-folder", res.Tuples[0].Key.Object) + require.Equal(t, "parent", res.Tuples[0].Key.Relation) + require.Equal(t, "folder:1", res.Tuples[0].Key.User) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationGet, + Object: "resource:dashboard.grafana.app/dashboards/foo", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:bar", res.Tuples[0].Key.User) + require.Equal(t, common.RelationGet, res.Tuples[0].Key.Relation) + require.Equal(t, "resource:dashboard.grafana.app/dashboards/foo", res.Tuples[0].Key.Object) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationGet, + Object: "resource:dashboard.grafana.app/dashboards/1", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 1a9fe263cee..555b9d928cd 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -44,6 +44,36 @@ const ( statusSubresource = "status" ) +func setup(t *testing.T, srv *Server) *Server { + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), + common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""), + common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""), + common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"), + common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderTuple("user:6", common.RelationGet, "1"), + common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""), + common.NewFolderParentTuple("5", "4"), + common.NewFolderParentTuple("6", "5"), + common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"), + common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"), + common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"), + common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"), + common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource), + common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"), + common.NewFolderResourceTuple("user:13", common.RelationGet, folderGroup, folderResource, statusSubresource, "5"), + common.NewTypedResourceTuple("user:14", common.RelationGet, common.TypeTeam, teamGroup, teamResource, statusSubresource, "1"), + common.NewTypedResourceTuple("user:15", common.RelationGet, common.TypeUser, userGroup, userResource, statusSubresource, "1"), + common.NewTypedResourceTuple("user:16", common.RelationGet, common.TypeServiceAccount, serviceAccountGroup, serviceAccountResource, statusSubresource, "1"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + func TestMain(m *testing.M) { testsuite.Run(m) } @@ -64,27 +94,43 @@ func TestIntegrationServer(t *testing.T) { } } - srv := setup(t, testStore, cfg) + srv := setupOpenFGAServer(t, testStore, cfg) t.Run("test check", func(t *testing.T) { + setup(t, srv) testCheck(t, srv) }) t.Run("test list", func(t *testing.T) { + setup(t, srv) testList(t, srv) }) t.Run("test list streaming", func(t *testing.T) { + setup(t, srv) srv.cfg.UseStreamedListObjects = true testList(t, srv) srv.cfg.UseStreamedListObjects = false }) t.Run("test batch check", func(t *testing.T) { + setup(t, srv) testBatchCheck(t, srv) }) + + t.Run("test mutate", func(t *testing.T) { + testMutate(t, srv) + }) + + t.Run("test mutate folders", func(t *testing.T) { + testMutateFolders(t, srv) + }) + + t.Run("test mutate resource permissions", func(t *testing.T) { + testMutateResourcePermissions(t, srv) + }) } -func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { +func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { t.Helper() store, err := store.NewEmbeddedStore(cfg, testDB, log.NewNopLogger()) @@ -95,38 +141,25 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService(), prometheus.NewRegistry()) require.NoError(t, err) + return srv +} + +func setupOpenFGADatabase(t *testing.T, srv *Server, tuples []*openfgav1.TupleKey) *Server { + t.Helper() + storeInf, err := srv.getStoreInfo(context.Background(), namespace) require.NoError(t, err) + // Clean up any existing store + _, err = srv.openfga.DeleteStore(context.Background(), &openfgav1.DeleteStoreRequest{ + StoreId: storeInf.ID, + }) + require.NoError(t, err) + // seed tuples writes := &openfgav1.WriteRequestWrites{ - TupleKeys: []*openfgav1.TupleKey{ - common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), - common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), - common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""), - common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""), - common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"), - common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderTuple("user:6", common.RelationGet, "1"), - common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""), - common.NewFolderParentTuple("5", "4"), - common.NewFolderParentTuple("6", "5"), - common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"), - common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"), - common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"), - common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"), - common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource), - common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"), - common.NewFolderResourceTuple("user:13", common.RelationGet, folderGroup, folderResource, statusSubresource, "5"), - common.NewTypedResourceTuple("user:14", common.RelationGet, common.TypeTeam, teamGroup, teamResource, statusSubresource, "1"), - common.NewTypedResourceTuple("user:15", common.RelationGet, common.TypeUser, userGroup, userResource, statusSubresource, "1"), - common.NewTypedResourceTuple("user:16", common.RelationGet, common.TypeServiceAccount, serviceAccountGroup, serviceAccountResource, statusSubresource, "1"), - }, - } - for _, w := range writes.TupleKeys { - t.Log(w.String()) + TupleKeys: tuples, + OnDuplicate: "ignore", } // First, try to delete any existing tuples to avoid conflicts @@ -140,16 +173,18 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { } // Try to delete existing tuples (ignore errors if they don't exist) - _, _ = openfga.Write(context.Background(), &openfgav1.WriteRequest{ + _, err = srv.openfga.Write(context.Background(), &openfgav1.WriteRequest{ StoreId: storeInf.ID, AuthorizationModelId: storeInf.ModelID, Deletes: &openfgav1.WriteRequestDeletes{ TupleKeys: deletes, + OnMissing: "ignore", }, }) + require.NoError(t, err) // Now write the new tuples - _, err = openfga.Write(context.Background(), &openfgav1.WriteRequest{ + _, err = srv.openfga.Write(context.Background(), &openfgav1.WriteRequest{ StoreId: storeInf.ID, AuthorizationModelId: storeInf.ModelID, Writes: writes, diff --git a/pkg/services/authz/zanzana/store.go b/pkg/services/authz/zanzana/store.go deleted file mode 100644 index 67361386744..00000000000 --- a/pkg/services/authz/zanzana/store.go +++ /dev/null @@ -1,18 +0,0 @@ -package zanzana - -import ( - "github.com/openfga/openfga/pkg/storage" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/grafana/pkg/services/authz/zanzana/store" -) - -func NewStore(cfg *setting.Cfg, logger log.Logger) (storage.OpenFGADatastore, error) { - return store.NewStore(cfg, logger) -} -func NewEmbeddedStore(cfg *setting.Cfg, db db.DB, logger log.Logger) (storage.OpenFGADatastore, error) { - return store.NewEmbeddedStore(cfg, db, logger) -} diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index a63f581b9ca..6cadfc0c199 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -1,12 +1,6 @@ package zanzana import ( - "fmt" - "strings" - - openfgav1 "github.com/openfga/api/proto/openfga/v1" - - authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) @@ -54,8 +48,8 @@ var ( ) const ( - KindDashboards string = "dashboards" - KindFolders string = "folders" + KindDashboards = common.KindDashboards + KindFolders = common.KindFolders ) var ( @@ -68,128 +62,15 @@ var ( ToOpenFGATuples = common.ToOpenFGATuples ToOpenFGATupleKey = common.ToOpenFGATupleKey ToOpenFGATupleKeyWithoutCondition = common.ToOpenFGATupleKeyWithoutCondition + + NewTupleEntry = common.NewTupleEntry + NewObjectEntry = common.NewObjectEntry + TranslateToResourceTuple = common.TranslateToResourceTuple + IsFolderResourceTuple = common.IsFolderResourceTuple + MergeFolderResourceTuples = common.MergeFolderResourceTuples + + TranslateToCheckRequest = common.TranslateToCheckRequest + TranslateToListRequest = common.TranslateToListRequest + TranslateToGroupResource = common.TranslateToGroupResource + TranslateBasicRole = common.TranslateBasicRole ) - -// NewTupleEntry constructs new openfga entry type:name[#relation]. -// Relation allows to specify group of users (subjects) related to type:name -// (for example, team:devs#member refers to users which are members of team devs) -func NewTupleEntry(objectType, name, relation string) string { - obj := fmt.Sprintf("%s:%s", objectType, name) - if relation != "" { - obj = fmt.Sprintf("%s#%s", obj, relation) - } - return obj -} - -func NewObjectEntry(objectType, group, resource, subresource, name string) string { - if objectType == TypeFolder { - return TypeFolder + ":" + name - } - - obj := fmt.Sprintf("%s:%s/%s", objectType, group, resource) - if subresource != "" { - obj = fmt.Sprintf("%s/%s", obj, subresource) - } - if name != "" { - obj = fmt.Sprintf("%s/%s", obj, name) - } - return obj -} - -func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) { - translation, ok := resourceTranslations[kind] - - if !ok { - return nil, false - } - - m, ok := translation.mapping[action] - if !ok { - return nil, false - } - - if name == "*" { - return common.NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource), true - } - - if translation.typ == TypeResource { - return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource, name), true - } - - if translation.typ == TypeFolder { - if m.group != "" && m.resource != "" { - return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, m.subresource, name), true - } - - return common.NewFolderTuple(subject, m.relation, name), true - } - - return common.NewTypedTuple(translation.typ, subject, m.relation, name), true -} - -func IsFolderResourceTuple(t *openfgav1.TupleKey) bool { - return strings.HasPrefix(t.Object, TypeFolder) && strings.HasPrefix(t.Relation, "resource_") -} - -func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) { - va := a.Condition.Context.Fields["subresources"] - vb := b.Condition.Context.Fields["subresources"] - va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...) -} - -func TranslateToCheckRequest(namespace, action, kind, name string) (*authlib.CheckRequest, bool) { - translation, ok := resourceTranslations[kind] - - if !ok { - return nil, false - } - - m, ok := translation.mapping[action] - if !ok { - return nil, false - } - - verb, ok := common.RelationToVerbMapping[m.relation] - if !ok { - return nil, false - } - - req := &authlib.CheckRequest{ - Namespace: namespace, - Verb: verb, - Group: translation.group, - Resource: translation.resource, - Name: name, - } - - return req, true -} - -func TranslateToListRequest(namespace, action, kind string) (*authlib.ListRequest, bool) { - translation, ok := resourceTranslations[kind] - - if !ok { - return nil, false - } - - // FIXME: support different verbs - req := &authlib.ListRequest{ - Namespace: namespace, - Group: translation.group, - Resource: translation.resource, - } - - return req, true -} - -func TranslateToGroupResource(kind string) string { - translation, ok := resourceTranslations[kind] - if !ok { - return "" - } - return common.FormatGroupResource(translation.group, translation.resource, "") -} - -func TranslateBasicRole(name string) string { - return basicRolesTranslations[name] -}