From 2f6499cf72718e112edb5db787e12f1a9f96f7aa Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 16 Jul 2025 17:52:01 +0200 Subject: [PATCH] feat(org): Introduce pagination and filtering on ListMemberships Signed-off-by: Javier Rodriguez --- app/cli/cmd/organization_member_delete.go | 4 +- app/cli/cmd/organization_member_list.go | 89 ++++- app/cli/cmd/organization_member_update.go | 2 +- app/cli/cmd/output.go | 3 +- app/cli/documentation/cli-reference.mdx | 30 +- app/cli/internal/action/membership_list.go | 56 ++- .../api/controlplane/v1/organization.pb.go | 346 ++++++++++------- .../api/controlplane/v1/organization.proto | 18 +- .../frontend/controlplane/v1/organization.ts | 120 +++++- ...viceListMembershipsRequest.jsonschema.json | 46 ++- ...nServiceListMembershipsRequest.schema.json | 46 ++- ...iceListMembershipsResponse.jsonschema.json | 4 + ...ServiceListMembershipsResponse.schema.json | 4 + .../internal/service/organization.go | 36 +- app/controlplane/pkg/biz/membership.go | 35 +- .../pkg/biz/membership_integration_test.go | 356 +++++++++++++++++- app/controlplane/pkg/biz/orginvitation.go | 11 +- app/controlplane/pkg/data/membership.go | 94 ++++- 18 files changed, 1115 insertions(+), 185 deletions(-) diff --git a/app/cli/cmd/organization_member_delete.go b/app/cli/cmd/organization_member_delete.go index 73fe7da0b..bbfcc228c 100644 --- a/app/cli/cmd/organization_member_delete.go +++ b/app/cli/cmd/organization_member_delete.go @@ -25,12 +25,12 @@ import ( // Get the membership entry associated to the current user for the given organization func loadMembershipCurrentOrg(ctx context.Context, membershipID string) (*action.MembershipItem, error) { - memberships, err := action.NewMembershipList(actionOpts).ListMembers(ctx) + res, err := action.NewMembershipList(actionOpts).ListMembers(ctx, 1, 1, &action.ListMembersOpts{MembershipID: &membershipID}) if err != nil { return nil, fmt.Errorf("listing memberships: %w", err) } - for _, m := range memberships { + for _, m := range res.Memberships { if m.ID == membershipID { return m, nil } diff --git a/app/cli/cmd/organization_member_list.go b/app/cli/cmd/organization_member_list.go index fd57edd49..a95c6e38c 100644 --- a/app/cli/cmd/organization_member_list.go +++ b/app/cli/cmd/organization_member_list.go @@ -19,40 +19,109 @@ import ( "fmt" "time" + "github.com/chainloop-dev/chainloop/app/cli/cmd/options" "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" ) func newOrganizationMemberList() *cobra.Command { + var ( + paginationOpts = &options.OffsetPaginationOpts{} + name string + email string + role string + ) + cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List the members of the current organization", + Example: ` # Let the default pagination apply + chainloop organization member list + + # Specify the page and page size + chainloop organization member list --page 2 --limit 10 + + # Filter by name + chainloop organization member list --name alice + + # Filter by email + chainloop organization member list --email alice@example.com + + # Filter by role + chainloop organization member list --role admin + + # Combine filters and pagination + chainloop organization member list --role admin --page 2 --limit 5 +`, + PreRunE: func(_ *cobra.Command, _ []string) error { + if paginationOpts.Page < 1 { + return fmt.Errorf("--page must be greater or equal than 1") + } + if paginationOpts.Limit < 1 { + return fmt.Errorf("--limit must be greater or equal than 1") + } + + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { - res, err := action.NewMembershipList(actionOpts).ListMembers(cmd.Context()) + opts := &action.ListMembersOpts{} + + switch { + case name != "": + opts.Name = &name + case email != "": + opts.Email = &email + case role != "": + opts.Role = &role + } + + res, err := action.NewMembershipList(actionOpts).ListMembers(cmd.Context(), paginationOpts.Page, paginationOpts.Limit, opts) if err != nil { return err } - return encodeOutput(res, orgMembershipsTableOutput) + if err := encodeOutput(res, orgMembershipsTableOutput); err != nil { + return err + } + + pgResponse := res.PaginationMeta + + if pgResponse.TotalPages >= paginationOpts.Page { + inPage := min(paginationOpts.Limit, len(res.Memberships)) + lowerBound := (paginationOpts.Page - 1) * paginationOpts.Limit + logger.Info().Msg(fmt.Sprintf("Showing [%d-%d] out of %d", lowerBound+1, lowerBound+inPage, pgResponse.TotalCount)) + } + + if pgResponse.TotalCount > pgResponse.Page*pgResponse.PageSize { + logger.Info().Msg(fmt.Sprintf("Next page available: %d", pgResponse.Page+1)) + } + + return nil }, } + cmd.Flags().StringVar(&name, "name", "", "Filter by member name or last name") + cmd.Flags().StringVar(&email, "email", "", "Filter by member email") + cmd.Flags().StringVar(&role, "role", "", fmt.Sprintf("Role of the user in the organization, available %s", action.AvailableRoles[:3])) + paginationOpts.AddFlags(cmd) + return cmd } -func orgMembershipsTableOutput(items []*action.MembershipItem) error { - if len(items) == 0 { - fmt.Println(UserWithNoOrganizationMsg) - return nil - } - +func orgMembershipsTableOutput(res *action.ListMembershipResult) error { t := newTableWriter() t.AppendHeader(table.Row{"ID", "Email", "Role", "Joined At"}) - for _, i := range items { - t.AppendRow(table.Row{i.ID, i.User.PrintUserProfileWithEmail(), i.Role, i.CreatedAt.Format(time.RFC822)}) + for _, m := range res.Memberships { + t.AppendRow(table.Row{ + m.ID, + m.User.PrintUserProfileWithEmail(), + m.Role, + m.CreatedAt.Format(time.RFC822), + }) t.AppendSeparator() } diff --git a/app/cli/cmd/organization_member_update.go b/app/cli/cmd/organization_member_update.go index 5d24c51d2..d59adf331 100644 --- a/app/cli/cmd/organization_member_update.go +++ b/app/cli/cmd/organization_member_update.go @@ -46,7 +46,7 @@ func newOrganizationMemberUpdateCmd() *cobra.Command { return err } - return encodeOutput([]*action.MembershipItem{res}, orgMembershipsTableOutput) + return encodeOutput(&action.ListMembershipResult{Memberships: []*action.MembershipItem{res}}, orgMembershipsTableOutput) }, } diff --git a/app/cli/cmd/output.go b/app/cli/cmd/output.go index 488cbf55d..9310a119e 100644 --- a/app/cli/cmd/output.go +++ b/app/cli/cmd/output.go @@ -53,7 +53,8 @@ type tabulatedData interface { []*action.OrgInvitationItem | *action.APITokenItem | []*action.APITokenItem | - *action.AttestationStatusMaterial + *action.AttestationStatusMaterial | + *action.ListMembershipResult } var ErrOutputFormatNotImplemented = errors.New("format not implemented") diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 7ef836da1..bb5ce16ac 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2573,10 +2573,38 @@ List the members of the current organization chainloop organization member list [flags] ``` +Examples + +``` +Let the default pagination apply +chainloop organization member list + +Specify the page and page size +chainloop organization member list --page 2 --limit 10 + +Filter by name +chainloop organization member list --name alice + +Filter by email +chainloop organization member list --email alice@example.com + +Filter by role +chainloop organization member list --role admin + +Combine filters and pagination +chainloop organization member list --role admin --page 2 --limit 5 + +``` + Options ``` --h, --help help for list +--email string Filter by member email +-h, --help help for list +--limit int number of items to show (default 50) +--name string Filter by member name or last name +--page int page number (default 1) +--role string Role of the user in the organization, available admin, owner, viewer ``` Options inherited from parent commands diff --git a/app/cli/internal/action/membership_list.go b/app/cli/internal/action/membership_list.go index 7a9b207cd..1a3fb9d02 100644 --- a/app/cli/internal/action/membership_list.go +++ b/app/cli/internal/action/membership_list.go @@ -17,6 +17,7 @@ package action import ( "context" + "fmt" "strings" "time" @@ -43,6 +44,22 @@ type MembershipItem struct { Role Role `json:"role"` } +type ListMembersOpts struct { + // MembershipID Optional, if provided, filters by a specific membership ID + MembershipID *string + // Name is the name of the user to filter by + Name *string + // Email is the email of the user to filter by + Email *string + // Role is the role of the user to filter by + Role *string +} + +type ListMembershipResult struct { + Memberships []*MembershipItem + PaginationMeta *OffsetPagination +} + func NewMembershipList(cfg *ActionsOpts) *MembershipList { return &MembershipList{cfg} } @@ -63,10 +80,33 @@ func (action *MembershipList) ListOrgs(ctx context.Context) ([]*MembershipItem, return result, nil } -// List members of the current organization -func (action *MembershipList) ListMembers(ctx context.Context) ([]*MembershipItem, error) { +// ListMembers lists the members of an organization with pagination and optional filters. +func (action *MembershipList) ListMembers(ctx context.Context, page int, pageSize int, opts *ListMembersOpts) (*ListMembershipResult, error) { + if page < 1 { + return nil, fmt.Errorf("page must be greater or equal to 1") + } + if pageSize < 1 { + return nil, fmt.Errorf("page-size must be greater or equal to 1") + } + client := pb.NewOrganizationServiceClient(action.cfg.CPConnection) - resp, err := client.ListMemberships(ctx, &pb.OrganizationServiceListMembershipsRequest{}) + req := &pb.OrganizationServiceListMembershipsRequest{ + MembershipId: opts.MembershipID, + Name: opts.Name, + Email: opts.Email, + Pagination: &pb.OffsetPaginationRequest{ + Page: int32(page), + PageSize: int32(pageSize), + }, + } + + // If a role is specified, convert it to the protobuf enum + if opts.Role != nil { + casted := stringToPbRole(Role(*opts.Role)) + req.Role = &casted + } + + resp, err := client.ListMemberships(ctx, req) if err != nil { return nil, err } @@ -76,7 +116,15 @@ func (action *MembershipList) ListMembers(ctx context.Context) ([]*MembershipIte result = append(result, pbMembershipItemToAction(p)) } - return result, nil + return &ListMembershipResult{ + Memberships: result, + PaginationMeta: &OffsetPagination{ + Page: int(resp.GetPagination().GetPage()), + PageSize: int(resp.GetPagination().GetPageSize()), + TotalPages: int(resp.GetPagination().GetTotalPages()), + TotalCount: int(resp.GetPagination().GetTotalCount()), + }, + }, nil } func pbOrgItemToAction(in *pb.OrgItem) *OrgItem { diff --git a/app/controlplane/api/controlplane/v1/organization.pb.go b/app/controlplane/api/controlplane/v1/organization.pb.go index f8dfc560a..5c758d5e2 100644 --- a/app/controlplane/api/controlplane/v1/organization.pb.go +++ b/app/controlplane/api/controlplane/v1/organization.pb.go @@ -40,6 +40,16 @@ type OrganizationServiceListMembershipsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + + MembershipId *string `protobuf:"bytes,1,opt,name=membership_id,json=membershipId,proto3,oneof" json:"membership_id,omitempty"` + // Optional filter by user name + Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"` + // Optional filter to search by user email address + Email *string `protobuf:"bytes,3,opt,name=email,proto3,oneof" json:"email,omitempty"` + // Optional filter by role + Role *MembershipRole `protobuf:"varint,4,opt,name=role,proto3,enum=controlplane.v1.MembershipRole,oneof" json:"role,omitempty"` + // Pagination parameters to limit and offset results + Pagination *OffsetPaginationRequest `protobuf:"bytes,5,opt,name=pagination,proto3" json:"pagination,omitempty"` } func (x *OrganizationServiceListMembershipsRequest) Reset() { @@ -74,12 +84,49 @@ func (*OrganizationServiceListMembershipsRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_organization_proto_rawDescGZIP(), []int{0} } +func (x *OrganizationServiceListMembershipsRequest) GetMembershipId() string { + if x != nil && x.MembershipId != nil { + return *x.MembershipId + } + return "" +} + +func (x *OrganizationServiceListMembershipsRequest) GetName() string { + if x != nil && x.Name != nil { + return *x.Name + } + return "" +} + +func (x *OrganizationServiceListMembershipsRequest) GetEmail() string { + if x != nil && x.Email != nil { + return *x.Email + } + return "" +} + +func (x *OrganizationServiceListMembershipsRequest) GetRole() MembershipRole { + if x != nil && x.Role != nil { + return *x.Role + } + return MembershipRole_MEMBERSHIP_ROLE_UNSPECIFIED +} + +func (x *OrganizationServiceListMembershipsRequest) GetPagination() *OffsetPaginationRequest { + if x != nil { + return x.Pagination + } + return nil +} + type OrganizationServiceListMembershipsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Result []*OrgMembershipItem `protobuf:"bytes,1,rep,name=result,proto3" json:"result,omitempty"` + // Pagination information for the response + Pagination *OffsetPaginationResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` } func (x *OrganizationServiceListMembershipsResponse) Reset() { @@ -121,6 +168,13 @@ func (x *OrganizationServiceListMembershipsResponse) GetResult() []*OrgMembershi return nil } +func (x *OrganizationServiceListMembershipsResponse) GetPagination() *OffsetPaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + type OrganizationServiceDeleteMembershipRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -513,117 +567,142 @@ var file_controlplane_v1_organization_proto_rawDesc = []byte{ 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x27, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, - 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2b, 0x0a, 0x29, 0x4f, - 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x68, 0x0a, 0x2a, 0x4f, 0x72, 0x67, 0x61, - 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, - 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x4d, 0x65, 0x6d, 0x62, - 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x22, 0x5b, 0x0a, 0x2a, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, - 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x2d, 0x0a, 0x0d, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, - 0x01, 0x52, 0x0c, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x49, 0x64, 0x22, - 0x2d, 0x0a, 0x2b, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, - 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9a, - 0x01, 0x0a, 0x2a, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, - 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, - 0x0d, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0c, - 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x04, - 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x69, 0x0a, 0x2b, 0x4f, - 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, - 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x72, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, - 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, - 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x3f, 0x0a, 0x20, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, - 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x55, 0x0a, 0x21, 0x4f, 0x72, 0x67, 0x61, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x06, - 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, - 0x72, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x9d, - 0x01, 0x0a, 0x20, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x3e, 0x0a, 0x19, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x5f, 0x76, 0x69, 0x6f, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x16, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4f, 0x6e, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x56, 0x69, 0x6f, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, - 0x42, 0x1c, 0x0a, 0x1a, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x76, 0x69, 0x6f, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x55, - 0x0a, 0x21, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, - 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0xa4, 0x05, 0x0a, 0x13, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6f, 0x0a, - 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, - 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, - 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, - 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x8a, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, - 0x69, 0x70, 0x73, 0x12, 0x3a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, - 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x74, 0x6f, 0x1a, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x27, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, + 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd2, 0x02, + 0x0a, 0x29, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, - 0x68, 0x69, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8d, 0x01, 0x0a, - 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, - 0x70, 0x12, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, + 0x68, 0x69, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0d, 0x6d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x0c, + 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, + 0x17, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, + 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x6f, + 0x6c, 0x65, 0x42, 0x0b, 0xba, 0x48, 0x08, 0x82, 0x01, 0x05, 0x10, 0x01, 0x22, 0x01, 0x00, 0x48, + 0x03, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x88, 0x01, 0x01, 0x12, 0x48, 0x0a, 0x0a, 0x70, 0x61, + 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8d, 0x01, 0x0a, - 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, - 0x70, 0x12, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, + 0x2e, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x68, 0x69, 0x70, 0x5f, 0x69, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, + 0x08, 0x0a, 0x06, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x72, 0x6f, + 0x6c, 0x65, 0x22, 0xb3, 0x01, 0x0a, 0x2a, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, + 0x70, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x49, 0x0a, + 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, + 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x5b, 0x0a, 0x2a, 0x4f, 0x72, 0x67, 0x61, + 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x0d, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x73, 0x68, 0x69, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0c, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x68, 0x69, 0x70, 0x49, 0x64, 0x22, 0x2d, 0x0a, 0x2b, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9a, 0x01, 0x0a, 0x2a, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x0d, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, + 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0c, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, + 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x6f, 0x6c, + 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x72, 0x6f, 0x6c, + 0x65, 0x22, 0x69, 0x0a, 0x2b, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3a, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, + 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x3f, 0x0a, 0x20, + 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x55, 0x0a, + 0x21, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x22, 0x9d, 0x01, 0x0a, 0x20, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x19, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, + 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x76, 0x69, 0x6f, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x16, 0x62, 0x6c, 0x6f, + 0x63, 0x6b, 0x4f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x56, 0x69, 0x6f, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x1c, 0x0a, 0x1a, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x5f, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x76, 0x69, 0x6f, 0x6c, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x55, 0x0a, 0x21, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x72, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x49, + 0x74, 0x65, 0x6d, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0xa4, 0x05, 0x0a, 0x13, + 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x6f, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x31, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x4c, 0x5a, 0x4a, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, - 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, - 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, - 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, - 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8a, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x73, 0x12, 0x3a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, + 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, + 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x8d, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x12, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x8d, 0x01, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x12, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -650,31 +729,36 @@ var file_controlplane_v1_organization_proto_goTypes = []interface{}{ (*OrganizationServiceCreateResponse)(nil), // 7: controlplane.v1.OrganizationServiceCreateResponse (*OrganizationServiceUpdateRequest)(nil), // 8: controlplane.v1.OrganizationServiceUpdateRequest (*OrganizationServiceUpdateResponse)(nil), // 9: controlplane.v1.OrganizationServiceUpdateResponse - (*OrgMembershipItem)(nil), // 10: controlplane.v1.OrgMembershipItem - (MembershipRole)(0), // 11: controlplane.v1.MembershipRole - (*OrgItem)(nil), // 12: controlplane.v1.OrgItem + (MembershipRole)(0), // 10: controlplane.v1.MembershipRole + (*OffsetPaginationRequest)(nil), // 11: controlplane.v1.OffsetPaginationRequest + (*OrgMembershipItem)(nil), // 12: controlplane.v1.OrgMembershipItem + (*OffsetPaginationResponse)(nil), // 13: controlplane.v1.OffsetPaginationResponse + (*OrgItem)(nil), // 14: controlplane.v1.OrgItem } var file_controlplane_v1_organization_proto_depIdxs = []int32{ - 10, // 0: controlplane.v1.OrganizationServiceListMembershipsResponse.result:type_name -> controlplane.v1.OrgMembershipItem - 11, // 1: controlplane.v1.OrganizationServiceUpdateMembershipRequest.role:type_name -> controlplane.v1.MembershipRole - 10, // 2: controlplane.v1.OrganizationServiceUpdateMembershipResponse.result:type_name -> controlplane.v1.OrgMembershipItem - 12, // 3: controlplane.v1.OrganizationServiceCreateResponse.result:type_name -> controlplane.v1.OrgItem - 12, // 4: controlplane.v1.OrganizationServiceUpdateResponse.result:type_name -> controlplane.v1.OrgItem - 6, // 5: controlplane.v1.OrganizationService.Create:input_type -> controlplane.v1.OrganizationServiceCreateRequest - 8, // 6: controlplane.v1.OrganizationService.Update:input_type -> controlplane.v1.OrganizationServiceUpdateRequest - 0, // 7: controlplane.v1.OrganizationService.ListMemberships:input_type -> controlplane.v1.OrganizationServiceListMembershipsRequest - 2, // 8: controlplane.v1.OrganizationService.DeleteMembership:input_type -> controlplane.v1.OrganizationServiceDeleteMembershipRequest - 4, // 9: controlplane.v1.OrganizationService.UpdateMembership:input_type -> controlplane.v1.OrganizationServiceUpdateMembershipRequest - 7, // 10: controlplane.v1.OrganizationService.Create:output_type -> controlplane.v1.OrganizationServiceCreateResponse - 9, // 11: controlplane.v1.OrganizationService.Update:output_type -> controlplane.v1.OrganizationServiceUpdateResponse - 1, // 12: controlplane.v1.OrganizationService.ListMemberships:output_type -> controlplane.v1.OrganizationServiceListMembershipsResponse - 3, // 13: controlplane.v1.OrganizationService.DeleteMembership:output_type -> controlplane.v1.OrganizationServiceDeleteMembershipResponse - 5, // 14: controlplane.v1.OrganizationService.UpdateMembership:output_type -> controlplane.v1.OrganizationServiceUpdateMembershipResponse - 10, // [10:15] is the sub-list for method output_type - 5, // [5:10] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 10, // 0: controlplane.v1.OrganizationServiceListMembershipsRequest.role:type_name -> controlplane.v1.MembershipRole + 11, // 1: controlplane.v1.OrganizationServiceListMembershipsRequest.pagination:type_name -> controlplane.v1.OffsetPaginationRequest + 12, // 2: controlplane.v1.OrganizationServiceListMembershipsResponse.result:type_name -> controlplane.v1.OrgMembershipItem + 13, // 3: controlplane.v1.OrganizationServiceListMembershipsResponse.pagination:type_name -> controlplane.v1.OffsetPaginationResponse + 10, // 4: controlplane.v1.OrganizationServiceUpdateMembershipRequest.role:type_name -> controlplane.v1.MembershipRole + 12, // 5: controlplane.v1.OrganizationServiceUpdateMembershipResponse.result:type_name -> controlplane.v1.OrgMembershipItem + 14, // 6: controlplane.v1.OrganizationServiceCreateResponse.result:type_name -> controlplane.v1.OrgItem + 14, // 7: controlplane.v1.OrganizationServiceUpdateResponse.result:type_name -> controlplane.v1.OrgItem + 6, // 8: controlplane.v1.OrganizationService.Create:input_type -> controlplane.v1.OrganizationServiceCreateRequest + 8, // 9: controlplane.v1.OrganizationService.Update:input_type -> controlplane.v1.OrganizationServiceUpdateRequest + 0, // 10: controlplane.v1.OrganizationService.ListMemberships:input_type -> controlplane.v1.OrganizationServiceListMembershipsRequest + 2, // 11: controlplane.v1.OrganizationService.DeleteMembership:input_type -> controlplane.v1.OrganizationServiceDeleteMembershipRequest + 4, // 12: controlplane.v1.OrganizationService.UpdateMembership:input_type -> controlplane.v1.OrganizationServiceUpdateMembershipRequest + 7, // 13: controlplane.v1.OrganizationService.Create:output_type -> controlplane.v1.OrganizationServiceCreateResponse + 9, // 14: controlplane.v1.OrganizationService.Update:output_type -> controlplane.v1.OrganizationServiceUpdateResponse + 1, // 15: controlplane.v1.OrganizationService.ListMemberships:output_type -> controlplane.v1.OrganizationServiceListMembershipsResponse + 3, // 16: controlplane.v1.OrganizationService.DeleteMembership:output_type -> controlplane.v1.OrganizationServiceDeleteMembershipResponse + 5, // 17: controlplane.v1.OrganizationService.UpdateMembership:output_type -> controlplane.v1.OrganizationServiceUpdateMembershipResponse + 13, // [13:18] is the sub-list for method output_type + 8, // [8:13] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_controlplane_v1_organization_proto_init() } @@ -682,6 +766,7 @@ func file_controlplane_v1_organization_proto_init() { if File_controlplane_v1_organization_proto != nil { return } + file_controlplane_v1_pagination_proto_init() file_controlplane_v1_response_messages_proto_init() if !protoimpl.UnsafeEnabled { file_controlplane_v1_organization_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { @@ -805,6 +890,7 @@ func file_controlplane_v1_organization_proto_init() { } } } + file_controlplane_v1_organization_proto_msgTypes[0].OneofWrappers = []interface{}{} file_controlplane_v1_organization_proto_msgTypes[8].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ diff --git a/app/controlplane/api/controlplane/v1/organization.proto b/app/controlplane/api/controlplane/v1/organization.proto index f082799b2..8ff7c53f9 100644 --- a/app/controlplane/api/controlplane/v1/organization.proto +++ b/app/controlplane/api/controlplane/v1/organization.proto @@ -18,6 +18,7 @@ syntax = "proto3"; package controlplane.v1; import "buf/validate/validate.proto"; +import "controlplane/v1/pagination.proto"; import "controlplane/v1/response_messages.proto"; option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1"; @@ -35,10 +36,25 @@ service OrganizationService { rpc UpdateMembership(OrganizationServiceUpdateMembershipRequest) returns (OrganizationServiceUpdateMembershipResponse); } -message OrganizationServiceListMembershipsRequest {} +message OrganizationServiceListMembershipsRequest { + optional string membership_id = 1 [(buf.validate.field).string.uuid = true]; + // Optional filter by user name + optional string name = 2; + // Optional filter to search by user email address + optional string email = 3; + // Optional filter by role + optional MembershipRole role = 4 [(buf.validate.field).enum = { + defined_only: true + not_in: [0] + }]; + // Pagination parameters to limit and offset results + OffsetPaginationRequest pagination = 5; +} message OrganizationServiceListMembershipsResponse { repeated OrgMembershipItem result = 1; + // Pagination information for the response + OffsetPaginationResponse pagination = 2; } message OrganizationServiceDeleteMembershipRequest { diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/organization.ts b/app/controlplane/api/gen/frontend/controlplane/v1/organization.ts index ef0c5622c..1dc40210f 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/organization.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/organization.ts @@ -2,6 +2,7 @@ import { grpc } from "@improbable-eng/grpc-web"; import { BrowserHeaders } from "browser-headers"; import _m0 from "protobufjs/minimal"; +import { OffsetPaginationRequest, OffsetPaginationResponse } from "./pagination"; import { MembershipRole, membershipRoleFromJSON, @@ -13,10 +14,29 @@ import { export const protobufPackage = "controlplane.v1"; export interface OrganizationServiceListMembershipsRequest { + membershipId?: + | string + | undefined; + /** Optional filter by user name */ + name?: + | string + | undefined; + /** Optional filter to search by user email address */ + email?: + | string + | undefined; + /** Optional filter by role */ + role?: + | MembershipRole + | undefined; + /** Pagination parameters to limit and offset results */ + pagination?: OffsetPaginationRequest; } export interface OrganizationServiceListMembershipsResponse { result: OrgMembershipItem[]; + /** Pagination information for the response */ + pagination?: OffsetPaginationResponse; } export interface OrganizationServiceDeleteMembershipRequest { @@ -54,11 +74,26 @@ export interface OrganizationServiceUpdateResponse { } function createBaseOrganizationServiceListMembershipsRequest(): OrganizationServiceListMembershipsRequest { - return {}; + return { membershipId: undefined, name: undefined, email: undefined, role: undefined, pagination: undefined }; } export const OrganizationServiceListMembershipsRequest = { - encode(_: OrganizationServiceListMembershipsRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode(message: OrganizationServiceListMembershipsRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.membershipId !== undefined) { + writer.uint32(10).string(message.membershipId); + } + if (message.name !== undefined) { + writer.uint32(18).string(message.name); + } + if (message.email !== undefined) { + writer.uint32(26).string(message.email); + } + if (message.role !== undefined) { + writer.uint32(32).int32(message.role); + } + if (message.pagination !== undefined) { + OffsetPaginationRequest.encode(message.pagination, writer.uint32(42).fork()).ldelim(); + } return writer; }, @@ -69,6 +104,41 @@ export const OrganizationServiceListMembershipsRequest = { while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.membershipId = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.name = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.email = reader.string(); + continue; + case 4: + if (tag !== 32) { + break; + } + + message.role = reader.int32() as any; + continue; + case 5: + if (tag !== 42) { + break; + } + + message.pagination = OffsetPaginationRequest.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -78,12 +148,25 @@ export const OrganizationServiceListMembershipsRequest = { return message; }, - fromJSON(_: any): OrganizationServiceListMembershipsRequest { - return {}; + fromJSON(object: any): OrganizationServiceListMembershipsRequest { + return { + membershipId: isSet(object.membershipId) ? String(object.membershipId) : undefined, + name: isSet(object.name) ? String(object.name) : undefined, + email: isSet(object.email) ? String(object.email) : undefined, + role: isSet(object.role) ? membershipRoleFromJSON(object.role) : undefined, + pagination: isSet(object.pagination) ? OffsetPaginationRequest.fromJSON(object.pagination) : undefined, + }; }, - toJSON(_: OrganizationServiceListMembershipsRequest): unknown { + toJSON(message: OrganizationServiceListMembershipsRequest): unknown { const obj: any = {}; + message.membershipId !== undefined && (obj.membershipId = message.membershipId); + message.name !== undefined && (obj.name = message.name); + message.email !== undefined && (obj.email = message.email); + message.role !== undefined && + (obj.role = message.role !== undefined ? membershipRoleToJSON(message.role) : undefined); + message.pagination !== undefined && + (obj.pagination = message.pagination ? OffsetPaginationRequest.toJSON(message.pagination) : undefined); return obj; }, @@ -94,15 +177,22 @@ export const OrganizationServiceListMembershipsRequest = { }, fromPartial, I>>( - _: I, + object: I, ): OrganizationServiceListMembershipsRequest { const message = createBaseOrganizationServiceListMembershipsRequest(); + message.membershipId = object.membershipId ?? undefined; + message.name = object.name ?? undefined; + message.email = object.email ?? undefined; + message.role = object.role ?? undefined; + message.pagination = (object.pagination !== undefined && object.pagination !== null) + ? OffsetPaginationRequest.fromPartial(object.pagination) + : undefined; return message; }, }; function createBaseOrganizationServiceListMembershipsResponse(): OrganizationServiceListMembershipsResponse { - return { result: [] }; + return { result: [], pagination: undefined }; } export const OrganizationServiceListMembershipsResponse = { @@ -110,6 +200,9 @@ export const OrganizationServiceListMembershipsResponse = { for (const v of message.result) { OrgMembershipItem.encode(v!, writer.uint32(10).fork()).ldelim(); } + if (message.pagination !== undefined) { + OffsetPaginationResponse.encode(message.pagination, writer.uint32(18).fork()).ldelim(); + } return writer; }, @@ -127,6 +220,13 @@ export const OrganizationServiceListMembershipsResponse = { message.result.push(OrgMembershipItem.decode(reader, reader.uint32())); continue; + case 2: + if (tag !== 18) { + break; + } + + message.pagination = OffsetPaginationResponse.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -139,6 +239,7 @@ export const OrganizationServiceListMembershipsResponse = { fromJSON(object: any): OrganizationServiceListMembershipsResponse { return { result: Array.isArray(object?.result) ? object.result.map((e: any) => OrgMembershipItem.fromJSON(e)) : [], + pagination: isSet(object.pagination) ? OffsetPaginationResponse.fromJSON(object.pagination) : undefined, }; }, @@ -149,6 +250,8 @@ export const OrganizationServiceListMembershipsResponse = { } else { obj.result = []; } + message.pagination !== undefined && + (obj.pagination = message.pagination ? OffsetPaginationResponse.toJSON(message.pagination) : undefined); return obj; }, @@ -163,6 +266,9 @@ export const OrganizationServiceListMembershipsResponse = { ): OrganizationServiceListMembershipsResponse { const message = createBaseOrganizationServiceListMembershipsResponse(); message.result = object.result?.map((e) => OrgMembershipItem.fromPartial(e)) || []; + message.pagination = (object.pagination !== undefined && object.pagination !== null) + ? OffsetPaginationResponse.fromPartial(object.pagination) + : undefined; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json index e103fe75d..251e3f0bf 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json @@ -2,7 +2,51 @@ "$id": "controlplane.v1.OrganizationServiceListMembershipsRequest.jsonschema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "properties": {}, + "patternProperties": { + "^(membership_id)$": { + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "type": "string" + } + }, + "properties": { + "email": { + "description": "Optional filter to search by user email address", + "type": "string" + }, + "membershipId": { + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "type": "string" + }, + "name": { + "description": "Optional filter by user name", + "type": "string" + }, + "pagination": { + "$ref": "controlplane.v1.OffsetPaginationRequest.jsonschema.json", + "description": "Pagination parameters to limit and offset results" + }, + "role": { + "anyOf": [ + { + "enum": [ + "MEMBERSHIP_ROLE_UNSPECIFIED", + "MEMBERSHIP_ROLE_ORG_VIEWER", + "MEMBERSHIP_ROLE_ORG_ADMIN", + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" + ], + "title": "Membership Role", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Optional filter by role" + } + }, "title": "Organization Service List Memberships Request", "type": "object" } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json index e3f2f03e2..85d0689d8 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json @@ -2,7 +2,51 @@ "$id": "controlplane.v1.OrganizationServiceListMembershipsRequest.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "properties": {}, + "patternProperties": { + "^(membershipId)$": { + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "type": "string" + } + }, + "properties": { + "email": { + "description": "Optional filter to search by user email address", + "type": "string" + }, + "membership_id": { + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "type": "string" + }, + "name": { + "description": "Optional filter by user name", + "type": "string" + }, + "pagination": { + "$ref": "controlplane.v1.OffsetPaginationRequest.schema.json", + "description": "Pagination parameters to limit and offset results" + }, + "role": { + "anyOf": [ + { + "enum": [ + "MEMBERSHIP_ROLE_UNSPECIFIED", + "MEMBERSHIP_ROLE_ORG_VIEWER", + "MEMBERSHIP_ROLE_ORG_ADMIN", + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" + ], + "title": "Membership Role", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Optional filter by role" + } + }, "title": "Organization Service List Memberships Request", "type": "object" } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsResponse.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsResponse.jsonschema.json index 7bef34948..d6a2c21c1 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsResponse.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsResponse.jsonschema.json @@ -3,6 +3,10 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { + "pagination": { + "$ref": "controlplane.v1.OffsetPaginationResponse.jsonschema.json", + "description": "Pagination information for the response" + }, "result": { "items": { "$ref": "controlplane.v1.OrgMembershipItem.jsonschema.json" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsResponse.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsResponse.schema.json index d0c3bc39b..fdd4a92d7 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsResponse.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceListMembershipsResponse.schema.json @@ -3,6 +3,10 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "properties": { + "pagination": { + "$ref": "controlplane.v1.OffsetPaginationResponse.schema.json", + "description": "Pagination information for the response" + }, "result": { "items": { "$ref": "controlplane.v1.OrgMembershipItem.schema.json" diff --git a/app/controlplane/internal/service/organization.go b/app/controlplane/internal/service/organization.go index 37d5a3ff2..954005181 100644 --- a/app/controlplane/internal/service/organization.go +++ b/app/controlplane/internal/service/organization.go @@ -21,6 +21,8 @@ import ( pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" + + "github.com/google/uuid" ) type OrganizationService struct { @@ -73,13 +75,38 @@ func (s *OrganizationService) Update(ctx context.Context, req *pb.OrganizationSe return &pb.OrganizationServiceUpdateResponse{Result: bizOrgToPb(org)}, nil } -func (s *OrganizationService) ListMemberships(ctx context.Context, _ *pb.OrganizationServiceListMembershipsRequest) (*pb.OrganizationServiceListMembershipsResponse, error) { +func (s *OrganizationService) ListMemberships(ctx context.Context, req *pb.OrganizationServiceListMembershipsRequest) (*pb.OrganizationServiceListMembershipsResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } - memberships, err := s.membershipUC.ByOrg(ctx, currentOrg.ID) + // Initialize the pagination options, with default values + paginationOpts, err := initializePaginationOpts(req.GetPagination()) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + opts := &biz.ListByOrgOpts{ + Name: req.Name, + Email: req.Email, + } + + if req.MembershipId != nil { + membershipUUID, err := uuid.Parse(req.GetMembershipId()) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + opts.MembershipID = &membershipUUID + } + + if req.Role != nil { + castedRole := biz.PbRoleToBiz(req.GetRole()) + opts.Role = &castedRole + } + + memberships, count, err := s.membershipUC.ByOrg(ctx, currentOrg.ID, opts, paginationOpts) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -89,7 +116,10 @@ func (s *OrganizationService) ListMemberships(ctx context.Context, _ *pb.Organiz result = append(result, bizMembershipToPb(m)) } - return &pb.OrganizationServiceListMembershipsResponse{Result: result}, nil + return &pb.OrganizationServiceListMembershipsResponse{ + Result: result, + Pagination: paginationToPb(count, paginationOpts.Offset(), paginationOpts.Limit()), + }, nil } func (s *OrganizationService) DeleteMembership(ctx context.Context, req *pb.OrganizationServiceDeleteMembershipRequest) (*pb.OrganizationServiceDeleteMembershipResponse, error) { diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 539a2103c..3a4fa5531 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -22,6 +22,8 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" + "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" ) @@ -40,11 +42,23 @@ type Membership struct { ResourceID uuid.UUID } +// ListByOrgOpts are the options to filter memberships of an organization +type ListByOrgOpts struct { + // MembershipID the ID of the membership to filter by + MembershipID *uuid.UUID + // Name the name of the user to filter memberships by + Name *string + // Email the email of the user to filter memberships by + Email *string + // Role the role of the user to filter memberships by + Role *authz.Role +} + type MembershipRepo interface { FindByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) FindByOrgIDAndUserEmail(ctx context.Context, orgID uuid.UUID, userEmail string) (*Membership, error) FindByUserAndResourceID(ctx context.Context, userID, resourceID uuid.UUID) (*Membership, error) - FindByOrg(ctx context.Context, orgID uuid.UUID) ([]*Membership, error) + FindByOrg(ctx context.Context, orgID uuid.UUID, opts *ListByOrgOpts, paginationOpts *pagination.OffsetPaginationOpts) ([]*Membership, int, error) FindByIDInUser(ctx context.Context, userID, ID uuid.UUID) (*Membership, error) FindByIDInOrg(ctx context.Context, orgID, ID uuid.UUID) (*Membership, error) FindByOrgAndUser(ctx context.Context, orgID, userID uuid.UUID) (*Membership, error) @@ -113,12 +127,12 @@ func (uc *MembershipUseCase) LeaveAndDeleteOrg(ctx context.Context, userID, memb // Check number of members in the org // If it's the only one, delete the org - membershipsInOrg, err := uc.repo.FindByOrg(ctx, m.OrganizationID) + _, membershipCount, err := uc.repo.FindByOrg(ctx, m.OrganizationID, &ListByOrgOpts{}, pagination.NewDefaultOffsetPaginationOpts()) if err != nil { return fmt.Errorf("failed to find memberships in org: %w", err) } - if len(membershipsInOrg) == 0 { + if membershipCount == 0 { // Delete the org uc.logger.Infow("msg", "Deleting organization", "organization_id", m.OrganizationID.String()) if err := uc.orgUseCase.Delete(ctx, m.OrganizationID.String()); err != nil { @@ -255,13 +269,22 @@ func (uc *MembershipUseCase) ByUser(ctx context.Context, userID string) ([]*Memb return uc.repo.FindByUser(ctx, userUUID) } -func (uc *MembershipUseCase) ByOrg(ctx context.Context, orgID string) ([]*Membership, error) { +func (uc *MembershipUseCase) ByOrg(ctx context.Context, orgID string, opts *ListByOrgOpts, paginationOpts *pagination.OffsetPaginationOpts) ([]*Membership, int, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { - return nil, NewErrInvalidUUID(err) + return nil, 0, NewErrInvalidUUID(err) + } + + if opts == nil { + opts = &ListByOrgOpts{} + } + + pgOpts := paginationOpts + if pgOpts == nil { + pgOpts = pagination.NewDefaultOffsetPaginationOpts() } - return uc.repo.FindByOrg(ctx, orgUUID) + return uc.repo.FindByOrg(ctx, orgUUID, opts, pgOpts) } // SetCurrent sets the current membership for the user diff --git a/app/controlplane/pkg/biz/membership_integration_test.go b/app/controlplane/pkg/biz/membership_integration_test.go index 3ec61a4d5..9f78ff51e 100644 --- a/app/controlplane/pkg/biz/membership_integration_test.go +++ b/app/controlplane/pkg/biz/membership_integration_test.go @@ -17,11 +17,13 @@ package biz_test import ( "context" + "fmt" "testing" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/testhelpers" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -47,24 +49,27 @@ func (s *membershipIntegrationTestSuite) TestByOrg() { s.NoError(err) s.Run("org 1", func() { - memberships, err := s.Membership.ByOrg(ctx, userOrg.ID) + memberships, count, err := s.Membership.ByOrg(ctx, userOrg.ID, &biz.ListByOrgOpts{}, nil) s.NoError(err) s.Len(memberships, 1) + s.Equal(1, count) s.Equal(memberships[0].OrganizationID.String(), userOrg.ID) s.Equal(memberships[0].User.Email, user.Email) s.Equal(memberships[0].Role, authz.RoleViewer) }) s.Run("shared org", func() { - memberships, err := s.Membership.ByOrg(ctx, sharedOrg.ID) + memberships, count, err := s.Membership.ByOrg(ctx, sharedOrg.ID, &biz.ListByOrgOpts{}, nil) s.NoError(err) s.Len(memberships, 2) + s.Equal(2, count) }) s.T().Run("non existing org", func(t *testing.T) { - memberships, err := s.Membership.ByOrg(ctx, uuid.NewString()) + memberships, count, err := s.Membership.ByOrg(ctx, uuid.NewString(), &biz.ListByOrgOpts{}, nil) s.NoError(err) s.Len(memberships, 0) + s.Equal(0, count) }) } @@ -118,9 +123,10 @@ func (s *membershipIntegrationTestSuite) TestDeleteWithOrg() { s.NoError(err) // User 2 is still a member - members, err := s.Membership.ByOrg(ctx, got.ID) + members, count, err := s.Membership.ByOrg(ctx, got.ID, &biz.ListByOrgOpts{}, nil) s.NoError(err) s.Len(members, 1) + s.Equal(1, count) s.Equal(user2.ID, members[0].User.ID) }) @@ -158,16 +164,18 @@ func (s *membershipIntegrationTestSuite) TestDeleteOther() { }) s.T().Run("I can delete other user membership", func(t *testing.T) { - memberships, err := s.Membership.ByOrg(ctx, sharedOrg.ID) + memberships, count, err := s.Membership.ByOrg(ctx, sharedOrg.ID, &biz.ListByOrgOpts{}, nil) s.NoError(err) s.Len(memberships, 1) + s.Equal(1, count) err = s.Membership.DeleteOther(ctx, sharedOrg.ID, user.ID, mUser2SharedOrg.ID.String()) s.NoError(err) - memberships, err = s.Membership.ByOrg(ctx, sharedOrg.ID) + memberships, count, err = s.Membership.ByOrg(ctx, sharedOrg.ID, &biz.ListByOrgOpts{}, nil) s.NoError(err) s.Len(memberships, 0) + s.Equal(0, count) }) } @@ -202,9 +210,10 @@ func (s *membershipIntegrationTestSuite) TestUpdateRole() { }) s.T().Run("I can update other roles", func(t *testing.T) { - memberships, err := s.Membership.ByOrg(ctx, sharedOrg.ID) + memberships, count, err := s.Membership.ByOrg(ctx, sharedOrg.ID, &biz.ListByOrgOpts{}, nil) s.NoError(err) s.Len(memberships, 1) + s.Equal(1, count) s.Equal(authz.RoleViewer, memberships[0].Role) got, err := s.Membership.UpdateRole(ctx, sharedOrg.ID, user.ID, mUser2SharedOrg.ID.String(), authz.RoleAdmin) @@ -489,9 +498,342 @@ func (s *membershipIntegrationTestSuite) TestDeleteWithGroups() { // Run the tests func TestMembershipUseCase(t *testing.T) { suite.Run(t, new(membershipIntegrationTestSuite)) + suite.Run(t, new(membershipFilteringPaginationTestSuite)) } // Utility struct to hold the test suite type membershipIntegrationTestSuite struct { testhelpers.UseCasesEachTestSuite } + +type membershipFilteringPaginationTestSuite struct { + testhelpers.UseCasesEachTestSuite + org *biz.Organization + user *biz.User +} + +func (s *membershipFilteringPaginationTestSuite) SetupTest() { + var err error + assert := assert.New(s.T()) + s.TestingUseCases = testhelpers.NewTestingUseCases(s.T()) + + ctx := context.Background() + s.org, err = s.Organization.CreateWithRandomName(ctx) + assert.NoError(err) + + // Create a user for membership tests + s.user, err = s.User.UpsertByEmail(ctx, fmt.Sprintf("test-user-%s@example.com", uuid.New().String()), nil) + assert.NoError(err) + + // Add user to organization + _, err = s.Membership.Create(ctx, s.org.ID, s.user.ID) + assert.NoError(err) +} + +// Test comprehensive filtering and pagination functionality +func (s *membershipFilteringPaginationTestSuite) TestByOrgWithFiltersAndPagination() { + ctx := context.Background() + + // Create an organization + org, err := s.Organization.CreateWithRandomName(ctx) + s.NoError(err) + + // Create users with different names and emails for filtering tests + johnSmith, err := s.User.UpsertByEmail(ctx, "john.smith@example.com", &biz.UpsertByEmailOpts{ + FirstName: toPtrS("John"), + LastName: toPtrS("Smith"), + }) + s.NoError(err) + + janeSmith, err := s.User.UpsertByEmail(ctx, "jane.smith@example.com", &biz.UpsertByEmailOpts{ + FirstName: toPtrS("Jane"), + LastName: toPtrS("Smith"), + }) + s.NoError(err) + + bobJohnson, err := s.User.UpsertByEmail(ctx, "bob.johnson@company.com", &biz.UpsertByEmailOpts{ + FirstName: toPtrS("Bob"), + LastName: toPtrS("Johnson"), + }) + s.NoError(err) + + aliceWilson, err := s.User.UpsertByEmail(ctx, "alice.wilson@test.org", &biz.UpsertByEmailOpts{ + FirstName: toPtrS("Alice"), + LastName: toPtrS("Wilson"), + }) + s.NoError(err) + + // Add all users to the organization + _, err = s.Membership.Create(ctx, org.ID, johnSmith.ID) + s.NoError(err) + _, err = s.Membership.Create(ctx, org.ID, janeSmith.ID) + s.NoError(err) + _, err = s.Membership.Create(ctx, org.ID, bobJohnson.ID) + s.NoError(err) + _, err = s.Membership.Create(ctx, org.ID, aliceWilson.ID) + s.NoError(err) + + s.Run("filter by first name", func() { + nameFilter := "Bob" + opts := &biz.ListByOrgOpts{Name: &nameFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(1, count) + s.Len(memberships, 1) + s.Equal("Bob", memberships[0].User.FirstName) + s.Equal("bob.johnson@company.com", memberships[0].User.Email) + }) + + s.Run("filter by last name", func() { + nameFilter := "Smith" + opts := &biz.ListByOrgOpts{Name: &nameFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(2, count) + s.Len(memberships, 2) + + // Should find both John Smith and Jane Smith + emails := []string{memberships[0].User.Email, memberships[1].User.Email} + s.Contains(emails, "john.smith@example.com") + s.Contains(emails, "jane.smith@example.com") + }) + + s.Run("filter by partial name", func() { + nameFilter := "ob" // Should match "Bob" + opts := &biz.ListByOrgOpts{Name: &nameFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(1, count) + s.Len(memberships, 1) + s.Equal("bob.johnson@company.com", memberships[0].User.Email) + }) + + s.Run("filter by email domain", func() { + emailFilter := "@example.com" + opts := &biz.ListByOrgOpts{Email: &emailFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(2, count) + s.Len(memberships, 2) + + // Should find both John Smith and Jane Smith + emails := []string{memberships[0].User.Email, memberships[1].User.Email} + s.Contains(emails, "john.smith@example.com") + s.Contains(emails, "jane.smith@example.com") + }) + + s.Run("filter by specific email", func() { + emailFilter := "bob.johnson" + opts := &biz.ListByOrgOpts{Email: &emailFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(1, count) + s.Len(memberships, 1) + s.Equal("bob.johnson@company.com", memberships[0].User.Email) + }) + + s.Run("filter with no matches", func() { + nameFilter := "NonExistentName" + opts := &biz.ListByOrgOpts{Name: &nameFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(0, count) + s.Len(memberships, 0) + }) + + s.Run("pagination with limit", func() { + opts := &biz.ListByOrgOpts{} + paginationOpts, err := pagination.NewOffsetPaginationOpts(0, 2) + s.NoError(err) + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, paginationOpts) + s.NoError(err) + s.Equal(4, count) // Total count should be 4 + s.Len(memberships, 2) // But only 2 results returned due to limit + }) + + s.Run("pagination with offset", func() { + opts := &biz.ListByOrgOpts{} + paginationOpts, err := pagination.NewOffsetPaginationOpts(2, 2) + s.NoError(err) + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, paginationOpts) + s.NoError(err) + s.Equal(4, count) // Total count should still be 4 + s.Len(memberships, 2) // Should return remaining 2 results + }) + + s.Run("pagination beyond available results", func() { + opts := &biz.ListByOrgOpts{} + paginationOpts, err := pagination.NewOffsetPaginationOpts(10, 5) + s.NoError(err) + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, paginationOpts) + s.NoError(err) + s.Equal(4, count) // Total count should still be 4 + s.Len(memberships, 0) // No results due to high offset + }) + + s.Run("pagination with filtering", func() { + nameFilter := "Smith" + opts := &biz.ListByOrgOpts{Name: &nameFilter} + paginationOpts, err := pagination.NewOffsetPaginationOpts(0, 1) + s.NoError(err) + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, paginationOpts) + s.NoError(err) + s.Equal(2, count) // Total filtered count should be 2 (both Smiths) + s.Len(memberships, 1) // But only 1 result returned due to limit + s.Contains(memberships[0].User.Email, "smith@example.com") + }) + + s.Run("empty filters should return all results", func() { + emptyName := "" + emptyEmail := "" + opts := &biz.ListByOrgOpts{ + Name: &emptyName, + Email: &emptyEmail, + } + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(4, count) + s.Len(memberships, 4) + }) + + s.Run("nil filter options should return all results", func() { + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, nil, nil) + s.NoError(err) + s.Equal(4, count) + s.Len(memberships, 4) + }) + + s.Run("case insensitive filtering", func() { + nameFilter := "SMITH" // uppercase + opts := &biz.ListByOrgOpts{Name: &nameFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(2, count) // Should still find both Smiths + s.Len(memberships, 2) + }) +} + +// Test that verifies the ordering of results +func (s *membershipFilteringPaginationTestSuite) TestByOrgOrdering() { + ctx := context.Background() + + // Create an organization + org, err := s.Organization.CreateWithRandomName(ctx) + s.NoError(err) + + // Create users and add them with some delay to test ordering + user1, err := s.User.UpsertByEmail(ctx, "first@example.com", nil) + s.NoError(err) + _, err = s.Membership.Create(ctx, org.ID, user1.ID) + s.NoError(err) + + // Small delay to ensure different creation times + user2, err := s.User.UpsertByEmail(ctx, "second@example.com", nil) + s.NoError(err) + _, err = s.Membership.Create(ctx, org.ID, user2.ID) + s.NoError(err) + + user3, err := s.User.UpsertByEmail(ctx, "third@example.com", nil) + s.NoError(err) + _, err = s.Membership.Create(ctx, org.ID, user3.ID) + s.NoError(err) + + s.Run("results should be ordered by creation date descending", func() { + opts := &biz.ListByOrgOpts{} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(3, count) + s.Len(memberships, 3) + + // Most recent should be first + s.Equal("third@example.com", memberships[0].User.Email) + s.Equal("second@example.com", memberships[1].User.Email) + s.Equal("first@example.com", memberships[2].User.Email) + + // Verify timestamps are in descending order + s.True(memberships[0].CreatedAt.After(*memberships[1].CreatedAt)) + s.True(memberships[1].CreatedAt.After(*memberships[2].CreatedAt)) + }) +} + +// Test edge cases and error scenarios +func (s *membershipFilteringPaginationTestSuite) TestByOrgEdgeCases() { + ctx := context.Background() + + // Create an organization + org, err := s.Organization.CreateWithRandomName(ctx) + s.NoError(err) + + s.Run("organization with no members", func() { + opts := &biz.ListByOrgOpts{} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(0, count) + s.Len(memberships, 0) + }) + + s.Run("non-existent organization", func() { + nonExistentOrgID := uuid.NewString() + opts := &biz.ListByOrgOpts{} + + memberships, count, err := s.Membership.ByOrg(ctx, nonExistentOrgID, opts, nil) + s.NoError(err) + s.Equal(0, count) + s.Len(memberships, 0) + }) + + s.Run("invalid UUID", func() { + invalidOrgID := "invalid-uuid" + opts := &biz.ListByOrgOpts{} + + _, _, err := s.Membership.ByOrg(ctx, invalidOrgID, opts, nil) + s.Error(err) + s.True(biz.IsErrInvalidUUID(err)) + }) + + // Add a user to test special characters in names/emails + user, err := s.User.UpsertByEmail(ctx, "test.user+special@example.com", &biz.UpsertByEmailOpts{ + FirstName: toPtrS("Test-User"), + LastName: toPtrS("O'Connor"), + }) + s.NoError(err) + _, err = s.Membership.Create(ctx, org.ID, user.ID) + s.NoError(err) + + s.Run("special characters in filters", func() { + nameFilter := "O'Connor" + opts := &biz.ListByOrgOpts{Name: &nameFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(1, count) + s.Len(memberships, 1) + s.Equal("Test-User", memberships[0].User.FirstName) + s.Equal("O'Connor", memberships[0].User.LastName) + }) + + s.Run("special characters in email filter", func() { + emailFilter := "test.user+special" + opts := &biz.ListByOrgOpts{Email: &emailFilter} + + memberships, count, err := s.Membership.ByOrg(ctx, org.ID, opts, nil) + s.NoError(err) + s.Equal(1, count) + s.Len(memberships, 1) + s.Equal("test.user+special@example.com", memberships[0].User.Email) + }) +} diff --git a/app/controlplane/pkg/biz/orginvitation.go b/app/controlplane/pkg/biz/orginvitation.go index 153eb935e..8d430e884 100644 --- a/app/controlplane/pkg/biz/orginvitation.go +++ b/app/controlplane/pkg/biz/orginvitation.go @@ -24,6 +24,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/chainloop-dev/chainloop/pkg/servicelogger" "github.com/go-kratos/kratos/v2/log" @@ -157,15 +158,15 @@ func (uc *OrgInvitationUseCase) Create(ctx context.Context, orgID, senderID, rec } // 4 - The receiver does exist in the org already - memberships, err := uc.mRepo.FindByOrg(ctx, orgUUID) + _, membershipCount, err := uc.mRepo.FindByOrg(ctx, orgUUID, &ListByOrgOpts{ + Email: &receiverEmail, + }, pagination.NewDefaultOffsetPaginationOpts()) if err != nil { return nil, fmt.Errorf("error finding memberships for user %s: %w", senderUUID.String(), err) } - for _, m := range memberships { - if m.User != nil && m.User.Email == receiverEmail { - return nil, NewErrValidationStr("user already exists in the org") - } + if membershipCount > 0 { + return nil, NewErrValidationStr("user already exists in the org") } // 5 - Check if there is already an invitation for this user for this org diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index 27407457e..61308a9c4 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -27,7 +27,9 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/groupmembership" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/membership" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/user" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" @@ -89,17 +91,99 @@ func (r *MembershipRepo) FindByUser(ctx context.Context, userID uuid.UUID) ([]*b } // FindByOrg finds all memberships for a given organization -func (r *MembershipRepo) FindByOrg(ctx context.Context, orgID uuid.UUID) ([]*biz.Membership, error) { - memberships, err := r.data.DB.Membership.Query().Where( +func (r *MembershipRepo) FindByOrg(ctx context.Context, orgID uuid.UUID, opts *biz.ListByOrgOpts, paginationOpts *pagination.OffsetPaginationOpts) ([]*biz.Membership, int, error) { + if paginationOpts == nil { + paginationOpts = pagination.NewDefaultOffsetPaginationOpts() + } + + if opts == nil { + opts = &biz.ListByOrgOpts{} + } + + // Build the base query + query := r.data.DB.Membership.Query().Where( membership.MembershipTypeEQ(authz.MembershipTypeUser), membership.ResourceTypeEQ(authz.ResourceTypeOrganization), membership.ResourceIDEQ(orgID), - ).WithUser().WithOrganization().All(ctx) + ).WithUser().WithOrganization() + + // Apply filters if provided + var predicates []predicate.Membership + if opts.Name != nil && *opts.Name != "" { + // Filter by user's first name or last name containing the search term + predicates = append(predicates, membership.HasUserWith( + user.Or( + user.FirstNameContainsFold(*opts.Name), + user.LastNameContainsFold(*opts.Name), + ), + )) + } + + // Filter by user's email containing the search term + if opts.Email != nil && *opts.Email != "" { + predicates = append(predicates, membership.HasUserWith(user.EmailContainsFold(*opts.Email))) + } + + // Apply OR predicates if any exist + if len(predicates) > 0 { + query = query.Where(membership.Or(predicates...)) + } + + // Filter by the membership ID if provided + if opts.MembershipID != nil { + query = query.Where(membership.IDEQ(*opts.MembershipID)) + } + + // Filter by role if provided + if opts.Role != nil { + query = query.Where(membership.RoleEQ(*opts.Role)) + } + + // Get the count of all filtered rows without the limit and offset + count, err := query.Count(ctx) if err != nil { - return nil, err + return nil, 0, err } - return entMembershipsToBiz(memberships), nil + // Apply pagination and execute the query + memberships, err := query. + Order(ent.Desc(membership.FieldCreatedAt)). + Limit(paginationOpts.Limit()). + Offset(paginationOpts.Offset()). + All(ctx) + if err != nil { + return nil, 0, err + } + + // Fetch all member IDs from the memberships, in this context they are user IDs + memberIDs := make([]uuid.UUID, 0, len(memberships)) + for _, m := range memberships { + memberIDs = append(memberIDs, m.MemberID) + } + + // Fetch user data for all the member IDs + users, err := r.data.DB.User.Query().Where(user.IDIn(memberIDs...)).All(ctx) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch user data: %w", err) + } + + // Create a map of users by ID + userMap := make(map[uuid.UUID]*ent.User, len(users)) + for _, u := range users { + userMap[u.ID] = u + } + + // Convert to biz.Membership objects and attach user data manually + result := make([]*biz.Membership, 0, len(memberships)) + for _, m := range memberships { + bizMembership := entMembershipToBiz(m) + if u, ok := userMap[m.MemberID]; ok { + bizMembership.User = entUserToBizUser(u) + } + result = append(result, bizMembership) + } + + return result, count, nil } // FindByOrgAndUser finds the membership for a given organization and user