From 1498970e7401faa58702a129e7d9c06bab363a8e Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Mon, 5 Jan 2026 15:09:45 +0100 Subject: [PATCH] Implement BatchCheck functionality in LegacyAccessClient and update related proto definitions - Added BatchCheck method to LegacyAccessClient for handling batch authorization checks. - Updated proto definitions to remove BatchCheckRequest and BatchCheckResponse messages, replacing them with a new structure. - Adjusted related client and server implementations to align with the new BatchCheck structure. - Modified tests to validate the new BatchCheck functionality and ensure proper integration with existing authorization logic. --- pkg/services/accesscontrol/authorizer.go | 96 ++ pkg/services/authz/proto/v1/extention.pb.go | 883 ++++-------------- pkg/services/authz/proto/v1/extention.proto | 25 - .../authz/proto/v1/extention_grpc.pb.go | 46 +- pkg/services/authz/zanzana/client.go | 1 - pkg/services/authz/zanzana/client/client.go | 6 +- pkg/services/authz/zanzana/client/noop.go | 7 +- .../authz/zanzana/client/shadow_client.go | 51 + pkg/services/authz/zanzana/common/info.go | 3 +- .../zanzana/server/server_batch_check.go | 458 ++++++++- .../zanzana/server/server_batch_check_test.go | 225 +++-- .../authz/zanzana/server/server_bench_test.go | 166 ++-- 12 files changed, 970 insertions(+), 997 deletions(-) diff --git a/pkg/services/accesscontrol/authorizer.go b/pkg/services/accesscontrol/authorizer.go index ac180fb2ac7..7e31569a1b4 100644 --- a/pkg/services/accesscontrol/authorizer.go +++ b/pkg/services/accesscontrol/authorizer.go @@ -167,3 +167,99 @@ func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, re return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name)) }, claims.NoopZookie{}, nil } + +func (c *LegacyAccessClient) BatchCheck(ctx context.Context, id claims.AuthInfo, req claims.BatchCheckRequest) (claims.BatchCheckResponse, error) { + ident, ok := id.(identity.Requester) + if !ok { + return claims.BatchCheckResponse{}, errors.New("expected identity.Requester for legacy access control") + } + + results := make(map[string]claims.BatchCheckResult, len(req.Checks)) + + // Cache checkers by action to avoid recreating them for each check + checkerCache := make(map[string]func(scopes ...string) bool) + + for _, check := range req.Checks { + opts, ok := c.opts[check.Resource] + if !ok { + // For now w fallback to grafana admin if no options are found for resource. + if ident.GetIsGrafanaAdmin() { + results[check.CorrelationID] = claims.BatchCheckResult{Allowed: true} + } else { + results[check.CorrelationID] = claims.BatchCheckResult{Allowed: false} + } + continue + } + + // Check if verb should be skipped + if opts.Unchecked[check.Verb] { + results[check.CorrelationID] = claims.BatchCheckResult{Allowed: true} + continue + } + + action, ok := opts.Mapping[check.Verb] + if !ok { + results[check.CorrelationID] = claims.BatchCheckResult{ + Allowed: false, + Error: fmt.Errorf("missing action for %s %s", check.Verb, check.Resource), + } + continue + } + + // Get or create cached checker for this action + checker, ok := checkerCache[action] + if !ok { + checker = Checker(ident, action) + checkerCache[action] = checker + } + + // Handle list and create verbs (no specific name) + // TODO: Should we allow list/create without name in a BatchCheck request? + if check.Name == "" { + if check.Verb == utils.VerbList || check.Verb == utils.VerbCreate { + // For list/create without name, check if user has the action at all + // TODO: Is this correct for Create? + results[check.CorrelationID] = claims.BatchCheckResult{ + Allowed: len(ident.GetPermissions()[action]) > 0, + } + } else { + results[check.CorrelationID] = claims.BatchCheckResult{ + Allowed: false, + Error: fmt.Errorf("unhandled authorization: %s %s", check.Group, check.Verb), + } + } + continue + } + + // Check with resolver or direct scope + var allowed bool + if opts.Resolver != nil { + ns, err := claims.ParseNamespace(check.Namespace) + if err != nil { + results[check.CorrelationID] = claims.BatchCheckResult{ + Allowed: false, + Error: err, + } + continue + } + scopes, err := opts.Resolver.Resolve(ctx, ns, check.Name) + if err != nil { + results[check.CorrelationID] = claims.BatchCheckResult{ + Allowed: false, + Error: err, + } + continue + } + allowed = checker(scopes...) + } else { + allowed = checker(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, check.Name)) + } + + results[check.CorrelationID] = claims.BatchCheckResult{Allowed: allowed} + } + + return claims.BatchCheckResponse{ + Results: results, + Zookie: claims.NoopZookie{}, + }, nil +} diff --git a/pkg/services/authz/proto/v1/extention.pb.go b/pkg/services/authz/proto/v1/extention.pb.go index deff13d5edd..245885ca50e 100644 --- a/pkg/services/authz/proto/v1/extention.pb.go +++ b/pkg/services/authz/proto/v1/extention.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.5 +// protoc-gen-go v1.36.8 // protoc (unknown) // source: extention.proto @@ -1933,238 +1933,6 @@ func (*WriteResponse) Descriptor() ([]byte, []int) { return file_extention_proto_rawDescGZIP(), []int{29} } -type BatchCheckRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"` - Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` - Items []*BatchCheckItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BatchCheckRequest) Reset() { - *x = BatchCheckRequest{} - mi := &file_extention_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BatchCheckRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BatchCheckRequest) ProtoMessage() {} - -func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[30] - 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 BatchCheckRequest.ProtoReflect.Descriptor instead. -func (*BatchCheckRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{30} -} - -func (x *BatchCheckRequest) GetSubject() string { - if x != nil { - return x.Subject - } - return "" -} - -func (x *BatchCheckRequest) GetNamespace() string { - if x != nil { - return x.Namespace - } - return "" -} - -func (x *BatchCheckRequest) GetItems() []*BatchCheckItem { - if x != nil { - return x.Items - } - return nil -} - -type BatchCheckItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - Verb string `protobuf:"bytes,1,opt,name=verb,proto3" json:"verb,omitempty"` - Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"` - Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"` - Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` - Subresource string `protobuf:"bytes,5,opt,name=subresource,proto3" json:"subresource,omitempty"` - Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BatchCheckItem) Reset() { - *x = BatchCheckItem{} - mi := &file_extention_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BatchCheckItem) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BatchCheckItem) ProtoMessage() {} - -func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[31] - 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 BatchCheckItem.ProtoReflect.Descriptor instead. -func (*BatchCheckItem) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{31} -} - -func (x *BatchCheckItem) GetVerb() string { - if x != nil { - return x.Verb - } - return "" -} - -func (x *BatchCheckItem) GetGroup() string { - if x != nil { - return x.Group - } - return "" -} - -func (x *BatchCheckItem) GetResource() string { - if x != nil { - return x.Resource - } - return "" -} - -func (x *BatchCheckItem) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *BatchCheckItem) GetSubresource() string { - if x != nil { - return x.Subresource - } - return "" -} - -func (x *BatchCheckItem) GetFolder() string { - if x != nil { - return x.Folder - } - return "" -} - -type BatchCheckResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Groups map[string]*BatchCheckGroupResource `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BatchCheckResponse) Reset() { - *x = BatchCheckResponse{} - mi := &file_extention_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BatchCheckResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BatchCheckResponse) ProtoMessage() {} - -func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[32] - 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 BatchCheckResponse.ProtoReflect.Descriptor instead. -func (*BatchCheckResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{32} -} - -func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource { - if x != nil { - return x.Groups - } - return nil -} - -type BatchCheckGroupResource struct { - state protoimpl.MessageState `protogen:"open.v1"` - Items map[string]bool `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BatchCheckGroupResource) Reset() { - *x = BatchCheckGroupResource{} - mi := &file_extention_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BatchCheckGroupResource) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BatchCheckGroupResource) ProtoMessage() {} - -func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[33] - 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 BatchCheckGroupResource.ProtoReflect.Descriptor instead. -func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{33} -} - -func (x *BatchCheckGroupResource) GetItems() map[string]bool { - if x != nil { - return x.Items - } - return nil -} - type QueryRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` @@ -2175,7 +1943,7 @@ type QueryRequest struct { func (x *QueryRequest) Reset() { *x = QueryRequest{} - mi := &file_extention_proto_msgTypes[34] + mi := &file_extention_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2187,7 +1955,7 @@ func (x *QueryRequest) String() string { func (*QueryRequest) ProtoMessage() {} func (x *QueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[34] + mi := &file_extention_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2200,7 +1968,7 @@ func (x *QueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. func (*QueryRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{34} + return file_extention_proto_rawDescGZIP(), []int{30} } func (x *QueryRequest) GetNamespace() string { @@ -2229,7 +1997,7 @@ type QueryResponse struct { func (x *QueryResponse) Reset() { *x = QueryResponse{} - mi := &file_extention_proto_msgTypes[35] + mi := &file_extention_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2241,7 +2009,7 @@ func (x *QueryResponse) String() string { func (*QueryResponse) ProtoMessage() {} func (x *QueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[35] + mi := &file_extention_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2254,7 +2022,7 @@ func (x *QueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead. func (*QueryResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{35} + return file_extention_proto_rawDescGZIP(), []int{31} } func (x *QueryResponse) GetResult() isQueryResponse_Result { @@ -2295,7 +2063,7 @@ type QueryOperation struct { func (x *QueryOperation) Reset() { *x = QueryOperation{} - mi := &file_extention_proto_msgTypes[36] + mi := &file_extention_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2307,7 +2075,7 @@ func (x *QueryOperation) String() string { func (*QueryOperation) ProtoMessage() {} func (x *QueryOperation) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[36] + mi := &file_extention_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2320,7 +2088,7 @@ func (x *QueryOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryOperation.ProtoReflect.Descriptor instead. func (*QueryOperation) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{36} + return file_extention_proto_rawDescGZIP(), []int{32} } func (x *QueryOperation) GetOperation() isQueryOperation_Operation { @@ -2359,7 +2127,7 @@ type GetFolderParentsQuery struct { func (x *GetFolderParentsQuery) Reset() { *x = GetFolderParentsQuery{} - mi := &file_extention_proto_msgTypes[37] + mi := &file_extention_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2371,7 +2139,7 @@ func (x *GetFolderParentsQuery) String() string { func (*GetFolderParentsQuery) ProtoMessage() {} func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[37] + mi := &file_extention_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2384,7 +2152,7 @@ func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsQuery.ProtoReflect.Descriptor instead. func (*GetFolderParentsQuery) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{37} + return file_extention_proto_rawDescGZIP(), []int{33} } func (x *GetFolderParentsQuery) GetFolder() string { @@ -2404,7 +2172,7 @@ type GetFolderParentsResult struct { func (x *GetFolderParentsResult) Reset() { *x = GetFolderParentsResult{} - mi := &file_extention_proto_msgTypes[38] + mi := &file_extention_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2416,7 +2184,7 @@ func (x *GetFolderParentsResult) String() string { func (*GetFolderParentsResult) ProtoMessage() {} func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[38] + mi := &file_extention_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2429,7 +2197,7 @@ func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsResult.ProtoReflect.Descriptor instead. func (*GetFolderParentsResult) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{38} + return file_extention_proto_rawDescGZIP(), []int{34} } func (x *GetFolderParentsResult) GetParentUids() []string { @@ -2441,398 +2209,159 @@ func (x *GetFolderParentsResult) GetParentUids() []string { var File_extention_proto protoreflect.FileDescriptor -var file_extention_proto_rawDesc = string([]byte{ - 0x0a, 0x0f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x12, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 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, 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, 0xc8, 0x09, 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, 0x12, 0x61, 0x0a, 0x14, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6f, 0x72, 0x67, 0x5f, 0x72, 0x6f, 0x6c, 0x65, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x61, 0x0a, 0x14, 0x64, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6f, 0x72, 0x67, 0x5f, 0x72, - 0x6f, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 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, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x58, - 0x0a, 0x11, 0x61, 0x64, 0x64, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6f, 0x72, 0x67, 0x5f, 0x72, - 0x6f, 0x6c, 0x65, 0x18, 0x07, 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, 0x41, - 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x55, 0x73, 0x65, - 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 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, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, - 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x60, 0x0a, 0x13, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, - 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 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, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x60, 0x0a, 0x13, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x62, 0x69, 0x6e, 0x64, - 0x69, 0x6e, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 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, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x60, - 0x0a, 0x13, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x62, 0x69, - 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 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, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, - 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, - 0x0c, 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, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, - 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x4a, 0x0a, 0x0b, - 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x0d, 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, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, - 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0a, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 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, 0x41, 0x0a, 0x17, - 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, - 0x65, 0x72, 0x61, 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, 0x12, 0x0a, 0x04, 0x72, - 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, - 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, - 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 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, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 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, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, - 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, - 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, - 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, - 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, - 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x7c, 0x0a, 0x1a, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, - 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, - 0x61, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, - 0x65, 0x61, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x7c, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x54, 0x65, 0x61, 0x6d, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, 0x61, 0x6d, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x61, - 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, - 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, - 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, - 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 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, 0x52, 0x6f, 0x6c, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x95, 0x01, - 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, - 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, - 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x44, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 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, 0x52, 0x6f, 0x6c, 0x65, 0x50, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3e, 0x0a, 0x0e, 0x52, 0x6f, 0x6c, 0x65, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x73, 0x63, 0x6f, 0x70, 0x65, 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, 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, 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, 0x22, 0x6e, 0x0a, 0x0c, - 0x51, 0x75, 0x65, 0x72, 0x79, 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, 0x40, 0x0a, 0x09, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, - 0x0e, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, - 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x78, 0x0a, 0x0e, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, - 0x0a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, - 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 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, - 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x48, 0x00, 0x52, 0x10, 0x67, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, - 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, - 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x39, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x46, 0x6f, - 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x75, 0x69, 0x64, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x55, 0x69, - 0x64, 0x73, 0x32, 0xac, 0x03, 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, 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, 0x12, 0x4c, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x20, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 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, 0x51, 0x75, 0x65, 0x72, 0x79, 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, -}) +const file_extention_proto_rawDesc = "" + + "\n" + + "\x0fextention.proto\x12\x12authz.extention.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"r\n" + + "\rMutateRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12C\n" + + "\n" + + "operations\x18\x02 \x03(\v2#.authz.extention.v1.MutateOperationR\n" + + "operations\"\x10\n" + + "\x0eMutateResponse\"\xc8\t\n" + + "\x0fMutateOperation\x12Z\n" + + "\x11set_folder_parent\x18\x01 \x01(\v2,.authz.extention.v1.SetFolderParentOperationH\x00R\x0fsetFolderParent\x12P\n" + + "\rdelete_folder\x18\x02 \x01(\v2).authz.extention.v1.DeleteFolderOperationH\x00R\fdeleteFolder\x12\\\n" + + "\x11create_permission\x18\x03 \x01(\v2-.authz.extention.v1.CreatePermissionOperationH\x00R\x10createPermission\x12\\\n" + + "\x11delete_permission\x18\x04 \x01(\v2-.authz.extention.v1.DeletePermissionOperationH\x00R\x10deletePermission\x12a\n" + + "\x14update_user_org_role\x18\x05 \x01(\v2..authz.extention.v1.UpdateUserOrgRoleOperationH\x00R\x11updateUserOrgRole\x12a\n" + + "\x14delete_user_org_role\x18\x06 \x01(\v2..authz.extention.v1.DeleteUserOrgRoleOperationH\x00R\x11deleteUserOrgRole\x12X\n" + + "\x11add_user_org_role\x18\a \x01(\v2+.authz.extention.v1.AddUserOrgRoleOperationH\x00R\x0eaddUserOrgRole\x12`\n" + + "\x13create_role_binding\x18\b \x01(\v2..authz.extention.v1.CreateRoleBindingOperationH\x00R\x11createRoleBinding\x12`\n" + + "\x13delete_role_binding\x18\t \x01(\v2..authz.extention.v1.DeleteRoleBindingOperationH\x00R\x11deleteRoleBinding\x12`\n" + + "\x13create_team_binding\x18\n" + + " \x01(\v2..authz.extention.v1.CreateTeamBindingOperationH\x00R\x11createTeamBinding\x12`\n" + + "\x13delete_team_binding\x18\v \x01(\v2..authz.extention.v1.DeleteTeamBindingOperationH\x00R\x11deleteTeamBinding\x12J\n" + + "\vcreate_role\x18\f \x01(\v2'.authz.extention.v1.CreateRoleOperationH\x00R\n" + + "createRole\x12J\n" + + "\vdelete_role\x18\r \x01(\v2'.authz.extention.v1.DeleteRoleOperationH\x00R\n" + + "deleteRoleB\v\n" + + "\toperation\"s\n" + + "\x18SetFolderParentOperation\x12\x16\n" + + "\x06folder\x18\x01 \x01(\tR\x06folder\x12\x16\n" + + "\x06parent\x18\x02 \x01(\tR\x06parent\x12'\n" + + "\x0fdelete_existing\x18\x03 \x01(\bR\x0edeleteExisting\"p\n" + + "\x15DeleteFolderOperation\x12\x16\n" + + "\x06folder\x18\x01 \x01(\tR\x06folder\x12\x16\n" + + "\x06parent\x18\x02 \x01(\tR\x06parent\x12'\n" + + "\x0fdelete_existing\x18\x03 \x01(\bR\x0edeleteExisting\"\x95\x01\n" + + "\x19CreatePermissionOperation\x128\n" + + "\bresource\x18\x01 \x01(\v2\x1c.authz.extention.v1.ResourceR\bresource\x12>\n" + + "\n" + + "permission\x18\x02 \x01(\v2\x1e.authz.extention.v1.PermissionR\n" + + "permission\"\x95\x01\n" + + "\x19DeletePermissionOperation\x128\n" + + "\bresource\x18\x01 \x01(\v2\x1c.authz.extention.v1.ResourceR\bresource\x12>\n" + + "\n" + + "permission\x18\x02 \x01(\v2\x1e.authz.extention.v1.PermissionR\n" + + "permission\"A\n" + + "\x17AddUserOrgRoleOperation\x12\x12\n" + + "\x04user\x18\x01 \x01(\tR\x04user\x12\x12\n" + + "\x04role\x18\x02 \x01(\tR\x04role\"D\n" + + "\x1aUpdateUserOrgRoleOperation\x12\x12\n" + + "\x04user\x18\x01 \x01(\tR\x04user\x12\x12\n" + + "\x04role\x18\x02 \x01(\tR\x04role\"D\n" + + "\x1aDeleteUserOrgRoleOperation\x12\x12\n" + + "\x04user\x18\x01 \x01(\tR\x04user\x12\x12\n" + + "\x04role\x18\x02 \x01(\tR\x04role\"\x9c\x01\n" + + "\x1aCreateRoleBindingOperation\x12!\n" + + "\fsubject_kind\x18\x01 \x01(\tR\vsubjectKind\x12!\n" + + "\fsubject_name\x18\x02 \x01(\tR\vsubjectName\x12\x1b\n" + + "\trole_kind\x18\x03 \x01(\tR\broleKind\x12\x1b\n" + + "\trole_name\x18\x04 \x01(\tR\broleName\"\x9c\x01\n" + + "\x1aDeleteRoleBindingOperation\x12!\n" + + "\fsubject_kind\x18\x01 \x01(\tR\vsubjectKind\x12!\n" + + "\fsubject_name\x18\x02 \x01(\tR\vsubjectName\x12\x1b\n" + + "\trole_kind\x18\x03 \x01(\tR\broleKind\x12\x1b\n" + + "\trole_name\x18\x04 \x01(\tR\broleName\"|\n" + + "\x1aCreateTeamBindingOperation\x12!\n" + + "\fsubject_name\x18\x01 \x01(\tR\vsubjectName\x12\x1b\n" + + "\tteam_name\x18\x02 \x01(\tR\bteamName\x12\x1e\n" + + "\n" + + "permission\x18\x03 \x01(\tR\n" + + "permission\"|\n" + + "\x1aDeleteTeamBindingOperation\x12!\n" + + "\fsubject_name\x18\x01 \x01(\tR\vsubjectName\x12\x1b\n" + + "\tteam_name\x18\x02 \x01(\tR\bteamName\x12\x1e\n" + + "\n" + + "permission\x18\x03 \x01(\tR\n" + + "permission\"\x95\x01\n" + + "\x13CreateRoleOperation\x12\x1b\n" + + "\trole_kind\x18\x01 \x01(\tR\broleKind\x12\x1b\n" + + "\trole_name\x18\x02 \x01(\tR\broleName\x12D\n" + + "\vpermissions\x18\x03 \x03(\v2\".authz.extention.v1.RolePermissionR\vpermissions\"\x95\x01\n" + + "\x13DeleteRoleOperation\x12\x1b\n" + + "\trole_kind\x18\x01 \x01(\tR\broleKind\x12\x1b\n" + + "\trole_name\x18\x02 \x01(\tR\broleName\x12D\n" + + "\vpermissions\x18\x03 \x03(\v2\".authz.extention.v1.RolePermissionR\vpermissions\">\n" + + "\x0eRolePermission\x12\x16\n" + + "\x06action\x18\x01 \x01(\tR\x06action\x12\x14\n" + + "\x05scope\x18\x02 \x01(\tR\x05scope\"P\n" + + "\bResource\x12\x14\n" + + "\x05group\x18\x01 \x01(\tR\x05group\x12\x1a\n" + + "\bresource\x18\x02 \x01(\tR\bresource\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"H\n" + + "\n" + + "Permission\x12\x12\n" + + "\x04kind\x18\x01 \x01(\tR\x04kind\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04verb\x18\x03 \x01(\tR\x04verb\"\x9b\x01\n" + + "\bTupleKey\x12\x12\n" + + "\x04user\x18\x01 \x01(\tR\x04user\x12\x1a\n" + + "\brelation\x18\x02 \x01(\tR\brelation\x12\x16\n" + + "\x06object\x18\x03 \x01(\tR\x06object\x12G\n" + + "\tcondition\x18\x04 \x01(\v2).authz.extention.v1.RelationshipConditionR\tcondition\"q\n" + + "\x05Tuple\x12.\n" + + "\x03key\x18\x01 \x01(\v2\x1c.authz.extention.v1.TupleKeyR\x03key\x128\n" + + "\ttimestamp\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\"b\n" + + "\x18TupleKeyWithoutCondition\x12\x12\n" + + "\x04user\x18\x01 \x01(\tR\x04user\x12\x1a\n" + + "\brelation\x18\x02 \x01(\tR\brelation\x12\x16\n" + + "\x06object\x18\x03 \x01(\tR\x06object\"^\n" + + "\x15RelationshipCondition\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x121\n" + + "\acontext\x18\x02 \x01(\v2\x17.google.protobuf.StructR\acontext\"\xda\x01\n" + + "\vReadRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12D\n" + + "\ttuple_key\x18\x02 \x01(\v2'.authz.extention.v1.ReadRequestTupleKeyR\btupleKey\x128\n" + + "\tpage_size\x18\x03 \x01(\v2\x1b.google.protobuf.Int32ValueR\bpageSize\x12-\n" + + "\x12continuation_token\x18\x04 \x01(\tR\x11continuationToken\"]\n" + + "\x13ReadRequestTupleKey\x12\x12\n" + + "\x04user\x18\x01 \x01(\tR\x04user\x12\x1a\n" + + "\brelation\x18\x02 \x01(\tR\brelation\x12\x16\n" + + "\x06object\x18\x03 \x01(\tR\x06object\"p\n" + + "\fReadResponse\x121\n" + + "\x06tuples\x18\x01 \x03(\v2\x19.authz.extention.v1.TupleR\x06tuples\x12-\n" + + "\x12continuation_token\x18\x02 \x01(\tR\x11continuationToken\"Q\n" + + "\x12WriteRequestWrites\x12;\n" + + "\n" + + "tuple_keys\x18\x01 \x03(\v2\x1c.authz.extention.v1.TupleKeyR\ttupleKeys\"b\n" + + "\x13WriteRequestDeletes\x12K\n" + + "\n" + + "tuple_keys\x18\x01 \x03(\v2,.authz.extention.v1.TupleKeyWithoutConditionR\ttupleKeys\"\xaf\x01\n" + + "\fWriteRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12>\n" + + "\x06writes\x18\x02 \x01(\v2&.authz.extention.v1.WriteRequestWritesR\x06writes\x12A\n" + + "\adeletes\x18\x03 \x01(\v2'.authz.extention.v1.WriteRequestDeletesR\adeletes\"\x0f\n" + + "\rWriteResponse\"n\n" + + "\fQueryRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12@\n" + + "\toperation\x18\x02 \x01(\v2\".authz.extention.v1.QueryOperationR\toperation\"n\n" + + "\rQueryResponse\x12S\n" + + "\x0efolder_parents\x18\x01 \x01(\v2*.authz.extention.v1.GetFolderParentsResultH\x00R\rfolderParentsB\b\n" + + "\x06result\"x\n" + + "\x0eQueryOperation\x12Y\n" + + "\x12get_folder_parents\x18\x01 \x01(\v2).authz.extention.v1.GetFolderParentsQueryH\x00R\x10getFolderParentsB\v\n" + + "\toperation\"/\n" + + "\x15GetFolderParentsQuery\x12\x16\n" + + "\x06folder\x18\x01 \x01(\tR\x06folder\"9\n" + + "\x16GetFolderParentsResult\x12\x1f\n" + + "\vparent_uids\x18\x01 \x03(\tR\n" + + "parentUids2\xcf\x02\n" + + "\x15AuthzExtentionService\x12I\n" + + "\x04Read\x12\x1f.authz.extention.v1.ReadRequest\x1a .authz.extention.v1.ReadResponse\x12L\n" + + "\x05Write\x12 .authz.extention.v1.WriteRequest\x1a!.authz.extention.v1.WriteResponse\x12O\n" + + "\x06Mutate\x12!.authz.extention.v1.MutateRequest\x1a\".authz.extention.v1.MutateResponse\x12L\n" + + "\x05Query\x12 .authz.extention.v1.QueryRequest\x1a!.authz.extention.v1.QueryResponseB8Z6github.com/grafana/grafana/pkg/services/authz/proto/v1b\x06proto3" var ( file_extention_proto_rawDescOnce sync.Once @@ -2846,7 +2375,7 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 35) var file_extention_proto_goTypes = []any{ (*MutateRequest)(nil), // 0: authz.extention.v1.MutateRequest (*MutateResponse)(nil), // 1: authz.extention.v1.MutateResponse @@ -2878,20 +2407,14 @@ var file_extention_proto_goTypes = []any{ (*WriteRequestDeletes)(nil), // 27: authz.extention.v1.WriteRequestDeletes (*WriteRequest)(nil), // 28: authz.extention.v1.WriteRequest (*WriteResponse)(nil), // 29: authz.extention.v1.WriteResponse - (*BatchCheckRequest)(nil), // 30: authz.extention.v1.BatchCheckRequest - (*BatchCheckItem)(nil), // 31: authz.extention.v1.BatchCheckItem - (*BatchCheckResponse)(nil), // 32: authz.extention.v1.BatchCheckResponse - (*BatchCheckGroupResource)(nil), // 33: authz.extention.v1.BatchCheckGroupResource - (*QueryRequest)(nil), // 34: authz.extention.v1.QueryRequest - (*QueryResponse)(nil), // 35: authz.extention.v1.QueryResponse - (*QueryOperation)(nil), // 36: authz.extention.v1.QueryOperation - (*GetFolderParentsQuery)(nil), // 37: authz.extention.v1.GetFolderParentsQuery - (*GetFolderParentsResult)(nil), // 38: authz.extention.v1.GetFolderParentsResult - nil, // 39: authz.extention.v1.BatchCheckResponse.GroupsEntry - nil, // 40: authz.extention.v1.BatchCheckGroupResource.ItemsEntry - (*timestamppb.Timestamp)(nil), // 41: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 42: google.protobuf.Struct - (*wrapperspb.Int32Value)(nil), // 43: google.protobuf.Int32Value + (*QueryRequest)(nil), // 30: authz.extention.v1.QueryRequest + (*QueryResponse)(nil), // 31: authz.extention.v1.QueryResponse + (*QueryOperation)(nil), // 32: authz.extention.v1.QueryOperation + (*GetFolderParentsQuery)(nil), // 33: authz.extention.v1.GetFolderParentsQuery + (*GetFolderParentsResult)(nil), // 34: authz.extention.v1.GetFolderParentsResult + (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 36: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 37: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ 2, // 0: authz.extention.v1.MutateRequest.operations:type_name -> authz.extention.v1.MutateOperation @@ -2916,37 +2439,31 @@ var file_extention_proto_depIdxs = []int32{ 16, // 19: authz.extention.v1.DeleteRoleOperation.permissions:type_name -> authz.extention.v1.RolePermission 22, // 20: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition 19, // 21: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey - 41, // 22: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp - 42, // 23: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 35, // 22: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 36, // 23: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct 24, // 24: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey - 43, // 25: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 37, // 25: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value 20, // 26: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple 19, // 27: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey 21, // 28: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition 26, // 29: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites 27, // 30: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes - 31, // 31: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem - 39, // 32: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry - 40, // 33: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry - 36, // 34: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation - 38, // 35: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult - 37, // 36: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery - 33, // 37: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource - 30, // 38: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest - 23, // 39: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest - 28, // 40: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest - 0, // 41: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest - 34, // 42: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest - 32, // 43: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse - 25, // 44: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse - 29, // 45: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse - 1, // 46: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse - 35, // 47: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse - 43, // [43:48] is the sub-list for method output_type - 38, // [38:43] is the sub-list for method input_type - 38, // [38:38] is the sub-list for extension type_name - 38, // [38:38] is the sub-list for extension extendee - 0, // [0:38] is the sub-list for field type_name + 32, // 31: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation + 34, // 32: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult + 33, // 33: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery + 23, // 34: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 28, // 35: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 0, // 36: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest + 30, // 37: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest + 25, // 38: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 29, // 39: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 1, // 40: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse + 31, // 41: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse + 38, // [38:42] is the sub-list for method output_type + 34, // [34:38] is the sub-list for method input_type + 34, // [34:34] is the sub-list for extension type_name + 34, // [34:34] is the sub-list for extension extendee + 0, // [0:34] is the sub-list for field type_name } func init() { file_extention_proto_init() } @@ -2969,10 +2486,10 @@ func file_extention_proto_init() { (*MutateOperation_CreateRole)(nil), (*MutateOperation_DeleteRole)(nil), } - file_extention_proto_msgTypes[35].OneofWrappers = []any{ + file_extention_proto_msgTypes[31].OneofWrappers = []any{ (*QueryResponse_FolderParents)(nil), } - file_extention_proto_msgTypes[36].OneofWrappers = []any{ + file_extention_proto_msgTypes[32].OneofWrappers = []any{ (*QueryOperation_GetFolderParents)(nil), } type x struct{} @@ -2981,7 +2498,7 @@ func file_extention_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_extention_proto_rawDesc), len(file_extention_proto_rawDesc)), NumEnums: 0, - NumMessages: 41, + NumMessages: 35, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index cb59e349f64..0d26d1549d1 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -9,8 +9,6 @@ import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; service AuthzExtentionService { - rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse); - rpc Read(ReadRequest) returns (ReadResponse); rpc Write(WriteRequest) returns (WriteResponse); @@ -231,29 +229,6 @@ message WriteRequest { message WriteResponse {} -message BatchCheckRequest { - string subject = 1; - string namespace = 2; - repeated BatchCheckItem items = 3; -} - -message BatchCheckItem { - string verb = 1; - string group = 2; - string resource = 3; - string name = 4; - string subresource = 5; - string folder = 6; -} - -message BatchCheckResponse { - map groups = 1; -} - -message BatchCheckGroupResource { - map items = 1; -} - message QueryRequest { string namespace = 1; QueryOperation operation = 2; diff --git a/pkg/services/authz/proto/v1/extention_grpc.pb.go b/pkg/services/authz/proto/v1/extention_grpc.pb.go index b320dbe8c07..2b482d7fbd7 100644 --- a/pkg/services/authz/proto/v1/extention_grpc.pb.go +++ b/pkg/services/authz/proto/v1/extention_grpc.pb.go @@ -19,18 +19,16 @@ import ( const _ = grpc.SupportPackageIsVersion8 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" - AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query" + AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read" + AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write" + AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate" + AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query" ) // AuthzExtentionServiceClient is the client API for AuthzExtentionService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type AuthzExtentionServiceClient interface { - 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) @@ -45,16 +43,6 @@ func NewAuthzExtentionServiceClient(cc grpc.ClientConnInterface) AuthzExtentionS return &authzExtentionServiceClient{cc} } -func (c *authzExtentionServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(BatchCheckResponse) - err := c.cc.Invoke(ctx, AuthzExtentionService_BatchCheck_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *authzExtentionServiceClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ReadResponse) @@ -99,7 +87,6 @@ func (c *authzExtentionServiceClient) Query(ctx context.Context, in *QueryReques // All implementations should embed UnimplementedAuthzExtentionServiceServer // for forward compatibility 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) @@ -110,9 +97,6 @@ type AuthzExtentionServiceServer interface { type UnimplementedAuthzExtentionServiceServer struct { } -func (UnimplementedAuthzExtentionServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method BatchCheck not implemented") -} func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Read not implemented") } @@ -137,24 +121,6 @@ func RegisterAuthzExtentionServiceServer(s grpc.ServiceRegistrar, srv AuthzExten s.RegisterService(&AuthzExtentionService_ServiceDesc, srv) } -func _AuthzExtentionService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(BatchCheckRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: AuthzExtentionService_BatchCheck_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _AuthzExtentionService_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ReadRequest) if err := dec(in); err != nil { @@ -234,10 +200,6 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "authz.extention.v1.AuthzExtentionService", HandlerType: (*AuthzExtentionServiceServer)(nil), Methods: []grpc.MethodDesc{ - { - MethodName: "BatchCheck", - Handler: _AuthzExtentionService_BatchCheck_Handler, - }, { MethodName: "Read", Handler: _AuthzExtentionService_Read_Handler, diff --git a/pkg/services/authz/zanzana/client.go b/pkg/services/authz/zanzana/client.go index f238fd27159..7b93ccc089e 100644 --- a/pkg/services/authz/zanzana/client.go +++ b/pkg/services/authz/zanzana/client.go @@ -13,7 +13,6 @@ type Client interface { authlib.AccessClient 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) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) diff --git a/pkg/services/authz/zanzana/client/client.go b/pkg/services/authz/zanzana/client/client.go index 3c51d707561..79fca14427b 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -68,11 +68,11 @@ func (c *Client) Write(ctx context.Context, req *authzextv1.WriteRequest) error return err } -func (c *Client) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { - ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Check") +func (c *Client) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) { + ctx, span := tracer.Start(ctx, "authlib.zanzana.client.BatchCheck") defer span.End() - return c.authzext.BatchCheck(ctx, req) + return c.authzlibclient.BatchCheck(ctx, id, req) } func (c *Client) WriteNew(ctx context.Context, req *authzextv1.WriteRequest) error { diff --git a/pkg/services/authz/zanzana/client/noop.go b/pkg/services/authz/zanzana/client/noop.go index 73860cd0c4e..388dc7b1e04 100644 --- a/pkg/services/authz/zanzana/client/noop.go +++ b/pkg/services/authz/zanzana/client/noop.go @@ -34,8 +34,11 @@ func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) er return nil } -func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { - return nil, nil +func (nc NoopClient) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) { + return authlib.BatchCheckResponse{ + Results: make(map[string]authlib.BatchCheckResult), + Zookie: authlib.NoopZookie{}, + }, nil } func (nc NoopClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { diff --git a/pkg/services/authz/zanzana/client/shadow_client.go b/pkg/services/authz/zanzana/client/shadow_client.go index 6f6e6dc6836..7f4cfbcc6e4 100644 --- a/pkg/services/authz/zanzana/client/shadow_client.go +++ b/pkg/services/authz/zanzana/client/shadow_client.go @@ -132,3 +132,54 @@ func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req aut return shadowItemChecker, authlib.NoopZookie{}, err } + +func (c *ShadowClient) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) { + acResChan := make(chan authlib.BatchCheckResponse, 1) + acErrChan := make(chan error, 1) + + go func() { + if c.zanzanaClient == nil { + return + } + + zanzanaCtx := context.WithoutCancel(ctx) + zanzanaCtxTimeout, cancel := context.WithTimeout(zanzanaCtx, zanzanaTimeout) + defer cancel() + + timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("zanzana")) + res, err := c.zanzanaClient.BatchCheck(zanzanaCtxTimeout, id, req) + if err != nil { + c.logger.Error("Failed to run zanzana batch check", "error", err) + } + timer.ObserveDuration() + + acRes := <-acResChan + acErr := <-acErrChan + + if acErr == nil { + // Compare results for each correlation ID + for corrID, acResult := range acRes.Results { + zanzanaResult, exists := res.Results[corrID] + if !exists { + c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc() + c.logger.Warn("Zanzana batch check missing result", "correlationId", corrID, "user", id.GetUID()) + continue + } + if zanzanaResult.Allowed != acResult.Allowed { + c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc() + c.logger.Warn("Zanzana batch check result does not match", "expected", acResult.Allowed, "actual", zanzanaResult.Allowed, "correlationId", corrID, "user", id.GetUID()) + } else { + c.metrics.evaluationStatusTotal.WithLabelValues("success").Inc() + } + } + } + }() + + timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("rbac")) + res, err := c.accessClient.BatchCheck(ctx, id, req) + timer.ObserveDuration() + acResChan <- res + acErrChan <- err + + return res, err +} diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go index 4e4bbedc0b7..de909c22461 100644 --- a/pkg/services/authz/zanzana/common/info.go +++ b/pkg/services/authz/zanzana/common/info.go @@ -10,7 +10,6 @@ import ( iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/services/accesscontrol" - authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) type typeInfo struct { @@ -73,7 +72,7 @@ func NewResourceInfoFromCheck(r *authzv1.CheckRequest) ResourceInfo { return resource } -func NewResourceInfoFromBatchItem(i *authzextv1.BatchCheckItem) ResourceInfo { +func NewResourceInfoFromBatchItem(i *authzv1.BatchCheckItem) ResourceInfo { typ, relations := getTypeAndRelations(i.GetGroup(), i.GetResource()) return newResource( typ, diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go index 62994c58973..97fc1ed4e76 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check.go +++ b/pkg/services/authz/zanzana/server/server_batch_check.go @@ -2,97 +2,457 @@ package server import ( "context" + "fmt" + "time" authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "google.golang.org/protobuf/types/known/structpb" - authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) -func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { +// checkKey represents a unique check to be performed +type checkKey struct { + relation string + object string +} + +// batchCheckBuilder encapsulates state for building OpenFGA batch checks +type batchCheckBuilder struct { + subject string + contextuals *openfgav1.ContextualTupleKeys + checks []*openfgav1.BatchCheckItem + checksSeen map[checkKey]bool + checkMapping map[string]checkKey + counter int +} + +func newBatchCheckBuilder(subject string, contextuals *openfgav1.ContextualTupleKeys) *batchCheckBuilder { + return &batchCheckBuilder{ + subject: subject, + contextuals: contextuals, + checks: make([]*openfgav1.BatchCheckItem, 0), + checksSeen: make(map[checkKey]bool), + checkMapping: make(map[string]checkKey), + counter: 0, + } +} + +func (b *batchCheckBuilder) addCheck(relation, object string, context *structpb.Struct) { + if object == "" { + return + } + + key := checkKey{relation: relation, object: object} + if b.checksSeen[key] { + return + } + b.checksSeen[key] = true + + correlationID := fmt.Sprintf("c%d", b.counter) + b.counter++ + + b.checks = append(b.checks, &openfgav1.BatchCheckItem{ + TupleKey: &openfgav1.CheckRequestTupleKey{ + User: b.subject, + Relation: relation, + Object: object, + }, + ContextualTuples: b.contextuals, + Context: context, + CorrelationId: correlationID, + }) + b.checkMapping[correlationID] = key +} + +// BatchCheck implements authzv1.AuthzServiceServer.BatchCheck +// This performs multiple access checks in a single request using OpenFGA's native BatchCheck API. +func (s *Server) BatchCheck(ctx context.Context, r *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) { ctx, span := s.tracer.Start(ctx, "server.BatchCheck") defer span.End() - if err := authorize(ctx, r.GetNamespace(), s.cfg); err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - return nil, err - } + span.SetAttributes(attribute.Int("check_count", len(r.GetChecks()))) - batchRes := &authzextv1.BatchCheckResponse{ - Groups: make(map[string]*authzextv1.BatchCheckGroupResource), - } + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.BatchCheck", "").Observe(time.Since(t).Seconds()) + }(time.Now()) - store, err := s.getStoreInfo(ctx, r.GetNamespace()) + res, err := s.batchCheck(ctx, r) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) + s.logger.Error("failed to perform batch check request", "error", err) + return nil, fmt.Errorf("failed to perform batch check request: %w", err) + } + + return res, nil +} + +func (s *Server) batchCheck(ctx context.Context, r *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) { + items := r.GetChecks() + if len(items) == 0 { + return &authzv1.BatchCheckResponse{ + Results: make(map[string]*authzv1.BatchCheckResult), + }, nil + } + + namespace := r.GetNamespace() + if err := authorize(ctx, namespace, s.cfg); err != nil { + return nil, err + } + + store, err := s.getStoreInfo(ctx, namespace) + if err != nil { return nil, err } contextuals, err := s.getContextuals(r.GetSubject()) if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) return nil, err } - groupResourceAccess := make(map[string]bool) + results := make(map[string]*authzv1.BatchCheckResult, len(items)) + subject := r.GetSubject() - for _, item := range r.GetItems() { - res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - return nil, err + // Phase 1: Check GroupResource access (broadest permissions) + // Example: user has "get" on "dashboards" group_resource → all dashboards allowed + s.runGroupResourcePhase(ctx, store, subject, items, contextuals, results) + if len(results) == len(items) { + return s.buildResponse(results), nil + } + + // Phase 2: Check folder permission inheritance (can_get, can_create, etc. on parent folder) + // Example: user has "can_get" on folder-A → all dashboards in folder-A allowed + s.runFolderPermissionPhase(ctx, store, subject, items, contextuals, results) + if len(results) == len(items) { + return s.buildResponse(results), nil + } + + // Phase 3: Check folder subresource access (folder_get, folder_create, etc.) + // Example: user has "folder_get" on folder-A → dashboards in folder-A allowed via subresource + s.runFolderSubresourcePhase(ctx, store, subject, items, contextuals, results) + if len(results) == len(items) { + return s.buildResponse(results), nil + } + + // Phase 4: Check direct resource access + // Example: user has "get" directly on dashboard-123 + s.runDirectResourcePhase(ctx, store, subject, items, contextuals, results) + + // Mark any remaining unresolved items as denied + for _, item := range items { + if _, resolved := results[item.GetCorrelationId()]; !resolved { + results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false} + } + } + + return s.buildResponse(results), nil +} + +func (s *Server) buildResponse(results map[string]*authzv1.BatchCheckResult) *authzv1.BatchCheckResponse { + return &authzv1.BatchCheckResponse{ + Results: results, + Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()}, + } +} + +// runGroupResourcePhase checks if the user has GroupResource-level access. +// This is the broadest permission - if allowed, all items in that group are allowed. +func (s *Server) runGroupResourcePhase( + ctx context.Context, + store *storeInfo, + subject string, + items []*authzv1.BatchCheckItem, + contextuals *openfgav1.ContextualTupleKeys, + results map[string]*authzv1.BatchCheckResult, +) { + // Group items by their GroupResource + type grInfo struct { + relation string + grIdent string + items []string // correlation IDs + } + groupedItems := make(map[string]*grInfo) // groupResource -> info + + for _, item := range items { + relation := common.VerbMapping[item.GetVerb()] + if !common.IsGroupResourceRelation(relation) { + continue } - groupResource := common.FormatGroupResource(item.GetGroup(), item.GetResource(), item.GetSubresource()) - if _, ok := batchRes.Groups[groupResource]; !ok { - batchRes.Groups[groupResource] = &authzextv1.BatchCheckGroupResource{ - Items: make(map[string]bool), + resource := common.NewResourceInfoFromBatchItem(item) + gr := resource.GroupResource() + + if _, exists := groupedItems[gr]; !exists { + groupedItems[gr] = &grInfo{ + relation: relation, + grIdent: resource.GroupResourceIdent(), + items: make([]string, 0), } } - batchRes.Groups[groupResource].Items[item.GetName()] = res.GetAllowed() + groupedItems[gr].items = append(groupedItems[gr].items, item.GetCorrelationId()) } - return batchRes, nil + if len(groupedItems) == 0 { + return + } + + // Build batch check for unique GroupResources + builder := newBatchCheckBuilder(subject, contextuals) + grCheckMapping := make(map[string]string) // OpenFGA correlationID -> groupResource + + for gr, info := range groupedItems { + correlationID := fmt.Sprintf("gr%d", builder.counter) + builder.counter++ + builder.checks = append(builder.checks, &openfgav1.BatchCheckItem{ + TupleKey: &openfgav1.CheckRequestTupleKey{ + User: subject, + Relation: info.relation, + Object: info.grIdent, + }, + ContextualTuples: contextuals, + CorrelationId: correlationID, + }) + grCheckMapping[correlationID] = gr + } + + openfgaRes, err := s.openfgaClient.BatchCheck(ctx, &openfgav1.BatchCheckRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + Checks: builder.checks, + }) + if err != nil { + s.logger.Warn("Failed to check group resource access", "error", err) + return + } + + // Mark all items in allowed GroupResources + for correlationID, result := range openfgaRes.GetResult() { + gr := grCheckMapping[correlationID] + if allowed, ok := result.GetCheckResult().(*openfgav1.BatchCheckSingleResult_Allowed); ok && allowed.Allowed { + for _, itemCorrelationID := range groupedItems[gr].items { + results[itemCorrelationID] = &authzv1.BatchCheckResult{Allowed: true} + } + } + } } -func (s *Server) batchCheckItem( +// runFolderPermissionPhase checks folder permission inheritance (can_get, can_create, etc.). +// This applies to folder-based resources like dashboards, panels, etc. +func (s *Server) runFolderPermissionPhase( ctx context.Context, - r *authzextv1.BatchCheckRequest, - item *authzextv1.BatchCheckItem, - contextuals *openfgav1.ContextualTupleKeys, store *storeInfo, - groupResourceAccess map[string]bool, -) (*authzv1.CheckResponse, error) { - var ( - relation = common.VerbMapping[item.GetVerb()] - resource = common.NewResourceInfoFromBatchItem(item) - groupResource = resource.GroupResource() - ) + subject string, + items []*authzv1.BatchCheckItem, + contextuals *openfgav1.ContextualTupleKeys, + results map[string]*authzv1.BatchCheckResult, +) { + builder := newBatchCheckBuilder(subject, contextuals) + checkToItems := make(map[checkKey][]string) // checkKey -> correlation IDs - allowed, ok := groupResourceAccess[groupResource] - if !ok { - res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, contextuals, store) - if err != nil { - return nil, err + for _, item := range items { + if _, resolved := results[item.GetCorrelationId()]; resolved { + continue } - allowed = res.GetAllowed() - groupResourceAccess[groupResource] = res.GetAllowed() + resource := common.NewResourceInfoFromBatchItem(item) + folderIdent := resource.FolderIdent() + + // Only folder-based generic resources use folder permission inheritance + if !resource.IsGeneric() || folderIdent == "" || !isFolderPermissionBasedResource(resource.GroupResource()) { + continue + } + + relation := common.VerbMapping[item.GetVerb()] + rel := common.FolderPermissionRelation(relation) + key := checkKey{relation: rel, object: folderIdent} + checkToItems[key] = append(checkToItems[key], item.GetCorrelationId()) + builder.addCheck(rel, folderIdent, resource.Context()) } - if allowed { - return &authzv1.CheckResponse{Allowed: true}, nil + if len(builder.checks) == 0 { + return } - if resource.IsGeneric() { - return s.checkGeneric(ctx, r.GetSubject(), relation, resource, contextuals, store) + checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder) + if err != nil { + s.logger.Warn("Failed folder permission phase", "error", err) + return } - return s.checkTyped(ctx, r.GetSubject(), relation, resource, contextuals, store) + // Mark items allowed by folder permissions + for key, allowed := range checkResults { + if allowed { + for _, correlationID := range checkToItems[key] { + results[correlationID] = &authzv1.BatchCheckResult{Allowed: true} + } + } + } +} + +// runFolderSubresourcePhase checks folder subresource access (folder_get, folder_create, etc.). +func (s *Server) runFolderSubresourcePhase( + ctx context.Context, + store *storeInfo, + subject string, + items []*authzv1.BatchCheckItem, + contextuals *openfgav1.ContextualTupleKeys, + results map[string]*authzv1.BatchCheckResult, +) { + builder := newBatchCheckBuilder(subject, contextuals) + checkToItems := make(map[checkKey][]string) + + for _, item := range items { + if _, resolved := results[item.GetCorrelationId()]; resolved { + continue + } + + resource := common.NewResourceInfoFromBatchItem(item) + relation := common.VerbMapping[item.GetVerb()] + + var objectIdent string + var subresRel string + + if resource.IsGeneric() { + // Generic resources: check subresource on folder + folderIdent := resource.FolderIdent() + if folderIdent == "" { + continue + } + subresRel = common.SubresourceRelation(relation) + if !common.IsSubresourceRelation(subresRel) { + continue + } + objectIdent = folderIdent + } else { + // Typed resources: check subresource on the resource itself + if !resource.HasSubresource() || !resource.IsValidRelation(relation) { + continue + } + objectIdent = resource.ResourceIdent() + if objectIdent == "" { + continue + } + subresRel = common.SubresourceRelation(relation) + } + + key := checkKey{relation: subresRel, object: objectIdent} + checkToItems[key] = append(checkToItems[key], item.GetCorrelationId()) + builder.addCheck(subresRel, objectIdent, resource.Context()) + } + + if len(builder.checks) == 0 { + return + } + + checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder) + if err != nil { + s.logger.Warn("Failed folder subresource phase", "error", err) + return + } + + for key, allowed := range checkResults { + if allowed { + for _, correlationID := range checkToItems[key] { + results[correlationID] = &authzv1.BatchCheckResult{Allowed: true} + } + } + } +} + +// runDirectResourcePhase checks direct resource access. +func (s *Server) runDirectResourcePhase( + ctx context.Context, + store *storeInfo, + subject string, + items []*authzv1.BatchCheckItem, + contextuals *openfgav1.ContextualTupleKeys, + results map[string]*authzv1.BatchCheckResult, +) { + builder := newBatchCheckBuilder(subject, contextuals) + checkToItems := make(map[checkKey][]string) + + for _, item := range items { + if _, resolved := results[item.GetCorrelationId()]; resolved { + continue + } + + resource := common.NewResourceInfoFromBatchItem(item) + relation := common.VerbMapping[item.GetVerb()] + + if !resource.IsValidRelation(relation) { + continue + } + + resourceIdent := resource.ResourceIdent() + if resourceIdent == "" { + continue + } + + // For folders, use the computed permission relation + checkRelation := relation + if resource.Type() == common.TypeFolder { + checkRelation = common.FolderPermissionRelation(relation) + } + + key := checkKey{relation: checkRelation, object: resourceIdent} + checkToItems[key] = append(checkToItems[key], item.GetCorrelationId()) + builder.addCheck(checkRelation, resourceIdent, resource.Context()) + } + + if len(builder.checks) == 0 { + return + } + + checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder) + if err != nil { + s.logger.Warn("Failed direct resource phase", "error", err) + return + } + + for key, allowed := range checkResults { + if allowed { + for _, correlationID := range checkToItems[key] { + results[correlationID] = &authzv1.BatchCheckResult{Allowed: true} + } + } + } +} + +// executeOpenFGABatchChecks executes the OpenFGA batch checks in chunks and returns results +func (s *Server) executeOpenFGABatchChecks(ctx context.Context, store *storeInfo, builder *batchCheckBuilder) (map[checkKey]bool, error) { + const maxChecksPerBatch = 50 + checkResults := make(map[checkKey]bool) + + for i := 0; i < len(builder.checks); i += maxChecksPerBatch { + end := i + maxChecksPerBatch + if end > len(builder.checks) { + end = len(builder.checks) + } + + openfgaRes, err := s.openfgaClient.BatchCheck(ctx, &openfgav1.BatchCheckRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + Checks: builder.checks[i:end], + }) + if err != nil { + return nil, fmt.Errorf("failed to perform OpenFGA batch check: %w", err) + } + + // Process results + for correlationID, result := range openfgaRes.GetResult() { + key, ok := builder.checkMapping[correlationID] + if !ok { + continue + } + if allowed, ok := result.GetCheckResult().(*openfgav1.BatchCheckSingleResult_Allowed); ok { + checkResults[key] = allowed.Allowed + } + } + } + + return checkResults, nil } diff --git a/pkg/services/authz/zanzana/server/server_batch_check_test.go b/pkg/services/authz/zanzana/server/server_batch_check_test.go index 6cb08032baa..fb337561278 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check_test.go +++ b/pkg/services/authz/zanzana/server/server_batch_check_test.go @@ -1,193 +1,192 @@ package server import ( + "fmt" "testing" + authzv1 "github.com/grafana/authlib/authz/proto/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/utils" - authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) func testBatchCheck(t *testing.T, server *Server) { - newReq := func(subject, verb, group, resource, subresource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest { - for i, item := range items { - items[i] = &authzextv1.BatchCheckItem{ - Verb: verb, - Group: group, - Resource: resource, - Subresource: subresource, - Name: item.GetName(), - Folder: item.GetFolder(), - } - } - - return &authzextv1.BatchCheckRequest{ - Namespace: namespace, + // Helper to create a batch check request + newReq := func(subject string, items []*authzv1.BatchCheckItem) *authzv1.BatchCheckRequest { + return &authzv1.BatchCheckRequest{ Subject: subject, - Items: items, + Namespace: namespace, + Checks: items, + } + } + + // Helper to create a batch check item with correlation ID + newItem := func(verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem { + correlationID := fmt.Sprintf("%s-%s-%s-%s", group, resource, folder, name) + return &authzv1.BatchCheckItem{ + Verb: verb, + Group: group, + Resource: resource, + Subresource: subresource, + Name: name, + Folder: folder, + CorrelationId: correlationID, } } t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ - {Name: "1", Folder: "1"}, - {Name: "2", Folder: "2"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"), })) require.NoError(t, err) - require.Len(t, res.Groups[groupResource].Items, 2) + require.Len(t, res.Results, 2) - assert.True(t, res.Groups[groupResource].Items["1"]) - assert.False(t, res.Groups[groupResource].Items["2"]) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed) + assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed) }) t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ - {Name: "1", Folder: "1"}, - {Name: "2", Folder: "2"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"), })) require.NoError(t, err) - assert.Len(t, res.Groups[groupResource].Items, 2) + require.Len(t, res.Results, 2) + + // user:2 has group_resource access, so both should be allowed + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed) }) t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ - {Name: "1", Folder: "1"}, - {Name: "2", Folder: "2"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"), })) require.NoError(t, err) - require.Len(t, res.Groups[groupResource].Items, 2) + require.Len(t, res.Results, 2) - assert.True(t, res.Groups[groupResource].Items["1"]) - assert.False(t, res.Groups[groupResource].Items["2"]) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed) + assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed) }) t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ - {Name: "1", Folder: "1"}, - {Name: "2", Folder: "3"}, - {Name: "3", Folder: "2"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "3"), })) require.NoError(t, err) - require.Len(t, res.Groups[groupResource].Items, 3) + require.Len(t, res.Results, 3) - assert.True(t, res.Groups[groupResource].Items["1"]) - assert.True(t, res.Groups[groupResource].Items["2"]) - assert.False(t, res.Groups[groupResource].Items["3"]) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "3", "2")].Allowed) + assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "3")].Allowed) }) t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ - {Name: "1", Folder: "1"}, - {Name: "2", Folder: "2"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"), })) require.NoError(t, err) - require.Len(t, res.Groups[groupResource].Items, 2) + require.Len(t, res.Results, 2) - assert.True(t, res.Groups[groupResource].Items["1"]) - assert.False(t, res.Groups[groupResource].Items["2"]) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed) + assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed) }) t.Run("user:6 should be able to read folder 1", func(t *testing.T) { - groupResource := common.FormatGroupResource(folderGroup, folderResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{ - {Name: "1"}, - {Name: "2"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, folderGroup, folderResource, "", "", "1"), + newItem(utils.VerbGet, folderGroup, folderResource, "", "", "2"), })) require.NoError(t, err) - require.Len(t, res.Groups[groupResource].Items, 2) + require.Len(t, res.Results, 2) - assert.True(t, res.Groups[groupResource].Items["1"]) - assert.False(t, res.Groups[groupResource].Items["2"]) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "1")].Allowed) + assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "2")].Allowed) }) t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) { - groupResource := common.FormatGroupResource(folderGroup, folderResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{ - {Name: "1"}, - {Name: "2"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, folderGroup, folderResource, "", "", "1"), + newItem(utils.VerbGet, folderGroup, folderResource, "", "", "2"), })) require.NoError(t, err) - require.Len(t, res.Groups[groupResource].Items, 2) - require.True(t, res.Groups[groupResource].Items["1"]) - require.True(t, res.Groups[groupResource].Items["2"]) + require.Len(t, res.Results, 2) + + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "1")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "2")].Allowed) }) - t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ - {Name: "10", Folder: "6"}, - {Name: "20", Folder: "6"}, + t.Run("user:8 should be able to read all resource:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) { + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "20"), })) require.NoError(t, err) - require.Len(t, res.Groups[groupResource].Items, 2) - require.True(t, res.Groups[groupResource].Items["10"]) - require.True(t, res.Groups[groupResource].Items["20"]) + require.Len(t, res.Results, 2) + + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "20")].Allowed) }) t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "") - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{ - {Name: "10", Folder: "6"}, - {Name: "20", Folder: "6"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", []*authzv1.BatchCheckItem{ + newItem(utils.VerbCreate, dashboardGroup, dashboardResource, "", "6", "10"), + newItem(utils.VerbCreate, dashboardGroup, dashboardResource, "", "6", "20"), })) require.NoError(t, err) - t.Log(res.Groups) - require.Len(t, res.Groups[groupResource].Items, 2) - require.True(t, res.Groups[groupResource].Items["10"]) - require.True(t, res.Groups[groupResource].Items["20"]) + require.Len(t, res.Results, 2) + + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "20")].Allowed) }) t.Run("user:10 should be able to get dashboard status for 10 and 11", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ - {Name: "10", Folder: "6"}, - {Name: "11", Folder: "6"}, - {Name: "12", Folder: "6"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "10"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"), })) require.NoError(t, err) - t.Log(res.Groups) - require.Len(t, res.Groups[groupResource].Items, 3) - require.True(t, res.Groups[groupResource].Items["10"]) - require.True(t, res.Groups[groupResource].Items["11"]) - require.False(t, res.Groups[groupResource].Items["12"]) + require.Len(t, res.Results, 3) + + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed) + assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed) }) t.Run("user:11 should be able to get dashboard status for 10, 11 and 12 through group_resource", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ - {Name: "10", Folder: "6"}, - {Name: "11", Folder: "6"}, - {Name: "12", Folder: "6"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "10"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"), })) require.NoError(t, err) - t.Log(res.Groups) - require.Len(t, res.Groups[groupResource].Items, 3) - require.True(t, res.Groups[groupResource].Items["10"]) - require.True(t, res.Groups[groupResource].Items["11"]) - require.True(t, res.Groups[groupResource].Items["12"]) + require.Len(t, res.Results, 3) + + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed) }) t.Run("user:12 should be able to get dashboard status in folder 5 and 6", func(t *testing.T) { - groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource) - res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{ - {Name: "10", Folder: "5"}, - {Name: "11", Folder: "6"}, - {Name: "12", Folder: "6"}, - {Name: "13", Folder: "1"}, + res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", []*authzv1.BatchCheckItem{ + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"), + newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13"), })) require.NoError(t, err) - require.Len(t, res.Groups[groupResource].Items, 4) - require.True(t, res.Groups[groupResource].Items["10"]) - require.True(t, res.Groups[groupResource].Items["11"]) - require.True(t, res.Groups[groupResource].Items["12"]) - require.False(t, res.Groups[groupResource].Items["13"]) + require.Len(t, res.Results, 4) + + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "5", "10")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed) + assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed) + assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "13")].Allowed) }) } diff --git a/pkg/services/authz/zanzana/server/server_bench_test.go b/pkg/services/authz/zanzana/server/server_bench_test.go index 1d76dbea66b..6ffed51990e 100644 --- a/pkg/services/authz/zanzana/server/server_bench_test.go +++ b/pkg/services/authz/zanzana/server/server_bench_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" "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/common" "github.com/grafana/grafana/pkg/services/authz/zanzana/store" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -37,14 +36,14 @@ const ( // Timeout for List operations listTimeout = 30 * time.Second + // BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch. + batchCheckSize = 50 + // Resource type constants for benchmarks benchDashboardGroup = "dashboard.grafana.app" benchDashboardResource = "dashboards" benchFolderGroup = "folder.grafana.app" benchFolderResource = "folders" - - // BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch. - batchCheckSize = 50 ) // benchmarkData holds all the generated test data for benchmarks @@ -338,6 +337,14 @@ func setupBenchmarkServer(b *testing.B) (*Server, *benchmarkData) { } cfg := setting.NewCfg() + + cfg.ZanzanaServer.CacheSettings.CheckCacheLimit = 100000 // Cache check results + cfg.ZanzanaServer.CacheSettings.CheckQueryCacheEnabled = true // Cache check subproblems + cfg.ZanzanaServer.CacheSettings.CheckIteratorCacheEnabled = true // Cache DB iterators for checks + cfg.ZanzanaServer.CacheSettings.CheckIteratorCacheMaxResults = 10000 // Max results per iterator + cfg.ZanzanaServer.CacheSettings.SharedIteratorEnabled = true // Share iterators across concurrent checks + cfg.ZanzanaServer.CacheSettings.SharedIteratorLimit = 10000 // Max shared iterators + testStore := sqlstore.NewTestStore(b, sqlstore.WithCfg(cfg)) openFGAStore, err := store.NewEmbeddedStore(cfg, testStore, log.NewNopLogger()) @@ -573,58 +580,62 @@ func BenchmarkCheck(b *testing.B) { }) } +// BenchmarkBatchCheck measures the performance of BatchCheck requests func BenchmarkBatchCheck(b *testing.B) { srv, data := setupBenchmarkServer(b) ctx := newContextWithNamespace() - // Helper to create batch check requests - newBatchCheckReq := func(subject string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest { - return &authzextv1.BatchCheckRequest{ - Namespace: benchNamespace, + // Helper to create batch check requests using the new authzv1 API + newBatchCheckReq := func(subject string, items []*authzv1.BatchCheckItem) *authzv1.BatchCheckRequest { + return &authzv1.BatchCheckRequest{ Subject: subject, - Items: items, + Namespace: benchNamespace, + Checks: items, } } // Helper to create batch items for resources in folders - createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzextv1.BatchCheckItem { - items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize) + createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzv1.BatchCheckItem { + items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize) for i := 0; i < batchCheckSize && i < len(resources); i++ { resource := resources[i] - items = append(items, &authzextv1.BatchCheckItem{ - Verb: utils.VerbGet, - Group: benchDashboardGroup, - Resource: benchDashboardResource, - Name: resource, - Folder: resourceFolders[resource], + items = append(items, &authzv1.BatchCheckItem{ + Verb: utils.VerbGet, + Group: benchDashboardGroup, + Resource: benchDashboardResource, + Name: resource, + Folder: resourceFolders[resource], + CorrelationId: fmt.Sprintf("item-%d", i), }) } return items } // Helper to create batch items for folders at a specific depth - createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzextv1.BatchCheckItem { - items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize) + createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzv1.BatchCheckItem { + items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize) for _, folder := range folders { if folderDepths[folder] == depth && len(items) < batchCheckSize { - items = append(items, &authzextv1.BatchCheckItem{ - Verb: utils.VerbGet, - Group: benchDashboardGroup, - Resource: benchDashboardResource, - Name: fmt.Sprintf("resource-in-%s", folder), - Folder: folder, + items = append(items, &authzv1.BatchCheckItem{ + Verb: utils.VerbGet, + Group: benchDashboardGroup, + Resource: benchDashboardResource, + Name: fmt.Sprintf("resource-in-%s", folder), + Folder: folder, + CorrelationId: fmt.Sprintf("item-%d", len(items)), }) } } // Fill remaining slots if needed for len(items) < batchCheckSize && len(folders) > 0 { folder := folders[len(items)%len(folders)] - items = append(items, &authzextv1.BatchCheckItem{ - Verb: utils.VerbGet, - Group: benchDashboardGroup, - Resource: benchDashboardResource, - Name: fmt.Sprintf("resource-%d", len(items)), - Folder: folder, + items = append(items, &authzv1.BatchCheckItem{ + Verb: utils.VerbGet, + Group: benchDashboardGroup, + Resource: benchDashboardResource, + Name: fmt.Sprintf("resource-%d", len(items)), + Folder: folder, + CorrelationId: fmt.Sprintf("item-%d", len(items)), }) } return items @@ -636,6 +647,7 @@ func BenchmarkBatchCheck(b *testing.B) { // User with group_resource permission - should have access to everything user := data.users[0] items := createBatchItems(data.resources, data.resourceFolders) + b.Logf("Testing BatchCheck with %d items, user has group_resource permission (all access)", len(items)) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -643,7 +655,7 @@ func BenchmarkBatchCheck(b *testing.B) { if err != nil { b.Fatal(err) } - _ = res.Groups + _ = res.Results } }) @@ -651,6 +663,7 @@ func BenchmarkBatchCheck(b *testing.B) { // User with folder permission on shallow folder user := data.users[usersPerPattern] items := createFolderBatchItems(data.folders, 1, data.folderDepths) + b.Logf("Testing BatchCheck with %d items at depth 1", len(items)) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -658,7 +671,7 @@ func BenchmarkBatchCheck(b *testing.B) { if err != nil { b.Fatal(err) } - _ = res.Groups + _ = res.Results } }) @@ -666,6 +679,7 @@ func BenchmarkBatchCheck(b *testing.B) { // User with folder permission on mid-depth folder user := data.users[2*usersPerPattern] items := createFolderBatchItems(data.folders, 4, data.folderDepths) + b.Logf("Testing BatchCheck with %d items at depth 4", len(items)) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -673,22 +687,7 @@ func BenchmarkBatchCheck(b *testing.B) { if err != nil { b.Fatal(err) } - _ = res.Groups - } - }) - - b.Run("FolderInheritance/Depth7", func(b *testing.B) { - // Check access on deepest folders (worst case for inheritance traversal) - user := data.users[usersPerPattern] - items := createFolderBatchItems(data.folders, data.maxDepth, data.folderDepths) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) - if err != nil { - b.Fatal(err) - } - _ = res.Groups + _ = res.Results } }) @@ -696,6 +695,7 @@ func BenchmarkBatchCheck(b *testing.B) { // User with direct resource permission user := data.users[4*usersPerPattern] items := createBatchItems(data.resources, data.resourceFolders) + b.Logf("Testing BatchCheck with %d items, user has direct resource permission", len(items)) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -703,22 +703,7 @@ func BenchmarkBatchCheck(b *testing.B) { if err != nil { b.Fatal(err) } - _ = res.Groups - } - }) - - b.Run("TeamMembership", func(b *testing.B) { - // User who is a team member, team has folder permission - user := data.users[5*usersPerPattern] - items := createBatchItems(data.resources, data.resourceFolders) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) - if err != nil { - b.Fatal(err) - } - _ = res.Groups + _ = res.Results } }) @@ -726,6 +711,7 @@ func BenchmarkBatchCheck(b *testing.B) { // User with no permissions - tests denial path user := data.users[len(data.users)-1] items := createBatchItems(data.resources, data.resourceFolders) + b.Logf("Testing BatchCheck with %d items, user has NO permissions (denial case)", len(items)) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -733,24 +719,28 @@ func BenchmarkBatchCheck(b *testing.B) { if err != nil { b.Fatal(err) } - _ = res.Groups + _ = res.Results } }) - b.Run("MixedFolders", func(b *testing.B) { - // Batch of items across different folder depths - user := data.users[usersPerPattern] - items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize) + b.Run("MixedAccess", func(b *testing.B) { + // Create items from different folders - user has access to some but not all + user := data.users[3*usersPerPattern] // folder-scoped resource permission + items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize) + + // Mix of accessible and inaccessible resources for i := 0; i < batchCheckSize; i++ { folder := data.folders[i%len(data.folders)] - items = append(items, &authzextv1.BatchCheckItem{ - Verb: utils.VerbGet, - Group: benchDashboardGroup, - Resource: benchDashboardResource, - Name: fmt.Sprintf("resource-%d", i), - Folder: folder, + items = append(items, &authzv1.BatchCheckItem{ + Verb: utils.VerbGet, + Group: benchDashboardGroup, + Resource: benchDashboardResource, + Name: fmt.Sprintf("resource-%d", i), + Folder: folder, + CorrelationId: fmt.Sprintf("item-%d", i), }) } + b.Logf("Testing BatchCheck with %d items, user has mixed access (some allowed, some denied)", len(items)) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -758,9 +748,31 @@ func BenchmarkBatchCheck(b *testing.B) { if err != nil { b.Fatal(err) } - _ = res.Groups + _ = res.Results } }) + + // Test BatchCheck at various folder depths + for depth := 0; depth <= data.maxDepth; depth++ { + depth := depth // capture for closure + if len(data.foldersByDepth[depth]) == 0 { + continue + } + b.Run(fmt.Sprintf("ByDepth/Depth%d", depth), func(b *testing.B) { + user := fmt.Sprintf("user:depth-%d-access", depth) + items := createFolderBatchItems(data.folders, depth, data.folderDepths) + b.Logf("Testing BatchCheck with %d items at depth %d", len(items), depth) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items)) + if err != nil { + b.Fatal(err) + } + _ = res.Results + } + }) + } } // BenchmarkList measures the performance of List requests (Compile equivalent)