diff --git a/pkg/services/accesscontrol/dualwrite/collectors_test.go b/pkg/services/accesscontrol/dualwrite/collectors_test.go index 32e021c4b1c..aef03185330 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors_test.go +++ b/pkg/services/accesscontrol/dualwrite/collectors_test.go @@ -246,6 +246,14 @@ func (m *mockZanzanaClient) Mutate(ctx context.Context, req *authzextv1.MutateRe return nil } +func (m *mockZanzanaClient) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*authzextv1.QueryResponse), args.Error(1) +} + func TestIntegrationTeamMembershipCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) diff --git a/pkg/services/authz/proto/v1/extention.pb.go b/pkg/services/authz/proto/v1/extention.pb.go index 0cc4cb36a16..03a44978db9 100644 --- a/pkg/services/authz/proto/v1/extention.pb.go +++ b/pkg/services/authz/proto/v1/extention.pb.go @@ -1621,6 +1621,280 @@ func (x *BatchCheckGroupResource) GetItems() map[string]bool { return nil } +type QueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Operation *QueryOperation `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryRequest) Reset() { + *x = QueryRequest{} + mi := &file_extention_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRequest) ProtoMessage() {} + +func (x *QueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[27] + 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 QueryRequest.ProtoReflect.Descriptor instead. +func (*QueryRequest) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{27} +} + +func (x *QueryRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *QueryRequest) GetOperation() *QueryOperation { + if x != nil { + return x.Operation + } + return nil +} + +type QueryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Result: + // + // *QueryResponse_FolderParents + Result isQueryResponse_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryResponse) Reset() { + *x = QueryResponse{} + mi := &file_extention_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryResponse) ProtoMessage() {} + +func (x *QueryResponse) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[28] + 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 QueryResponse.ProtoReflect.Descriptor instead. +func (*QueryResponse) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{28} +} + +func (x *QueryResponse) GetResult() isQueryResponse_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *QueryResponse) GetFolderParents() *GetFolderParentsResult { + if x != nil { + if x, ok := x.Result.(*QueryResponse_FolderParents); ok { + return x.FolderParents + } + } + return nil +} + +type isQueryResponse_Result interface { + isQueryResponse_Result() +} + +type QueryResponse_FolderParents struct { + FolderParents *GetFolderParentsResult `protobuf:"bytes,1,opt,name=folder_parents,json=folderParents,proto3,oneof"` +} + +func (*QueryResponse_FolderParents) isQueryResponse_Result() {} + +type QueryOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Operation: + // + // *QueryOperation_GetFolderParents + Operation isQueryOperation_Operation `protobuf_oneof:"operation"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryOperation) Reset() { + *x = QueryOperation{} + mi := &file_extention_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryOperation) ProtoMessage() {} + +func (x *QueryOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[29] + 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 QueryOperation.ProtoReflect.Descriptor instead. +func (*QueryOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{29} +} + +func (x *QueryOperation) GetOperation() isQueryOperation_Operation { + if x != nil { + return x.Operation + } + return nil +} + +func (x *QueryOperation) GetGetFolderParents() *GetFolderParentsQuery { + if x != nil { + if x, ok := x.Operation.(*QueryOperation_GetFolderParents); ok { + return x.GetFolderParents + } + } + return nil +} + +type isQueryOperation_Operation interface { + isQueryOperation_Operation() +} + +type QueryOperation_GetFolderParents struct { + GetFolderParents *GetFolderParentsQuery `protobuf:"bytes,1,opt,name=get_folder_parents,json=getFolderParents,proto3,oneof"` +} + +func (*QueryOperation_GetFolderParents) isQueryOperation_Operation() {} + +type GetFolderParentsQuery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UID of the folder + Folder string `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFolderParentsQuery) Reset() { + *x = GetFolderParentsQuery{} + mi := &file_extention_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFolderParentsQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFolderParentsQuery) ProtoMessage() {} + +func (x *GetFolderParentsQuery) 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 GetFolderParentsQuery.ProtoReflect.Descriptor instead. +func (*GetFolderParentsQuery) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{30} +} + +func (x *GetFolderParentsQuery) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +type GetFolderParentsResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // List of parent folder UIDs + ParentUids []string `protobuf:"bytes,1,rep,name=parent_uids,json=parentUids,proto3" json:"parent_uids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFolderParentsResult) Reset() { + *x = GetFolderParentsResult{} + mi := &file_extention_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFolderParentsResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFolderParentsResult) ProtoMessage() {} + +func (x *GetFolderParentsResult) 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 GetFolderParentsResult.ProtoReflect.Descriptor instead. +func (*GetFolderParentsResult) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{31} +} + +func (x *GetFolderParentsResult) GetParentUids() []string { + if x != nil { + return x.ParentUids + } + return nil +} + var File_extention_proto protoreflect.FileDescriptor var file_extention_proto_rawDesc = string([]byte{ @@ -1861,33 +2135,66 @@ var file_extention_proto_rawDesc = string([]byte{ 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x32, 0xde, 0x02, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, - 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, - 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, - 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, - 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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, }) var ( @@ -1902,7 +2209,7 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 29) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 34) var file_extention_proto_goTypes = []any{ (*MutateRequest)(nil), // 0: authz.extention.v1.MutateRequest (*MutateResponse)(nil), // 1: authz.extention.v1.MutateResponse @@ -1931,11 +2238,16 @@ var file_extention_proto_goTypes = []any{ (*BatchCheckItem)(nil), // 24: authz.extention.v1.BatchCheckItem (*BatchCheckResponse)(nil), // 25: authz.extention.v1.BatchCheckResponse (*BatchCheckGroupResource)(nil), // 26: authz.extention.v1.BatchCheckGroupResource - nil, // 27: authz.extention.v1.BatchCheckResponse.GroupsEntry - nil, // 28: authz.extention.v1.BatchCheckGroupResource.ItemsEntry - (*timestamppb.Timestamp)(nil), // 29: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 30: google.protobuf.Struct - (*wrapperspb.Int32Value)(nil), // 31: google.protobuf.Int32Value + (*QueryRequest)(nil), // 27: authz.extention.v1.QueryRequest + (*QueryResponse)(nil), // 28: authz.extention.v1.QueryResponse + (*QueryOperation)(nil), // 29: authz.extention.v1.QueryOperation + (*GetFolderParentsQuery)(nil), // 30: authz.extention.v1.GetFolderParentsQuery + (*GetFolderParentsResult)(nil), // 31: authz.extention.v1.GetFolderParentsResult + nil, // 32: authz.extention.v1.BatchCheckResponse.GroupsEntry + nil, // 33: authz.extention.v1.BatchCheckGroupResource.ItemsEntry + (*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 35: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 36: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ 2, // 0: authz.extention.v1.MutateRequest.operations:type_name -> authz.extention.v1.MutateOperation @@ -1952,32 +2264,37 @@ var file_extention_proto_depIdxs = []int32{ 11, // 11: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission 15, // 12: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition 12, // 13: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey - 29, // 14: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp - 30, // 15: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 34, // 14: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 35, // 15: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct 17, // 16: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey - 31, // 17: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 36, // 17: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value 13, // 18: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple 12, // 19: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey 14, // 20: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition 19, // 21: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites 20, // 22: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes 24, // 23: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem - 27, // 24: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry - 28, // 25: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry - 26, // 26: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource - 23, // 27: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest - 16, // 28: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest - 21, // 29: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest - 0, // 30: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest - 25, // 31: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse - 18, // 32: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse - 22, // 33: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse - 1, // 34: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse - 31, // [31:35] is the sub-list for method output_type - 27, // [27:31] is the sub-list for method input_type - 27, // [27:27] is the sub-list for extension type_name - 27, // [27:27] is the sub-list for extension extendee - 0, // [0:27] is the sub-list for field type_name + 32, // 24: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry + 33, // 25: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry + 29, // 26: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation + 31, // 27: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult + 30, // 28: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery + 26, // 29: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource + 23, // 30: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest + 16, // 31: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 21, // 32: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 0, // 33: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest + 27, // 34: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest + 25, // 35: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse + 18, // 36: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 22, // 37: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 1, // 38: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse + 28, // 39: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse + 35, // [35:40] is the sub-list for method output_type + 30, // [30:35] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name } func init() { file_extention_proto_init() } @@ -1994,13 +2311,19 @@ func file_extention_proto_init() { (*MutateOperation_DeleteUserOrgRole)(nil), (*MutateOperation_AddUserOrgRole)(nil), } + file_extention_proto_msgTypes[28].OneofWrappers = []any{ + (*QueryResponse_FolderParents)(nil), + } + file_extention_proto_msgTypes[29].OneofWrappers = []any{ + (*QueryOperation_GetFolderParents)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_extention_proto_rawDesc), len(file_extention_proto_rawDesc)), NumEnums: 0, - NumMessages: 29, + NumMessages: 34, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index 37a05a5d75c..2a11b8c5b97 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -15,6 +15,7 @@ service AuthzExtentionService { rpc Write(WriteRequest) returns (WriteResponse); rpc Mutate(MutateRequest) returns (MutateResponse); + rpc Query(QueryRequest) returns (QueryResponse); } message MutateRequest { @@ -183,3 +184,30 @@ message BatchCheckResponse { message BatchCheckGroupResource { map items = 1; } + +message QueryRequest { + string namespace = 1; + QueryOperation operation = 2; +} + +message QueryResponse { + oneof result { + GetFolderParentsResult folder_parents = 1; + } +} + +message QueryOperation { + oneof operation { + GetFolderParentsQuery get_folder_parents = 1; + } +} + +message GetFolderParentsQuery { + // UID of the folder + string folder = 1; +} + +message GetFolderParentsResult { + // List of parent folder UIDs + repeated string parent_uids = 1; +} diff --git a/pkg/services/authz/proto/v1/extention_grpc.pb.go b/pkg/services/authz/proto/v1/extention_grpc.pb.go index f83b14c1c8d..b320dbe8c07 100644 --- a/pkg/services/authz/proto/v1/extention_grpc.pb.go +++ b/pkg/services/authz/proto/v1/extention_grpc.pb.go @@ -23,6 +23,7 @@ const ( 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. @@ -33,6 +34,7 @@ type AuthzExtentionServiceClient interface { 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) + Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryResponse, error) } type authzExtentionServiceClient struct { @@ -83,6 +85,16 @@ func (c *authzExtentionServiceClient) Mutate(ctx context.Context, in *MutateRequ return out, nil } +func (c *authzExtentionServiceClient) Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QueryResponse) + err := c.cc.Invoke(ctx, AuthzExtentionService_Query_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AuthzExtentionServiceServer is the server API for AuthzExtentionService service. // All implementations should embed UnimplementedAuthzExtentionServiceServer // for forward compatibility @@ -91,6 +103,7 @@ type AuthzExtentionServiceServer interface { Read(context.Context, *ReadRequest) (*ReadResponse, error) Write(context.Context, *WriteRequest) (*WriteResponse, error) Mutate(context.Context, *MutateRequest) (*MutateResponse, error) + Query(context.Context, *QueryRequest) (*QueryResponse, error) } // UnimplementedAuthzExtentionServiceServer should be embedded to have forward compatible implementations. @@ -109,6 +122,9 @@ func (UnimplementedAuthzExtentionServiceServer) Write(context.Context, *WriteReq func (UnimplementedAuthzExtentionServiceServer) Mutate(context.Context, *MutateRequest) (*MutateResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Mutate not implemented") } +func (UnimplementedAuthzExtentionServiceServer) Query(context.Context, *QueryRequest) (*QueryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Query not implemented") +} // UnsafeAuthzExtentionServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to AuthzExtentionServiceServer will @@ -193,6 +209,24 @@ func _AuthzExtentionService_Mutate_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _AuthzExtentionService_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthzExtentionServiceServer).Query(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthzExtentionService_Query_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthzExtentionServiceServer).Query(ctx, req.(*QueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AuthzExtentionService_ServiceDesc is the grpc.ServiceDesc for AuthzExtentionService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -216,6 +250,10 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Mutate", Handler: _AuthzExtentionService_Mutate_Handler, }, + { + MethodName: "Query", + Handler: _AuthzExtentionService_Query_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "extention.proto", diff --git a/pkg/services/authz/zanzana/client.go b/pkg/services/authz/zanzana/client.go index a95ead698cb..f238fd27159 100644 --- a/pkg/services/authz/zanzana/client.go +++ b/pkg/services/authz/zanzana/client.go @@ -16,4 +16,5 @@ type Client interface { 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 68809a84fa8..3c51d707561 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -90,3 +90,10 @@ func (c *Client) Mutate(ctx context.Context, req *authzextv1.MutateRequest) erro _, err := c.authzext.Mutate(ctx, req) return err } + +func (c *Client) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Query") + defer span.End() + + return c.authzext.Query(ctx, req) +} diff --git a/pkg/services/authz/zanzana/client/noop.go b/pkg/services/authz/zanzana/client/noop.go index d0397740b5e..73860cd0c4e 100644 --- a/pkg/services/authz/zanzana/client/noop.go +++ b/pkg/services/authz/zanzana/client/noop.go @@ -41,3 +41,7 @@ func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckR func (nc NoopClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { return nil } + +func (nc NoopClient) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + return nil, nil +} diff --git a/pkg/services/authz/zanzana/server/server_query.go b/pkg/services/authz/zanzana/server/server_query.go new file mode 100644 index 00000000000..596c3805a5a --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_query.go @@ -0,0 +1,91 @@ +package server + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + ctx, span := s.tracer.Start(ctx, "server.Query") + defer span.End() + + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.Query", req.GetNamespace()).Observe(time.Since(t).Seconds()) + }(time.Now()) + + res, err := s.query(ctx, req) + if err != nil { + s.logger.Error("failed to perform query request", "error", err, "namespace", req.GetNamespace()) + return nil, errors.New("failed to perform query request") + } + + return res, nil +} + +func (s *Server) query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + if err := authorize(ctx, req.GetNamespace(), s.cfg); err != nil { + return nil, err + } + + storeInf, err := s.getStoreInfo(ctx, req.Namespace) + if err != nil { + return nil, fmt.Errorf("failed to get openfga store: %w", err) + } + + if req.Operation == nil { + return nil, errors.New("operation cannot be nil") + } + + switch op := req.Operation.Operation.(type) { + case *authzextv1.QueryOperation_GetFolderParents: + return s.queryFolderParents(ctx, storeInf, op.GetFolderParents) + default: + return nil, errors.New("unsupported query operation type") + } +} + +func (s *Server) queryFolderParents(ctx context.Context, store *storeInfo, req *authzextv1.GetFolderParentsQuery) (*authzextv1.QueryResponse, error) { + ctx, span := s.tracer.Start(ctx, "server.queryFolderParents") + defer span.End() + + if req.GetFolder() == "" { + return nil, errors.New("folder UID cannot be empty") + } + + // Get raw tuples from OpenFGA + tuples, err := s.listFolderParents(ctx, store, req.GetFolder()) + if err != nil { + return nil, fmt.Errorf("failed to list folder parents: %w", err) + } + + // Extract parent UIDs from tuples (business logic now server-side) + parentUIDs := make([]string, 0, len(tuples)) + for _, tuple := range tuples { + // Extract UID from format "folder:UID" or "folder:UID#relation" + userParts := strings.Split(tuple.Key.User, ":") + if len(userParts) != 2 { + return nil, fmt.Errorf("invalid user format: %s, expected format: folder:UID or folder:UID#relation", tuple.Key.User) + } + + // Remove any relation part after # + uidAndRelationParts := strings.Split(userParts[1], "#") + if len(uidAndRelationParts) == 0 { + return nil, fmt.Errorf("invalid user format: %s, expected format: folder:UID or folder:UID#relation", tuple.Key.User) + } + + parentUIDs = append(parentUIDs, uidAndRelationParts[0]) + } + + return &authzextv1.QueryResponse{ + Result: &authzextv1.QueryResponse_FolderParents{ + FolderParents: &authzextv1.GetFolderParentsResult{ + ParentUids: parentUIDs, + }, + }, + }, nil +} diff --git a/pkg/services/authz/zanzana/server/server_query_folder_test.go b/pkg/services/authz/zanzana/server/server_query_folder_test.go new file mode 100644 index 00000000000..5e9284a59df --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_query_folder_test.go @@ -0,0 +1,133 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupFolders() []*openfgav1.TupleKey { + // seed tuples with a folder hierarchy: + // folder 1 (root) + // └── folder 11 + // ├── folder 111 + // └── folder 112 + // └── folder 12 + return []*openfgav1.TupleKey{ + common.NewFolderParentTuple("11", "1"), + common.NewFolderParentTuple("12", "1"), + common.NewFolderParentTuple("111", "11"), + common.NewFolderParentTuple("112", "11"), + } +} + +func setupQueryFolders(t *testing.T, srv *Server) *Server { + t.Helper() + + tuples := []*openfgav1.TupleKey{} + tuples = append(tuples, setupFolders()...) + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testQueryFolders(t *testing.T, srv *Server) { + setupQueryFolders(t, srv) + + t.Run("should query folder parents successfully", func(t *testing.T) { + res, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "11", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotNil(t, res.GetFolderParents()) + require.Len(t, res.GetFolderParents().ParentUids, 1) + require.Equal(t, "1", res.GetFolderParents().ParentUids[0]) + }) + + t.Run("should query nested folder parents successfully", func(t *testing.T) { + res, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "111", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotNil(t, res.GetFolderParents()) + require.Len(t, res.GetFolderParents().ParentUids, 1) + require.Equal(t, "11", res.GetFolderParents().ParentUids[0]) + }) + + t.Run("should return empty list for folder with no parents", func(t *testing.T) { + res, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "1", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotNil(t, res.GetFolderParents()) + require.Len(t, res.GetFolderParents().ParentUids, 0) + }) + + t.Run("should return empty list for non-existent folder", func(t *testing.T) { + res, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "non-existent", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotNil(t, res.GetFolderParents()) + require.Len(t, res.GetFolderParents().ParentUids, 0) + }) + + t.Run("should return error for empty folder UID", func(t *testing.T) { + _, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "", + }, + }, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to perform query request") + }) + + t.Run("should return error for nil operation", func(t *testing.T) { + _, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: nil, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to perform query request") + }) +} diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 720ff37e486..df4f90cf85f 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -132,6 +132,10 @@ func TestIntegrationServer(t *testing.T) { t.Run("test mutate org roles", func(t *testing.T) { testMutateOrgRoles(t, srv) }) + + t.Run("test query folders", func(t *testing.T) { + testQueryFolders(t, srv) + }) } func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {