From e4704f8ac9f93b11070bebcb2edd4c4aa8884592 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Fri, 27 Jun 2025 16:39:22 +0200 Subject: [PATCH 1/6] feat(groups): Allow group membership management Signed-off-by: Javier Rodriguez --- app/cli/cmd/group.go | 33 + app/cli/cmd/group_create.go | 80 ++ app/cli/cmd/group_delete.go | 59 ++ app/cli/cmd/group_describe.go | 51 ++ app/cli/cmd/group_list.go | 158 ++++ app/cli/cmd/group_member.go | 30 + app/cli/cmd/group_member_add.go | 58 ++ app/cli/cmd/group_member_delete.go | 60 ++ app/cli/cmd/group_member_list.go | 133 +++ app/cli/cmd/output.go | 5 +- app/cli/cmd/root.go | 2 +- app/cli/internal/action/group_create.go | 76 ++ app/cli/internal/action/group_delete.go | 47 + app/cli/internal/action/group_describe.go | 52 ++ app/cli/internal/action/group_list.go | 92 ++ app/cli/internal/action/group_member_add.go | 53 ++ .../internal/action/group_member_delete.go | 48 + app/cli/internal/action/group_member_list.go | 129 +++ .../api/controlplane/v1/group.pb.go | 824 ++++++++++++------ .../api/controlplane/v1/group.proto | 65 +- .../api/controlplane/v1/group_grpc.pb.go | 90 +- .../controlplane/v1/response_messages.pb.go | 409 ++++----- .../controlplane/v1/response_messages.proto | 15 - .../api/controlplane/v1/shared_message.pb.go | 192 ++++ .../api/controlplane/v1/shared_message.proto | 43 + .../api/gen/frontend/controlplane/v1/group.ts | 496 +++++++++-- .../controlplane/v1/response_messages.ts | 77 -- .../controlplane/v1/shared_message.ts | 100 +++ ...rolplane.v1.GroupReference.jsonschema.json | 32 + ...controlplane.v1.GroupReference.schema.json | 32 + ...oupServiceAddMemberRequest.jsonschema.json | 41 + ...1.GroupServiceAddMemberRequest.schema.json | 41 + ...upServiceAddMemberResponse.jsonschema.json | 9 + ....GroupServiceAddMemberResponse.schema.json | 9 + ....GroupServiceDeleteRequest.jsonschema.json | 16 +- ...e.v1.GroupServiceDeleteRequest.schema.json | 16 +- ....v1.GroupServiceGetRequest.jsonschema.json | 18 +- ...lane.v1.GroupServiceGetRequest.schema.json | 18 +- ...pServiceListMembersRequest.jsonschema.json | 18 +- ...GroupServiceListMembersRequest.schema.json | 18 +- ...ServiceRemoveMemberRequest.jsonschema.json | 33 + ...roupServiceRemoveMemberRequest.schema.json | 33 + ...erviceRemoveMemberResponse.jsonschema.json | 9 + ...oupServiceRemoveMemberResponse.schema.json | 9 + ....GroupServiceUpdateRequest.jsonschema.json | 30 +- ...e.v1.GroupServiceUpdateRequest.schema.json | 30 +- ...plane.v1.IdentityReference.jsonschema.json | 20 + ...trolplane.v1.IdentityReference.schema.json | 20 + app/controlplane/cmd/wire.go | 3 +- app/controlplane/cmd/wire_gen.go | 10 +- app/controlplane/internal/service/group.go | 212 +++-- app/controlplane/internal/service/service.go | 125 +++ app/controlplane/pkg/auditor/events/group.go | 74 +- .../pkg/auditor/events/group_test.go | 109 ++- .../testdata/groups/group_member_added.json | 18 + .../testdata/groups/group_member_removed.json | 17 + app/controlplane/pkg/authz/authz.go | 28 +- app/controlplane/pkg/authz/membership.go | 2 + app/controlplane/pkg/biz/biz.go | 10 + app/controlplane/pkg/biz/group.go | 316 ++++++- .../pkg/biz/group_integration_test.go | 541 +++++++++++- app/controlplane/pkg/biz/membership.go | 1 + .../pkg/data/ent/membership/membership.go | 4 +- .../ent/migrate/migrations/20250627143634.sql | 4 + .../pkg/data/ent/migrate/migrations/atlas.sum | 3 +- .../pkg/data/ent/migrate/schema.go | 9 +- .../data/ent/orginvitation/orginvitation.go | 2 +- .../pkg/data/ent/schema/group_membership.go | 11 + app/controlplane/pkg/data/group.go | 145 ++- app/controlplane/pkg/data/membership.go | 30 + app/controlplane/pkg/pagination/offset.go | 3 +- 71 files changed, 4678 insertions(+), 928 deletions(-) create mode 100644 app/cli/cmd/group.go create mode 100644 app/cli/cmd/group_create.go create mode 100644 app/cli/cmd/group_delete.go create mode 100644 app/cli/cmd/group_describe.go create mode 100644 app/cli/cmd/group_list.go create mode 100644 app/cli/cmd/group_member.go create mode 100644 app/cli/cmd/group_member_add.go create mode 100644 app/cli/cmd/group_member_delete.go create mode 100644 app/cli/cmd/group_member_list.go create mode 100644 app/cli/internal/action/group_create.go create mode 100644 app/cli/internal/action/group_delete.go create mode 100644 app/cli/internal/action/group_describe.go create mode 100644 app/cli/internal/action/group_list.go create mode 100644 app/cli/internal/action/group_member_add.go create mode 100644 app/cli/internal/action/group_member_delete.go create mode 100644 app/cli/internal/action/group_member_list.go create mode 100644 app/controlplane/api/controlplane/v1/shared_message.pb.go create mode 100644 app/controlplane/api/controlplane/v1/shared_message.proto create mode 100644 app/controlplane/api/gen/frontend/controlplane/v1/shared_message.ts create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupReference.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupReference.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberRequest.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberRequest.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberResponse.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberResponse.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberRequest.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberRequest.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberResponse.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberResponse.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.IdentityReference.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.IdentityReference.schema.json create mode 100644 app/controlplane/pkg/auditor/events/testdata/groups/group_member_added.json create mode 100644 app/controlplane/pkg/auditor/events/testdata/groups/group_member_removed.json create mode 100644 app/controlplane/pkg/data/ent/migrate/migrations/20250627143634.sql diff --git a/app/cli/cmd/group.go b/app/cli/cmd/group.go new file mode 100644 index 000000000..f35e6e103 --- /dev/null +++ b/app/cli/cmd/group.go @@ -0,0 +1,33 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func newGroupCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "group", + Aliases: []string{"groups", "grp", "g"}, + Short: "Group management", + Hidden: true, + } + + cmd.AddCommand(newGroupCreateCmd(), newGroupDescribeCmd(), newGroupListCmd(), newGroupDeleteCmd(), newGroupMembersCmd()) + + return cmd +} diff --git a/app/cli/cmd/group_create.go b/app/cli/cmd/group_create.go new file mode 100644 index 000000000..9a25909e0 --- /dev/null +++ b/app/cli/cmd/group_create.go @@ -0,0 +1,80 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" +) + +func newGroupCreateCmd() *cobra.Command { + var name, description string + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new group", + Example: ` chainloop group create --name [groupName] + + # With a description + chainloop group create --name developers --description "Group for developers" + `, + RunE: func(cmd *cobra.Command, _ []string) error { + resp, err := action.NewGroupCreate(actionOpts).Run(cmd.Context(), name, description) + if err != nil { + return err + } + + // Print the group details + if err := encodeOutput(resp, groupItemTableOutput); err != nil { + return fmt.Errorf("failed to print group: %w", err) + } + + logger.Info().Msg("Group created successfully") + + return nil + }, + } + + cmd.Flags().StringVar(&name, "name", "", "group name") + err := cmd.MarkFlagRequired("name") + cobra.CheckErr(err) + cmd.Flags().StringVar(&description, "description", "", "group description") + cmd.Flags().SortFlags = false + + return cmd +} + +// Format function for group output +func groupItemTableOutput(data *action.GroupCreateItem) error { + t := newTableWriter() + + t.AppendHeader(table.Row{"ID", "Name", "Description", "Created At", "Updated At"}) + t.AppendRow(table.Row{ + data.ID, + data.Name, + data.Description, + data.CreatedAt, + data.UpdatedAt, + }) + + t.Render() + + return nil +} diff --git a/app/cli/cmd/group_delete.go b/app/cli/cmd/group_delete.go new file mode 100644 index 000000000..b03d94fd1 --- /dev/null +++ b/app/cli/cmd/group_delete.go @@ -0,0 +1,59 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/spf13/cobra" +) + +func newGroupDeleteCmd() *cobra.Command { + var groupName string + var force bool + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a group", + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + + // Ask for confirmation, unless the --yes flag is set + if !flagYes { + logger.Warn().Msgf("Are you sure you want to delete the group '%s'?", groupName) + + if err := confirmDeletion(); err != nil { + return err + } + } + + if err := action.NewGroupDelete(actionOpts).Run(ctx, groupName); err != nil { + return fmt.Errorf("removing group: %w", err) + } + + logger.Info().Msgf("Group '%s' has been removed", groupName) + return nil + }, + } + + cmd.Flags().StringVar(&groupName, "name", "", "Name of the group to remove") + cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt") + err := cmd.MarkFlagRequired("name") + cobra.CheckErr(err) + + return cmd +} diff --git a/app/cli/cmd/group_describe.go b/app/cli/cmd/group_describe.go new file mode 100644 index 000000000..d99664967 --- /dev/null +++ b/app/cli/cmd/group_describe.go @@ -0,0 +1,51 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/spf13/cobra" +) + +func newGroupDescribeCmd() *cobra.Command { + var groupName string + + cmd := &cobra.Command{ + Use: "describe", + Short: "Get detailed information about a specific group", + RunE: func(cmd *cobra.Command, _ []string) error { + group, err := action.NewGroupDescribe(actionOpts).Run(cmd.Context(), groupName) + if err != nil { + return fmt.Errorf("describing group: %w", err) + } + + // Print the group details + if err := encodeOutput(group, groupItemTableOutput); err != nil { + return fmt.Errorf("failed to print group: %w", err) + } + + return nil + }, + } + + cmd.Flags().StringVar(&groupName, "name", "", "Name of the group to describe") + err := cmd.MarkFlagRequired("name") + cobra.CheckErr(err) + + return cmd +} diff --git a/app/cli/cmd/group_list.go b/app/cli/cmd/group_list.go new file mode 100644 index 000000000..d54ee1ddd --- /dev/null +++ b/app/cli/cmd/group_list.go @@ -0,0 +1,158 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +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 newGroupListCmd() *cobra.Command { + var paginationOpts = &options.OffsetPaginationOpts{} + var groupName, description, memberEmail string + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List existing Groups", + Example: ` # Let the default pagination apply + chainloop group list + + # Specify the page and page size + chainloop group list --page 2 --limit 10 + + # Output in json format to paginate using scripts + chainloop group list --page 2 --limit 10 --output json + + # Filter by group name + chainloop group list --group-name developers + + # Filter by description + chainloop group list --description "team members" + + # Filter by member email + chainloop group list --member-email user@example.com + + # Show the full report + chainloop group list --full +`, + 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, _ []string) error { + filterOpts := &action.GroupListFilterOpts{ + GroupName: groupName, + Description: description, + MemberEmail: memberEmail, + } + + res, err := action.NewGroupList(actionOpts).Run(cmd.Context(), paginationOpts.Page, paginationOpts.Limit, filterOpts) + if err != nil { + return err + } + + if err := encodeOutput(res, GroupListTableOutput); err != nil { + return err + } + + pgResponse := res.Pagination + + if pgResponse.TotalPages >= paginationOpts.Page { + inPage := min(paginationOpts.Limit, len(res.Groups)) + 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().BoolVar(&full, "full", false, "show the full report") + cmd.Flags().StringVar(&groupName, "group-name", "", "filter by group name") + cmd.Flags().StringVar(&description, "description", "", "filter by description") + cmd.Flags().StringVar(&memberEmail, "member-email", "", "filter by member email") + paginationOpts.AddFlags(cmd) + + return cmd +} + +func GroupListTableOutput(groupListResult *action.GroupListResult) error { + if len(groupListResult.Groups) == 0 { + fmt.Println("there are no groups yet") + return nil + } + + headerRow := table.Row{"ID", "Name", "Description", "Created At"} + headerRowFull := table.Row{"ID", "Name", "Description", "Created At", "Updated At"} + + t := newTableWriter() + if full { + t.AppendHeader(headerRowFull) + } else { + t.AppendHeader(headerRow) + } + + for _, g := range groupListResult.Groups { + var row table.Row + + if !full { + row = table.Row{ + g.ID, g.Name, g.Description, + formatTime(g.CreatedAt), + } + } else { + row = table.Row{ + g.ID, g.Name, g.Description, + formatTime(g.CreatedAt), formatTime(g.UpdatedAt), + } + } + + t.AppendRow(row) + } + t.Render() + + return nil +} + +// Helper function to format time string +func formatTime(timeStr string) string { + if timeStr == "" { + return "" + } + + t, err := time.Parse(time.RFC3339, timeStr) + if err != nil { + return timeStr + } + + return t.Format(time.RFC822) +} diff --git a/app/cli/cmd/group_member.go b/app/cli/cmd/group_member.go new file mode 100644 index 000000000..220cdf5e8 --- /dev/null +++ b/app/cli/cmd/group_member.go @@ -0,0 +1,30 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import "github.com/spf13/cobra" + +func newGroupMembersCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "member", + Aliases: []string{"m"}, + Short: "Group members management", + } + + cmd.AddCommand(newGroupMemberListCmd(), newGroupMemberAddCmd(), newGroupMemberDeleteCmd()) + + return cmd +} diff --git a/app/cli/cmd/group_member_add.go b/app/cli/cmd/group_member_add.go new file mode 100644 index 000000000..a1bf3a7fa --- /dev/null +++ b/app/cli/cmd/group_member_add.go @@ -0,0 +1,58 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/spf13/cobra" +) + +func newGroupMemberAddCmd() *cobra.Command { + var groupName, memberEmail string + var isMaintainer bool + + cmd := &cobra.Command{ + Use: "add", + Short: "Add a member to a group", + Example: ` # Add a member to a group + chainloop group members add --name developers --email user@example.com + + # Add a member as a maintainer + chainloop group members add --name developers --member-email user@example.com --maintainer +`, + RunE: func(cmd *cobra.Command, _ []string) error { + err := action.NewGroupMemberAdd(actionOpts).Run(cmd.Context(), groupName, memberEmail, isMaintainer) + if err != nil { + return err + } + + logger.Info().Msgf("Member %s successfully added to group %s", memberEmail, groupName) + return nil + }, + } + + cmd.Flags().StringVar(&groupName, "name", "", "name of the group") + err := cmd.MarkFlagRequired("name") + cobra.CheckErr(err) + + cmd.Flags().StringVar(&memberEmail, "email", "", "email of the member to add") + err = cmd.MarkFlagRequired("email") + cobra.CheckErr(err) + + cmd.Flags().BoolVar(&isMaintainer, "maintainer", false, "add member as a maintainer") + + return cmd +} diff --git a/app/cli/cmd/group_member_delete.go b/app/cli/cmd/group_member_delete.go new file mode 100644 index 000000000..e7232989d --- /dev/null +++ b/app/cli/cmd/group_member_delete.go @@ -0,0 +1,60 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/spf13/cobra" +) + +func newGroupMemberDeleteCmd() *cobra.Command { + var groupName, memberEmail string + + cmd := &cobra.Command{ + Use: "delete", + Short: "Remove a member from a group", + Example: ` # Remove a member from a group + chainloop group member delete --name developers --email user@example.com +`, + RunE: func(cmd *cobra.Command, _ []string) error { + // Ask for confirmation, unless the --yes flag is set + if !flagYes { + logger.Warn().Msgf("You are about to remove the user %q from the group %q\n", memberEmail, groupName) + + if err := confirmDeletion(); err != nil { + return err + } + } + + if err := action.NewGroupMemberDelete(actionOpts).Run(cmd.Context(), groupName, memberEmail); err != nil { + return err + } + + logger.Info().Msgf("Member %s successfully removed from group %s", memberEmail, groupName) + return nil + }, + } + + cmd.Flags().StringVar(&groupName, "name", "", "name of the group") + err := cmd.MarkFlagRequired("name") + cobra.CheckErr(err) + + cmd.Flags().StringVar(&memberEmail, "email", "", "email of the member to remove") + err = cmd.MarkFlagRequired("email") + cobra.CheckErr(err) + + return cmd +} diff --git a/app/cli/cmd/group_member_list.go b/app/cli/cmd/group_member_list.go new file mode 100644 index 000000000..1285a69a0 --- /dev/null +++ b/app/cli/cmd/group_member_list.go @@ -0,0 +1,133 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "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 newGroupMemberListCmd() *cobra.Command { + var paginationOpts = &options.OffsetPaginationOpts{} + var groupName, memberEmail string + var role string + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"list", "ls"}, + Short: "List members of a group", + Example: ` # List all members of a group + chainloop group members list --name developers + + # List only maintainers of a group + chainloop group members list --name developers --role maintainer + + # List only members of a group + chainloop group members list --name developers --role member + + # Filter by member email + chainloop group members list --name developers --member-email user@example.com + + # Specify the page and page size + chainloop group members list --name developers --page 2 --limit 10 + + # Output in json format for scripts + chainloop group members list --name developers --output json +`, + 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") + } + + // Validate role flag if provided + if role != "" && role != "maintainer" && role != "member" { + return fmt.Errorf("--role must be either 'maintainer' or 'member'") + } + + return nil + }, + RunE: func(cmd *cobra.Command, _ []string) error { + filterOpts := &action.GroupMemberListFilterOpts{ + GroupName: groupName, + MemberEmail: memberEmail, + Role: role, + } + + res, err := action.NewGroupMemberList(actionOpts).Run(cmd.Context(), paginationOpts.Page, paginationOpts.Limit, filterOpts) + if err != nil { + return err + } + + if err := encodeOutput(res, GroupMemberListTableOutput); err != nil { + return err + } + + pgResponse := res.Pagination + + if pgResponse.TotalPages >= paginationOpts.Page { + inPage := min(paginationOpts.Limit, len(res.Members)) + 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(&groupName, "name", "", "name of the group") + err := cmd.MarkFlagRequired("name") + cobra.CheckErr(err) + cmd.Flags().StringVar(&memberEmail, "member-email", "", "filter by member email") + cmd.Flags().StringVar(&role, "role", "", "filter by role (maintainer, member), by default all members are listed") + paginationOpts.AddFlags(cmd) + + return cmd +} + +func GroupMemberListTableOutput(memberListResult *action.GroupMemberListResult) error { + if len(memberListResult.Members) == 0 { + fmt.Println("there are no members in this group") + return nil + } + + headerRow := table.Row{"Email", "Role", "Added At"} + + t := newTableWriter() + t.AppendHeader(headerRow) + + for _, m := range memberListResult.Members { + row := table.Row{ + m.User.PrintUserProfileWithEmail(), + m.Role, + formatTime(m.AddedAt), + } + t.AppendRow(row) + } + t.Render() + + return nil +} diff --git a/app/cli/cmd/output.go b/app/cli/cmd/output.go index 5a0c05b59..e2fa4e010 100644 --- a/app/cli/cmd/output.go +++ b/app/cli/cmd/output.go @@ -53,7 +53,10 @@ type tabulatedData interface { []*action.OrgInvitationItem | *action.APITokenItem | []*action.APITokenItem | - *action.AttestationStatusMaterial + *action.AttestationStatusMaterial | + *action.GroupCreateItem | + *action.GroupListResult | + *action.GroupMemberListResult } var ErrOutputFormatNotImplemented = errors.New("format not implemented") diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index aca56ef3d..bc6a3a5c5 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -250,7 +250,7 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { rootCmd.AddCommand(newWorkflowCmd(), newAuthCmd(), NewVersionCmd(), newAttestationCmd(), newArtifactCmd(), newConfigCmd(), newIntegrationCmd(), newOrganizationCmd(), newCASBackendCmd(), - newReferrerDiscoverCmd(), + newReferrerDiscoverCmd(), newGroupCmd(), ) return rootCmd diff --git a/app/cli/internal/action/group_create.go b/app/cli/internal/action/group_create.go new file mode 100644 index 000000000..aec759fe0 --- /dev/null +++ b/app/cli/internal/action/group_create.go @@ -0,0 +1,76 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + "time" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +// GroupCreateItem represents the response structure for a created group +type GroupCreateItem struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` +} + +// GroupCreate handles the creation of a new group +type GroupCreate struct { + cfg *ActionsOpts +} + +// NewGroupCreate creates a new instance of GroupCreate +func NewGroupCreate(cfg *ActionsOpts) *GroupCreate { + return &GroupCreate{cfg} +} + +// Run executes the group creation operation +func (action *GroupCreate) Run(ctx context.Context, name, description string) (*GroupCreateItem, error) { + client := pb.NewGroupServiceClient(action.cfg.CPConnection) + resp, err := client.Create(ctx, &pb.GroupServiceCreateRequest{ + Name: name, + Description: description, + }) + if err != nil { + return nil, err + } + + return pbGroupItemToAction(resp.GetGroup()), nil +} + +// pbGroupItemToAction converts a protobuf group item to the action model +func pbGroupItemToAction(group *pb.Group) *GroupCreateItem { + createdAt := "" + if group.CreatedAt != nil { + createdAt = group.CreatedAt.AsTime().Format(time.RFC3339) + } + updatedAt := "" + if group.UpdatedAt != nil { + updatedAt = group.UpdatedAt.AsTime().Format(time.RFC3339) + } + + return &GroupCreateItem{ + ID: group.Id, + Name: group.Name, + Description: group.Description, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} diff --git a/app/cli/internal/action/group_delete.go b/app/cli/internal/action/group_delete.go new file mode 100644 index 000000000..37ef69ce5 --- /dev/null +++ b/app/cli/internal/action/group_delete.go @@ -0,0 +1,47 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +// GroupDelete handles deleting a group +type GroupDelete struct { + cfg *ActionsOpts +} + +// NewGroupDelete creates a new instance of GroupDelete +func NewGroupDelete(cfg *ActionsOpts) *GroupDelete { + return &GroupDelete{cfg} +} + +// Run executes the group deletion operation +func (action *GroupDelete) Run(ctx context.Context, groupName string) error { + client := pb.NewGroupServiceClient(action.cfg.CPConnection) + + // Build the request + req := &pb.GroupServiceDeleteRequest{ + GroupReference: &pb.IdentityReference{ + Name: &groupName, + }, + } + + _, err := client.Delete(ctx, req) + return err +} diff --git a/app/cli/internal/action/group_describe.go b/app/cli/internal/action/group_describe.go new file mode 100644 index 000000000..2b2442fdc --- /dev/null +++ b/app/cli/internal/action/group_describe.go @@ -0,0 +1,52 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +// GroupDescribe handles retrieving detailed information about a specific group +type GroupDescribe struct { + cfg *ActionsOpts +} + +// NewGroupDescribe creates a new instance of GroupDescribe +func NewGroupDescribe(cfg *ActionsOpts) *GroupDescribe { + return &GroupDescribe{cfg} +} + +// Run executes the group describe operation +func (action *GroupDescribe) Run(ctx context.Context, groupName string) (*GroupCreateItem, error) { + client := pb.NewGroupServiceClient(action.cfg.CPConnection) + + // Build the request + req := &pb.GroupServiceGetRequest{ + GroupReference: &pb.IdentityReference{ + Name: &groupName, + }, + } + + resp, err := client.Get(ctx, req) + if err != nil { + return nil, err + } + + // Convert the response to our model + return pbGroupItemToAction(resp.GetGroup()), nil +} diff --git a/app/cli/internal/action/group_list.go b/app/cli/internal/action/group_list.go new file mode 100644 index 000000000..002d8019b --- /dev/null +++ b/app/cli/internal/action/group_list.go @@ -0,0 +1,92 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +// GroupListResult represents the response for a list of groups +type GroupListResult struct { + Groups []*GroupCreateItem `json:"groups"` + Pagination *OffsetPagination `json:"pagination"` +} + +// GroupListFilterOpts contains the filters for group listing +type GroupListFilterOpts struct { + GroupName string + Description string + MemberEmail string +} + +// GroupList handles the listing of groups +type GroupList struct { + cfg *ActionsOpts +} + +// NewGroupList creates a new instance of GroupList +func NewGroupList(cfg *ActionsOpts) *GroupList { + return &GroupList{cfg} +} + +// Run executes the group list operation with pagination and filtering +func (action *GroupList) Run(ctx context.Context, page, limit int, filterOpts *GroupListFilterOpts) (*GroupListResult, error) { + client := pb.NewGroupServiceClient(action.cfg.CPConnection) + + // Build the request + req := &pb.GroupServiceListRequest{ + Pagination: &pb.OffsetPaginationRequest{ + Page: int32(page), + PageSize: int32(limit), + }, + } + + // Create filters for the request + if filterOpts.GroupName != "" { + req.Name = &filterOpts.GroupName + } + if filterOpts.Description != "" { + req.Description = &filterOpts.Description + } + if filterOpts.MemberEmail != "" { + req.MemberEmail = &filterOpts.MemberEmail + } + + resp, err := client.List(ctx, req) + if err != nil { + return nil, err + } + + // Convert the response to our model + result := &GroupListResult{ + Groups: make([]*GroupCreateItem, 0, len(resp.GetGroups())), + Pagination: &OffsetPagination{ + Page: int(resp.GetPagination().GetPage()), + PageSize: int(resp.GetPagination().GetPageSize()), + TotalCount: int(resp.GetPagination().GetTotalCount()), + TotalPages: int(resp.GetPagination().GetTotalPages()), + }, + } + + // Process each group + for _, group := range resp.GetGroups() { + result.Groups = append(result.Groups, pbGroupItemToAction(group)) + } + + return result, nil +} diff --git a/app/cli/internal/action/group_member_add.go b/app/cli/internal/action/group_member_add.go new file mode 100644 index 000000000..ffbad6101 --- /dev/null +++ b/app/cli/internal/action/group_member_add.go @@ -0,0 +1,53 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +// GroupMemberAdd handles adding a member to a group +type GroupMemberAdd struct { + cfg *ActionsOpts +} + +// NewGroupMemberAdd creates a new instance of GroupMemberAdd +func NewGroupMemberAdd(cfg *ActionsOpts) *GroupMemberAdd { + return &GroupMemberAdd{cfg} +} + +// Run executes the group member addition operation +func (action *GroupMemberAdd) Run(ctx context.Context, groupName, memberEmail string, isMaintainer bool) error { + client := pb.NewGroupServiceClient(action.cfg.CPConnection) + + // Build the request + req := &pb.GroupServiceAddMemberRequest{ + GroupReference: &pb.IdentityReference{ + Name: &groupName, + }, + UserEmail: memberEmail, + IsMaintainer: isMaintainer, + } + + _, err := client.AddMember(ctx, req) + if err != nil { + return err + } + + return nil +} diff --git a/app/cli/internal/action/group_member_delete.go b/app/cli/internal/action/group_member_delete.go new file mode 100644 index 000000000..341f9baf1 --- /dev/null +++ b/app/cli/internal/action/group_member_delete.go @@ -0,0 +1,48 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +// GroupMemberDelete handles removing a member from a group +type GroupMemberDelete struct { + cfg *ActionsOpts +} + +// NewGroupMemberDelete creates a new instance of GroupMemberDelete +func NewGroupMemberDelete(cfg *ActionsOpts) *GroupMemberDelete { + return &GroupMemberDelete{cfg} +} + +// Run executes the group member removal operation +func (action *GroupMemberDelete) Run(ctx context.Context, groupName, memberEmail string) error { + client := pb.NewGroupServiceClient(action.cfg.CPConnection) + + // Build the request + req := &pb.GroupServiceRemoveMemberRequest{ + GroupReference: &pb.IdentityReference{ + Name: &groupName, + }, + UserEmail: memberEmail, + } + + _, err := client.RemoveMember(ctx, req) + return err +} diff --git a/app/cli/internal/action/group_member_list.go b/app/cli/internal/action/group_member_list.go new file mode 100644 index 000000000..b0eef9ad6 --- /dev/null +++ b/app/cli/internal/action/group_member_list.go @@ -0,0 +1,129 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + "time" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +// GroupMemberItem represents a member in a group +type GroupMemberItem struct { + User UserItem `json:"user"` + Role string `json:"role"` + AddedAt string `json:"added_at"` +} + +// GroupMemberListResult represents the response for a list of group members +type GroupMemberListResult struct { + Members []*GroupMemberItem `json:"members"` + Pagination *OffsetPagination `json:"pagination"` +} + +// GroupMemberListFilterOpts contains the filters for group member listing +type GroupMemberListFilterOpts struct { + GroupName string + MemberEmail string + Role string // can be "maintainer" or "member" +} + +// GroupMemberList handles the listing of group members +type GroupMemberList struct { + cfg *ActionsOpts +} + +// NewGroupMemberList creates a new instance of GroupMemberList +func NewGroupMemberList(cfg *ActionsOpts) *GroupMemberList { + return &GroupMemberList{cfg} +} + +// Run executes the group member list operation with pagination and filtering +func (action *GroupMemberList) Run(ctx context.Context, page, limit int, filterOpts *GroupMemberListFilterOpts) (*GroupMemberListResult, error) { + client := pb.NewGroupServiceClient(action.cfg.CPConnection) + + // Build the request + req := &pb.GroupServiceListMembersRequest{ + GroupReference: &pb.IdentityReference{ + Name: &filterOpts.GroupName, + }, + Pagination: &pb.OffsetPaginationRequest{ + Page: int32(page), + PageSize: int32(limit), + }, + } + + // Apply filters + if filterOpts.MemberEmail != "" { + req.MemberEmail = &filterOpts.MemberEmail + } + if filterOpts.Role != "" { + isMaintainer := filterOpts.Role == "maintainer" + req.Maintainers = &isMaintainer + } + + resp, err := client.ListMembers(ctx, req) + if err != nil { + return nil, err + } + + // Convert the response to our model + result := &GroupMemberListResult{ + Members: make([]*GroupMemberItem, 0, len(resp.GetMembers())), + Pagination: &OffsetPagination{ + Page: int(resp.GetPagination().GetPage()), + PageSize: int(resp.GetPagination().GetPageSize()), + TotalCount: int(resp.GetPagination().GetTotalCount()), + TotalPages: int(resp.GetPagination().GetTotalPages()), + }, + } + + // Process each member + for _, member := range resp.GetMembers() { + result.Members = append(result.Members, pbGroupMemberToAction(member)) + } + + return result, nil +} + +// pbGroupMemberToAction converts a protobuf group member to the action model +func pbGroupMemberToAction(member *pb.GroupMember) *GroupMemberItem { + addedAt := "" + if member.CreatedAt != nil { + addedAt = member.CreatedAt.AsTime().Format(time.RFC3339) + } + + return &GroupMemberItem{ + User: UserItem{ + ID: member.User.GetId(), + Email: member.User.GetEmail(), + FirstName: member.User.GetFirstName(), + LastName: member.User.GetLastName(), + }, + Role: getRoleName(member.IsMaintainer), + AddedAt: addedAt, + } +} + +// getRoleName returns a human-readable role name from the role enum +func getRoleName(maintainer bool) string { + if maintainer { + return "Maintainer" + } else { + return "Member" + } +} diff --git a/app/controlplane/api/controlplane/v1/group.pb.go b/app/controlplane/api/controlplane/v1/group.pb.go index 428ed5763..b61292fc2 100644 --- a/app/controlplane/api/controlplane/v1/group.pb.go +++ b/app/controlplane/api/controlplane/v1/group.pb.go @@ -150,8 +150,8 @@ type GroupServiceGetRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // UUID of the group to retrieve - GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + // IdentityReference is used to specify the group by either its ID or name + GroupReference *IdentityReference `protobuf:"bytes,1,opt,name=group_reference,json=groupReference,proto3" json:"group_reference,omitempty"` } func (x *GroupServiceGetRequest) Reset() { @@ -186,11 +186,11 @@ func (*GroupServiceGetRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_group_proto_rawDescGZIP(), []int{2} } -func (x *GroupServiceGetRequest) GetGroupId() string { +func (x *GroupServiceGetRequest) GetGroupReference() *IdentityReference { if x != nil { - return x.GroupId + return x.GroupReference } - return "" + return nil } // GroupServiceGetResponse contains the requested group information @@ -382,12 +382,12 @@ type GroupServiceUpdateRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // UUID of the group to update - GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + // IdentityReference is used to specify the group by either its ID or name + GroupReference *IdentityReference `protobuf:"bytes,1,opt,name=group_reference,json=groupReference,proto3" json:"group_reference,omitempty"` // New name for the group (if provided) - Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"` + NewName *string `protobuf:"bytes,3,opt,name=new_name,json=newName,proto3,oneof" json:"new_name,omitempty"` // New description for the group (if provided) - Description *string `protobuf:"bytes,3,opt,name=description,proto3,oneof" json:"description,omitempty"` + NewDescription *string `protobuf:"bytes,4,opt,name=new_description,json=newDescription,proto3,oneof" json:"new_description,omitempty"` } func (x *GroupServiceUpdateRequest) Reset() { @@ -422,23 +422,23 @@ func (*GroupServiceUpdateRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_group_proto_rawDescGZIP(), []int{6} } -func (x *GroupServiceUpdateRequest) GetGroupId() string { +func (x *GroupServiceUpdateRequest) GetGroupReference() *IdentityReference { if x != nil { - return x.GroupId + return x.GroupReference } - return "" + return nil } -func (x *GroupServiceUpdateRequest) GetName() string { - if x != nil && x.Name != nil { - return *x.Name +func (x *GroupServiceUpdateRequest) GetNewName() string { + if x != nil && x.NewName != nil { + return *x.NewName } return "" } -func (x *GroupServiceUpdateRequest) GetDescription() string { - if x != nil && x.Description != nil { - return *x.Description +func (x *GroupServiceUpdateRequest) GetNewDescription() string { + if x != nil && x.NewDescription != nil { + return *x.NewDescription } return "" } @@ -498,8 +498,8 @@ type GroupServiceDeleteRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // UUID of the group to delete - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // IdentityReference is used to specify the group by either its ID or name + GroupReference *IdentityReference `protobuf:"bytes,1,opt,name=group_reference,json=groupReference,proto3" json:"group_reference,omitempty"` } func (x *GroupServiceDeleteRequest) Reset() { @@ -534,11 +534,11 @@ func (*GroupServiceDeleteRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_group_proto_rawDescGZIP(), []int{8} } -func (x *GroupServiceDeleteRequest) GetId() string { +func (x *GroupServiceDeleteRequest) GetGroupReference() *IdentityReference { if x != nil { - return x.Id + return x.GroupReference } - return "" + return nil } // GroupServiceDeleteResponse is returned upon successful deletion of a group @@ -643,14 +643,14 @@ type GroupServiceListMembersRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // UUID of the group whose members are to be listed - GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + // IdentityReference is used to specify the group by either its ID or name + GroupReference *IdentityReference `protobuf:"bytes,1,opt,name=group_reference,json=groupReference,proto3" json:"group_reference,omitempty"` // Optional filter to search only by maintainers or not - Maintainers *bool `protobuf:"varint,2,opt,name=maintainers,proto3,oneof" json:"maintainers,omitempty"` + Maintainers *bool `protobuf:"varint,3,opt,name=maintainers,proto3,oneof" json:"maintainers,omitempty"` // Optional filter to search by member email address - MemberEmail *string `protobuf:"bytes,3,opt,name=member_email,json=memberEmail,proto3,oneof" json:"member_email,omitempty"` + MemberEmail *string `protobuf:"bytes,4,opt,name=member_email,json=memberEmail,proto3,oneof" json:"member_email,omitempty"` // Pagination parameters to limit and offset results - Pagination *OffsetPaginationRequest `protobuf:"bytes,4,opt,name=pagination,proto3" json:"pagination,omitempty"` + Pagination *OffsetPaginationRequest `protobuf:"bytes,5,opt,name=pagination,proto3" json:"pagination,omitempty"` } func (x *GroupServiceListMembersRequest) Reset() { @@ -685,11 +685,11 @@ func (*GroupServiceListMembersRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_group_proto_rawDescGZIP(), []int{11} } -func (x *GroupServiceListMembersRequest) GetGroupId() string { +func (x *GroupServiceListMembersRequest) GetGroupReference() *IdentityReference { if x != nil { - return x.GroupId + return x.GroupReference } - return "" + return nil } func (x *GroupServiceListMembersRequest) GetMaintainers() bool { @@ -713,6 +713,209 @@ func (x *GroupServiceListMembersRequest) GetPagination() *OffsetPaginationReques return nil } +// GroupServiceAddMemberRequest contains the information needed to add a user to a group +type GroupServiceAddMemberRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // IdentityReference is used to specify the group by either its ID or name + GroupReference *IdentityReference `protobuf:"bytes,1,opt,name=group_reference,json=groupReference,proto3" json:"group_reference,omitempty"` + // The user to add to the group + UserEmail string `protobuf:"bytes,3,opt,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` + // Indicates whether the user should have maintainer (admin) privileges in the group + IsMaintainer bool `protobuf:"varint,4,opt,name=is_maintainer,json=isMaintainer,proto3" json:"is_maintainer,omitempty"` +} + +func (x *GroupServiceAddMemberRequest) Reset() { + *x = GroupServiceAddMemberRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_group_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GroupServiceAddMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupServiceAddMemberRequest) ProtoMessage() {} + +func (x *GroupServiceAddMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_group_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupServiceAddMemberRequest.ProtoReflect.Descriptor instead. +func (*GroupServiceAddMemberRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_group_proto_rawDescGZIP(), []int{12} +} + +func (x *GroupServiceAddMemberRequest) GetGroupReference() *IdentityReference { + if x != nil { + return x.GroupReference + } + return nil +} + +func (x *GroupServiceAddMemberRequest) GetUserEmail() string { + if x != nil { + return x.UserEmail + } + return "" +} + +func (x *GroupServiceAddMemberRequest) GetIsMaintainer() bool { + if x != nil { + return x.IsMaintainer + } + return false +} + +// GroupServiceAddMemberResponse contains the information about the group member that was added +type GroupServiceAddMemberResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GroupServiceAddMemberResponse) Reset() { + *x = GroupServiceAddMemberResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_group_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GroupServiceAddMemberResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupServiceAddMemberResponse) ProtoMessage() {} + +func (x *GroupServiceAddMemberResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_group_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupServiceAddMemberResponse.ProtoReflect.Descriptor instead. +func (*GroupServiceAddMemberResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_group_proto_rawDescGZIP(), []int{13} +} + +// GroupServiceRemoveMemberRequest contains the information needed to remove a user from a group +type GroupServiceRemoveMemberRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // IdentityReference is used to specify the group by either its ID or name + GroupReference *IdentityReference `protobuf:"bytes,1,opt,name=group_reference,json=groupReference,proto3" json:"group_reference,omitempty"` + // The user to remove from the group + UserEmail string `protobuf:"bytes,3,opt,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` +} + +func (x *GroupServiceRemoveMemberRequest) Reset() { + *x = GroupServiceRemoveMemberRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_group_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GroupServiceRemoveMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupServiceRemoveMemberRequest) ProtoMessage() {} + +func (x *GroupServiceRemoveMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_group_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupServiceRemoveMemberRequest.ProtoReflect.Descriptor instead. +func (*GroupServiceRemoveMemberRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_group_proto_rawDescGZIP(), []int{14} +} + +func (x *GroupServiceRemoveMemberRequest) GetGroupReference() *IdentityReference { + if x != nil { + return x.GroupReference + } + return nil +} + +func (x *GroupServiceRemoveMemberRequest) GetUserEmail() string { + if x != nil { + return x.UserEmail + } + return "" +} + +// GroupServiceRemoveMemberResponse is returned upon successful removal of a user from a group +type GroupServiceRemoveMemberResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GroupServiceRemoveMemberResponse) Reset() { + *x = GroupServiceRemoveMemberResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_group_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GroupServiceRemoveMemberResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupServiceRemoveMemberResponse) ProtoMessage() {} + +func (x *GroupServiceRemoveMemberResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_group_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupServiceRemoveMemberResponse.ProtoReflect.Descriptor instead. +func (*GroupServiceRemoveMemberResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_group_proto_rawDescGZIP(), []int{15} +} + // Group represents a collection of users with shared access to resources type Group struct { state protoimpl.MessageState @@ -736,7 +939,7 @@ type Group struct { func (x *Group) Reset() { *x = Group{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_group_proto_msgTypes[12] + mi := &file_controlplane_v1_group_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -749,7 +952,7 @@ func (x *Group) String() string { func (*Group) ProtoMessage() {} func (x *Group) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_group_proto_msgTypes[12] + mi := &file_controlplane_v1_group_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -762,7 +965,7 @@ func (x *Group) ProtoReflect() protoreflect.Message { // Deprecated: Use Group.ProtoReflect.Descriptor instead. func (*Group) Descriptor() ([]byte, []int) { - return file_controlplane_v1_group_proto_rawDescGZIP(), []int{12} + return file_controlplane_v1_group_proto_rawDescGZIP(), []int{16} } func (x *Group) GetId() string { @@ -826,7 +1029,7 @@ type GroupMember struct { func (x *GroupMember) Reset() { *x = GroupMember{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_group_proto_msgTypes[13] + mi := &file_controlplane_v1_group_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -839,7 +1042,7 @@ func (x *GroupMember) String() string { func (*GroupMember) ProtoMessage() {} func (x *GroupMember) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_group_proto_msgTypes[13] + mi := &file_controlplane_v1_group_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -852,7 +1055,7 @@ func (x *GroupMember) ProtoReflect() protoreflect.Message { // Deprecated: Use GroupMember.ProtoReflect.Descriptor instead. func (*GroupMember) Descriptor() ([]byte, []int) { - return file_controlplane_v1_group_proto_rawDescGZIP(), []int{13} + return file_controlplane_v1_group_proto_rawDescGZIP(), []int{17} } func (x *GroupMember) GetUser() *User { @@ -895,174 +1098,231 @@ var file_controlplane_v1_group_proto_rawDesc = []byte{ 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, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5a, 0x0a, 0x19, 0x47, 0x72, 0x6f, 0x75, 0x70, - 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, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x4a, 0x0a, 0x1a, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, - 0x40, 0x0a, 0x16, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x08, 0x67, 0x72, 0x6f, - 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, - 0xc8, 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, - 0x64, 0x22, 0x47, 0x0a, 0x17, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0xf5, 0x01, 0x0a, 0x17, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, - 0x25, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x0b, - 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x48, - 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x28, 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, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, - 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, - 0x69, 0x6c, 0x22, 0x95, 0x01, 0x0a, 0x18, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2e, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 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, 0xac, 0x01, 0x0a, 0x19, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, - 0x01, 0x01, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, - 0x12, 0x1f, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, - 0xba, 0x48, 0x03, 0xd0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, - 0x01, 0x12, 0x2d, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xd0, 0x01, 0x01, 0x48, 0x01, - 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, - 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5a, 0x0a, + 0x19, 0x47, 0x72, 0x6f, 0x75, 0x70, 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, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4a, 0x0a, 0x1a, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x35, 0x0a, 0x19, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1c, 0x0a, 0x1a, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x1f, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, - 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, - 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x07, 0x6d, - 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 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, 0x92, 0x02, 0x0a, 0x1e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x72, 0x03, - 0xb0, 0x01, 0x01, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x0b, - 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x08, 0x42, 0x06, 0xba, 0x48, 0x03, 0xd0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x6d, 0x61, 0x69, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x0c, 0x6d, - 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xd0, 0x01, 0x01, 0x48, 0x01, 0x52, 0x0b, 0x6d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x48, 0x0a, 0x0a, 0x70, - 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x28, 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, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x73, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xec, 0x01, 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, - 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xd3, 0x01, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4d, - 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, - 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, - 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x73, 0x4d, 0x61, 0x69, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, - 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x32, 0xec, 0x04, 0x0a, 0x0c, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x06, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x5a, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x6d, 0x0a, 0x16, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x53, 0x0a, 0x0f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 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, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, 0x06, 0xba, 0x48, + 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, + 0x65, 0x6e, 0x63, 0x65, 0x22, 0x47, 0x0a, 0x17, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x2c, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0xf5, 0x01, + 0x0a, 0x17, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, + 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x6d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x02, 0x52, 0x0b, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x88, 0x01, + 0x01, 0x12, 0x48, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 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, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, + 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x95, 0x01, 0x0a, 0x18, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x73, 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, 0xef, 0x01, + 0x0a, 0x19, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x53, 0x0a, 0x0f, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 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, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, + 0x52, 0x0e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x12, 0x26, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xd0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x07, 0x6e, 0x65, + 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, 0x0f, 0x6e, 0x65, 0x77, 0x5f, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xd0, 0x01, 0x01, 0x48, 0x01, 0x52, 0x0e, 0x6e, 0x65, 0x77, + 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x0b, + 0x0a, 0x09, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, + 0x6e, 0x65, 0x77, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x4a, 0x0a, 0x1a, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, + 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x70, 0x0a, 0x19, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x53, 0x0a, 0x0f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 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, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x1c, 0x0a, + 0x1a, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x1f, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, + 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x36, 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x07, + 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 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, 0xbf, 0x02, 0x0a, 0x1e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x53, 0x0a, 0x0f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x72, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 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, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2d, 0x0a, 0x0b, 0x6d, 0x61, + 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xd0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x6d, 0x61, 0x69, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x0c, 0x6d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xd0, 0x01, 0x01, 0x48, 0x01, 0x52, 0x0b, 0x6d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 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, 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, 0x0e, 0x0a, 0x0c, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x73, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x65, + 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xc0, 0x01, 0x0a, 0x1c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x53, 0x0a, 0x0f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x72, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 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, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0a, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x72, 0x02, 0x60, 0x01, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x45, 0x6d, 0x61, + 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x73, 0x4d, 0x61, 0x69, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x1d, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9e, 0x01, 0x0a, 0x1f, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x53, 0x0a, 0x0f, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 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, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, + 0x01, 0x52, 0x0e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x12, 0x26, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x60, 0x01, 0x52, 0x09, + 0x75, 0x73, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x22, 0x0a, 0x20, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xec, 0x01, + 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, + 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xd3, 0x01, 0x0a, + 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x04, + 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x73, 0x5f, 0x6d, 0x61, + 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, + 0x69, 0x73, 0x4d, 0x61, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0a, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x32, 0xd1, 0x06, 0x0a, 0x0c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, + 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x28, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5d, 0x0a, - 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x28, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, - 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x63, 0x0a, 0x06, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x5d, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x28, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x63, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x63, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x63, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x2a, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x72, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, - 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, - 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 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, + 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x72, 0x0a, + 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x2f, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, + 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x6c, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2d, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, + 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x75, 0x0a, 0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, + 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 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 ( @@ -1077,59 +1337,74 @@ func file_controlplane_v1_group_proto_rawDescGZIP() []byte { return file_controlplane_v1_group_proto_rawDescData } -var file_controlplane_v1_group_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_controlplane_v1_group_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_controlplane_v1_group_proto_goTypes = []interface{}{ - (*GroupServiceCreateRequest)(nil), // 0: controlplane.v1.GroupServiceCreateRequest - (*GroupServiceCreateResponse)(nil), // 1: controlplane.v1.GroupServiceCreateResponse - (*GroupServiceGetRequest)(nil), // 2: controlplane.v1.GroupServiceGetRequest - (*GroupServiceGetResponse)(nil), // 3: controlplane.v1.GroupServiceGetResponse - (*GroupServiceListRequest)(nil), // 4: controlplane.v1.GroupServiceListRequest - (*GroupServiceListResponse)(nil), // 5: controlplane.v1.GroupServiceListResponse - (*GroupServiceUpdateRequest)(nil), // 6: controlplane.v1.GroupServiceUpdateRequest - (*GroupServiceUpdateResponse)(nil), // 7: controlplane.v1.GroupServiceUpdateResponse - (*GroupServiceDeleteRequest)(nil), // 8: controlplane.v1.GroupServiceDeleteRequest - (*GroupServiceDeleteResponse)(nil), // 9: controlplane.v1.GroupServiceDeleteResponse - (*GroupServiceListMembersResponse)(nil), // 10: controlplane.v1.GroupServiceListMembersResponse - (*GroupServiceListMembersRequest)(nil), // 11: controlplane.v1.GroupServiceListMembersRequest - (*Group)(nil), // 12: controlplane.v1.Group - (*GroupMember)(nil), // 13: controlplane.v1.GroupMember - (*OffsetPaginationRequest)(nil), // 14: controlplane.v1.OffsetPaginationRequest - (*OffsetPaginationResponse)(nil), // 15: controlplane.v1.OffsetPaginationResponse - (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp - (*User)(nil), // 17: controlplane.v1.User + (*GroupServiceCreateRequest)(nil), // 0: controlplane.v1.GroupServiceCreateRequest + (*GroupServiceCreateResponse)(nil), // 1: controlplane.v1.GroupServiceCreateResponse + (*GroupServiceGetRequest)(nil), // 2: controlplane.v1.GroupServiceGetRequest + (*GroupServiceGetResponse)(nil), // 3: controlplane.v1.GroupServiceGetResponse + (*GroupServiceListRequest)(nil), // 4: controlplane.v1.GroupServiceListRequest + (*GroupServiceListResponse)(nil), // 5: controlplane.v1.GroupServiceListResponse + (*GroupServiceUpdateRequest)(nil), // 6: controlplane.v1.GroupServiceUpdateRequest + (*GroupServiceUpdateResponse)(nil), // 7: controlplane.v1.GroupServiceUpdateResponse + (*GroupServiceDeleteRequest)(nil), // 8: controlplane.v1.GroupServiceDeleteRequest + (*GroupServiceDeleteResponse)(nil), // 9: controlplane.v1.GroupServiceDeleteResponse + (*GroupServiceListMembersResponse)(nil), // 10: controlplane.v1.GroupServiceListMembersResponse + (*GroupServiceListMembersRequest)(nil), // 11: controlplane.v1.GroupServiceListMembersRequest + (*GroupServiceAddMemberRequest)(nil), // 12: controlplane.v1.GroupServiceAddMemberRequest + (*GroupServiceAddMemberResponse)(nil), // 13: controlplane.v1.GroupServiceAddMemberResponse + (*GroupServiceRemoveMemberRequest)(nil), // 14: controlplane.v1.GroupServiceRemoveMemberRequest + (*GroupServiceRemoveMemberResponse)(nil), // 15: controlplane.v1.GroupServiceRemoveMemberResponse + (*Group)(nil), // 16: controlplane.v1.Group + (*GroupMember)(nil), // 17: controlplane.v1.GroupMember + (*IdentityReference)(nil), // 18: controlplane.v1.IdentityReference + (*OffsetPaginationRequest)(nil), // 19: controlplane.v1.OffsetPaginationRequest + (*OffsetPaginationResponse)(nil), // 20: controlplane.v1.OffsetPaginationResponse + (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp + (*User)(nil), // 22: controlplane.v1.User } var file_controlplane_v1_group_proto_depIdxs = []int32{ - 12, // 0: controlplane.v1.GroupServiceCreateResponse.group:type_name -> controlplane.v1.Group - 12, // 1: controlplane.v1.GroupServiceGetResponse.group:type_name -> controlplane.v1.Group - 14, // 2: controlplane.v1.GroupServiceListRequest.pagination:type_name -> controlplane.v1.OffsetPaginationRequest - 12, // 3: controlplane.v1.GroupServiceListResponse.groups:type_name -> controlplane.v1.Group - 15, // 4: controlplane.v1.GroupServiceListResponse.pagination:type_name -> controlplane.v1.OffsetPaginationResponse - 12, // 5: controlplane.v1.GroupServiceUpdateResponse.group:type_name -> controlplane.v1.Group - 13, // 6: controlplane.v1.GroupServiceListMembersResponse.members:type_name -> controlplane.v1.GroupMember - 15, // 7: controlplane.v1.GroupServiceListMembersResponse.pagination:type_name -> controlplane.v1.OffsetPaginationResponse - 14, // 8: controlplane.v1.GroupServiceListMembersRequest.pagination:type_name -> controlplane.v1.OffsetPaginationRequest - 16, // 9: controlplane.v1.Group.created_at:type_name -> google.protobuf.Timestamp - 16, // 10: controlplane.v1.Group.updated_at:type_name -> google.protobuf.Timestamp - 17, // 11: controlplane.v1.GroupMember.user:type_name -> controlplane.v1.User - 16, // 12: controlplane.v1.GroupMember.created_at:type_name -> google.protobuf.Timestamp - 16, // 13: controlplane.v1.GroupMember.updated_at:type_name -> google.protobuf.Timestamp - 0, // 14: controlplane.v1.GroupService.Create:input_type -> controlplane.v1.GroupServiceCreateRequest - 2, // 15: controlplane.v1.GroupService.Get:input_type -> controlplane.v1.GroupServiceGetRequest - 4, // 16: controlplane.v1.GroupService.List:input_type -> controlplane.v1.GroupServiceListRequest - 6, // 17: controlplane.v1.GroupService.Update:input_type -> controlplane.v1.GroupServiceUpdateRequest - 8, // 18: controlplane.v1.GroupService.Delete:input_type -> controlplane.v1.GroupServiceDeleteRequest - 11, // 19: controlplane.v1.GroupService.ListMembers:input_type -> controlplane.v1.GroupServiceListMembersRequest - 1, // 20: controlplane.v1.GroupService.Create:output_type -> controlplane.v1.GroupServiceCreateResponse - 3, // 21: controlplane.v1.GroupService.Get:output_type -> controlplane.v1.GroupServiceGetResponse - 5, // 22: controlplane.v1.GroupService.List:output_type -> controlplane.v1.GroupServiceListResponse - 7, // 23: controlplane.v1.GroupService.Update:output_type -> controlplane.v1.GroupServiceUpdateResponse - 9, // 24: controlplane.v1.GroupService.Delete:output_type -> controlplane.v1.GroupServiceDeleteResponse - 10, // 25: controlplane.v1.GroupService.ListMembers:output_type -> controlplane.v1.GroupServiceListMembersResponse - 20, // [20:26] is the sub-list for method output_type - 14, // [14:20] is the sub-list for method input_type - 14, // [14:14] is the sub-list for extension type_name - 14, // [14:14] is the sub-list for extension extendee - 0, // [0:14] is the sub-list for field type_name + 16, // 0: controlplane.v1.GroupServiceCreateResponse.group:type_name -> controlplane.v1.Group + 18, // 1: controlplane.v1.GroupServiceGetRequest.group_reference:type_name -> controlplane.v1.IdentityReference + 16, // 2: controlplane.v1.GroupServiceGetResponse.group:type_name -> controlplane.v1.Group + 19, // 3: controlplane.v1.GroupServiceListRequest.pagination:type_name -> controlplane.v1.OffsetPaginationRequest + 16, // 4: controlplane.v1.GroupServiceListResponse.groups:type_name -> controlplane.v1.Group + 20, // 5: controlplane.v1.GroupServiceListResponse.pagination:type_name -> controlplane.v1.OffsetPaginationResponse + 18, // 6: controlplane.v1.GroupServiceUpdateRequest.group_reference:type_name -> controlplane.v1.IdentityReference + 16, // 7: controlplane.v1.GroupServiceUpdateResponse.group:type_name -> controlplane.v1.Group + 18, // 8: controlplane.v1.GroupServiceDeleteRequest.group_reference:type_name -> controlplane.v1.IdentityReference + 17, // 9: controlplane.v1.GroupServiceListMembersResponse.members:type_name -> controlplane.v1.GroupMember + 20, // 10: controlplane.v1.GroupServiceListMembersResponse.pagination:type_name -> controlplane.v1.OffsetPaginationResponse + 18, // 11: controlplane.v1.GroupServiceListMembersRequest.group_reference:type_name -> controlplane.v1.IdentityReference + 19, // 12: controlplane.v1.GroupServiceListMembersRequest.pagination:type_name -> controlplane.v1.OffsetPaginationRequest + 18, // 13: controlplane.v1.GroupServiceAddMemberRequest.group_reference:type_name -> controlplane.v1.IdentityReference + 18, // 14: controlplane.v1.GroupServiceRemoveMemberRequest.group_reference:type_name -> controlplane.v1.IdentityReference + 21, // 15: controlplane.v1.Group.created_at:type_name -> google.protobuf.Timestamp + 21, // 16: controlplane.v1.Group.updated_at:type_name -> google.protobuf.Timestamp + 22, // 17: controlplane.v1.GroupMember.user:type_name -> controlplane.v1.User + 21, // 18: controlplane.v1.GroupMember.created_at:type_name -> google.protobuf.Timestamp + 21, // 19: controlplane.v1.GroupMember.updated_at:type_name -> google.protobuf.Timestamp + 0, // 20: controlplane.v1.GroupService.Create:input_type -> controlplane.v1.GroupServiceCreateRequest + 2, // 21: controlplane.v1.GroupService.Get:input_type -> controlplane.v1.GroupServiceGetRequest + 4, // 22: controlplane.v1.GroupService.List:input_type -> controlplane.v1.GroupServiceListRequest + 6, // 23: controlplane.v1.GroupService.Update:input_type -> controlplane.v1.GroupServiceUpdateRequest + 8, // 24: controlplane.v1.GroupService.Delete:input_type -> controlplane.v1.GroupServiceDeleteRequest + 11, // 25: controlplane.v1.GroupService.ListMembers:input_type -> controlplane.v1.GroupServiceListMembersRequest + 12, // 26: controlplane.v1.GroupService.AddMember:input_type -> controlplane.v1.GroupServiceAddMemberRequest + 14, // 27: controlplane.v1.GroupService.RemoveMember:input_type -> controlplane.v1.GroupServiceRemoveMemberRequest + 1, // 28: controlplane.v1.GroupService.Create:output_type -> controlplane.v1.GroupServiceCreateResponse + 3, // 29: controlplane.v1.GroupService.Get:output_type -> controlplane.v1.GroupServiceGetResponse + 5, // 30: controlplane.v1.GroupService.List:output_type -> controlplane.v1.GroupServiceListResponse + 7, // 31: controlplane.v1.GroupService.Update:output_type -> controlplane.v1.GroupServiceUpdateResponse + 9, // 32: controlplane.v1.GroupService.Delete:output_type -> controlplane.v1.GroupServiceDeleteResponse + 10, // 33: controlplane.v1.GroupService.ListMembers:output_type -> controlplane.v1.GroupServiceListMembersResponse + 13, // 34: controlplane.v1.GroupService.AddMember:output_type -> controlplane.v1.GroupServiceAddMemberResponse + 15, // 35: controlplane.v1.GroupService.RemoveMember:output_type -> controlplane.v1.GroupServiceRemoveMemberResponse + 28, // [28:36] is the sub-list for method output_type + 20, // [20:28] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_controlplane_v1_group_proto_init() } @@ -1139,6 +1414,7 @@ func file_controlplane_v1_group_proto_init() { } file_controlplane_v1_pagination_proto_init() file_controlplane_v1_response_messages_proto_init() + file_controlplane_v1_shared_message_proto_init() if !protoimpl.UnsafeEnabled { file_controlplane_v1_group_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GroupServiceCreateRequest); i { @@ -1285,7 +1561,7 @@ func file_controlplane_v1_group_proto_init() { } } file_controlplane_v1_group_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Group); i { + switch v := v.(*GroupServiceAddMemberRequest); i { case 0: return &v.state case 1: @@ -1297,6 +1573,54 @@ func file_controlplane_v1_group_proto_init() { } } file_controlplane_v1_group_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GroupServiceAddMemberResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_group_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GroupServiceRemoveMemberRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_group_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GroupServiceRemoveMemberResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_group_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Group); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_group_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GroupMember); i { case 0: return &v.state @@ -1318,7 +1642,7 @@ func file_controlplane_v1_group_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controlplane_v1_group_proto_rawDesc, NumEnums: 0, - NumMessages: 14, + NumMessages: 18, NumExtensions: 0, NumServices: 1, }, diff --git a/app/controlplane/api/controlplane/v1/group.proto b/app/controlplane/api/controlplane/v1/group.proto index 5125cd8c0..99b46a307 100644 --- a/app/controlplane/api/controlplane/v1/group.proto +++ b/app/controlplane/api/controlplane/v1/group.proto @@ -20,6 +20,7 @@ package controlplane.v1; import "buf/validate/validate.proto"; import "controlplane/v1/pagination.proto"; import "controlplane/v1/response_messages.proto"; +import "controlplane/v1/shared_message.proto"; import "google/protobuf/timestamp.proto"; option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1"; @@ -38,6 +39,10 @@ service GroupService { rpc Delete(GroupServiceDeleteRequest) returns (GroupServiceDeleteResponse) {} // ListMembers retrieves the members of a specific group rpc ListMembers(GroupServiceListMembersRequest) returns (GroupServiceListMembersResponse) {} + // AddMember adds a user to a group with an optional maintainer role + rpc AddMember(GroupServiceAddMemberRequest) returns (GroupServiceAddMemberResponse) {} + // RemoveMember removes a user from a group + rpc RemoveMember(GroupServiceRemoveMemberRequest) returns (GroupServiceRemoveMemberResponse) {} } // GroupServiceCreateRequest contains the information needed to create a new group @@ -56,11 +61,8 @@ message GroupServiceCreateResponse { // GroupServiceGetRequest contains the identifier for the group to retrieve message GroupServiceGetRequest { - // UUID of the group to retrieve - string group_id = 1 [ - (buf.validate.field).string.uuid = true, - (buf.validate.field).required = true - ]; + // IdentityReference is used to specify the group by either its ID or name + IdentityReference group_reference = 1 [(buf.validate.field).required = true]; } // GroupServiceGetResponse contains the requested group information @@ -91,15 +93,13 @@ message GroupServiceListResponse { // GroupServiceUpdateRequest contains the fields that can be updated for a group message GroupServiceUpdateRequest { - // UUID of the group to update - string group_id = 1 [ - (buf.validate.field).string.uuid = true, - (buf.validate.field).required = true - ]; + // IdentityReference is used to specify the group by either its ID or name + IdentityReference group_reference = 1 [(buf.validate.field).required = true]; + // New name for the group (if provided) - optional string name = 2 [(buf.validate.field).ignore_empty = true]; + optional string new_name = 3 [(buf.validate.field).ignore_empty = true]; // New description for the group (if provided) - optional string description = 3 [(buf.validate.field).ignore_empty = true]; + optional string new_description = 4 [(buf.validate.field).ignore_empty = true]; } // GroupServiceUpdateResponse contains the updated group information @@ -110,8 +110,8 @@ message GroupServiceUpdateResponse { // GroupServiceDeleteRequest contains the identifier for the group to delete message GroupServiceDeleteRequest { - // UUID of the group to delete - string id = 1 [(buf.validate.field).string.uuid = true]; + // IdentityReference is used to specify the group by either its ID or name + IdentityReference group_reference = 1 [(buf.validate.field).required = true]; } // GroupServiceDeleteResponse is returned upon successful deletion of a group @@ -126,19 +126,40 @@ message GroupServiceListMembersResponse { // GroupServiceListMembersRequest contains the identifier for the group whose members are to be listed message GroupServiceListMembersRequest { - // UUID of the group whose members are to be listed - string group_id = 1 [ - (buf.validate.field).string.uuid = true, - (buf.validate.field).required = true - ]; + // IdentityReference is used to specify the group by either its ID or name + IdentityReference group_reference = 1 [(buf.validate.field).required = true]; // Optional filter to search only by maintainers or not - optional bool maintainers = 2 [(buf.validate.field).ignore_empty = true]; + optional bool maintainers = 3 [(buf.validate.field).ignore_empty = true]; // Optional filter to search by member email address - optional string member_email = 3 [(buf.validate.field).ignore_empty = true]; + optional string member_email = 4 [(buf.validate.field).ignore_empty = true]; // Pagination parameters to limit and offset results - OffsetPaginationRequest pagination = 4; + OffsetPaginationRequest pagination = 5; +} + +// GroupServiceAddMemberRequest contains the information needed to add a user to a group +message GroupServiceAddMemberRequest { + // IdentityReference is used to specify the group by either its ID or name + IdentityReference group_reference = 1 [(buf.validate.field).required = true]; + // The user to add to the group + string user_email = 3 [(buf.validate.field).string.email = true]; + // Indicates whether the user should have maintainer (admin) privileges in the group + bool is_maintainer = 4; } +// GroupServiceAddMemberResponse contains the information about the group member that was added +message GroupServiceAddMemberResponse {} + +// GroupServiceRemoveMemberRequest contains the information needed to remove a user from a group +message GroupServiceRemoveMemberRequest { + // IdentityReference is used to specify the group by either its ID or name + IdentityReference group_reference = 1 [(buf.validate.field).required = true]; + // The user to remove from the group + string user_email = 3 [(buf.validate.field).string.email = true]; +} + +// GroupServiceRemoveMemberResponse is returned upon successful removal of a user from a group +message GroupServiceRemoveMemberResponse {} + // Group represents a collection of users with shared access to resources message Group { // Unique identifier for the group diff --git a/app/controlplane/api/controlplane/v1/group_grpc.pb.go b/app/controlplane/api/controlplane/v1/group_grpc.pb.go index cfb483a9b..4a74c2329 100644 --- a/app/controlplane/api/controlplane/v1/group_grpc.pb.go +++ b/app/controlplane/api/controlplane/v1/group_grpc.pb.go @@ -34,12 +34,14 @@ import ( const _ = grpc.SupportPackageIsVersion7 const ( - GroupService_Create_FullMethodName = "/controlplane.v1.GroupService/Create" - GroupService_Get_FullMethodName = "/controlplane.v1.GroupService/Get" - GroupService_List_FullMethodName = "/controlplane.v1.GroupService/List" - GroupService_Update_FullMethodName = "/controlplane.v1.GroupService/Update" - GroupService_Delete_FullMethodName = "/controlplane.v1.GroupService/Delete" - GroupService_ListMembers_FullMethodName = "/controlplane.v1.GroupService/ListMembers" + GroupService_Create_FullMethodName = "/controlplane.v1.GroupService/Create" + GroupService_Get_FullMethodName = "/controlplane.v1.GroupService/Get" + GroupService_List_FullMethodName = "/controlplane.v1.GroupService/List" + GroupService_Update_FullMethodName = "/controlplane.v1.GroupService/Update" + GroupService_Delete_FullMethodName = "/controlplane.v1.GroupService/Delete" + GroupService_ListMembers_FullMethodName = "/controlplane.v1.GroupService/ListMembers" + GroupService_AddMember_FullMethodName = "/controlplane.v1.GroupService/AddMember" + GroupService_RemoveMember_FullMethodName = "/controlplane.v1.GroupService/RemoveMember" ) // GroupServiceClient is the client API for GroupService service. @@ -58,6 +60,10 @@ type GroupServiceClient interface { Delete(ctx context.Context, in *GroupServiceDeleteRequest, opts ...grpc.CallOption) (*GroupServiceDeleteResponse, error) // ListMembers retrieves the members of a specific group ListMembers(ctx context.Context, in *GroupServiceListMembersRequest, opts ...grpc.CallOption) (*GroupServiceListMembersResponse, error) + // AddMember adds a user to a group with an optional maintainer role + AddMember(ctx context.Context, in *GroupServiceAddMemberRequest, opts ...grpc.CallOption) (*GroupServiceAddMemberResponse, error) + // RemoveMember removes a user from a group + RemoveMember(ctx context.Context, in *GroupServiceRemoveMemberRequest, opts ...grpc.CallOption) (*GroupServiceRemoveMemberResponse, error) } type groupServiceClient struct { @@ -122,6 +128,24 @@ func (c *groupServiceClient) ListMembers(ctx context.Context, in *GroupServiceLi return out, nil } +func (c *groupServiceClient) AddMember(ctx context.Context, in *GroupServiceAddMemberRequest, opts ...grpc.CallOption) (*GroupServiceAddMemberResponse, error) { + out := new(GroupServiceAddMemberResponse) + err := c.cc.Invoke(ctx, GroupService_AddMember_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) RemoveMember(ctx context.Context, in *GroupServiceRemoveMemberRequest, opts ...grpc.CallOption) (*GroupServiceRemoveMemberResponse, error) { + out := new(GroupServiceRemoveMemberResponse) + err := c.cc.Invoke(ctx, GroupService_RemoveMember_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // GroupServiceServer is the server API for GroupService service. // All implementations must embed UnimplementedGroupServiceServer // for forward compatibility @@ -138,6 +162,10 @@ type GroupServiceServer interface { Delete(context.Context, *GroupServiceDeleteRequest) (*GroupServiceDeleteResponse, error) // ListMembers retrieves the members of a specific group ListMembers(context.Context, *GroupServiceListMembersRequest) (*GroupServiceListMembersResponse, error) + // AddMember adds a user to a group with an optional maintainer role + AddMember(context.Context, *GroupServiceAddMemberRequest) (*GroupServiceAddMemberResponse, error) + // RemoveMember removes a user from a group + RemoveMember(context.Context, *GroupServiceRemoveMemberRequest) (*GroupServiceRemoveMemberResponse, error) mustEmbedUnimplementedGroupServiceServer() } @@ -163,6 +191,12 @@ func (UnimplementedGroupServiceServer) Delete(context.Context, *GroupServiceDele func (UnimplementedGroupServiceServer) ListMembers(context.Context, *GroupServiceListMembersRequest) (*GroupServiceListMembersResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListMembers not implemented") } +func (UnimplementedGroupServiceServer) AddMember(context.Context, *GroupServiceAddMemberRequest) (*GroupServiceAddMemberResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddMember not implemented") +} +func (UnimplementedGroupServiceServer) RemoveMember(context.Context, *GroupServiceRemoveMemberRequest) (*GroupServiceRemoveMemberResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveMember not implemented") +} func (UnimplementedGroupServiceServer) mustEmbedUnimplementedGroupServiceServer() {} // UnsafeGroupServiceServer may be embedded to opt out of forward compatibility for this service. @@ -284,6 +318,42 @@ func _GroupService_ListMembers_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _GroupService_AddMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GroupServiceAddMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).AddMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_AddMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).AddMember(ctx, req.(*GroupServiceAddMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_RemoveMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GroupServiceRemoveMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).RemoveMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_RemoveMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).RemoveMember(ctx, req.(*GroupServiceRemoveMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + // GroupService_ServiceDesc is the grpc.ServiceDesc for GroupService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -315,6 +385,14 @@ var GroupService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListMembers", Handler: _GroupService_ListMembers_Handler, }, + { + MethodName: "AddMember", + Handler: _GroupService_AddMember_Handler, + }, + { + MethodName: "RemoveMember", + Handler: _GroupService_RemoveMember_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "controlplane/v1/group.proto", diff --git a/app/controlplane/api/controlplane/v1/response_messages.pb.go b/app/controlplane/api/controlplane/v1/response_messages.pb.go index dea53eeb4..1bdc45e1e 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.pb.go +++ b/app/controlplane/api/controlplane/v1/response_messages.pb.go @@ -1946,88 +1946,6 @@ func (x *CASBackendItem) GetIsInline() bool { return false } -// EntityRef is a reference to an entity in the system that can be either by name or ID -type EntityRef struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to Ref: - // - // *EntityRef_EntityId - // *EntityRef_EntityName - Ref isEntityRef_Ref `protobuf_oneof:"ref"` -} - -func (x *EntityRef) Reset() { - *x = EntityRef{} - if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *EntityRef) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EntityRef) ProtoMessage() {} - -func (x *EntityRef) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use EntityRef.ProtoReflect.Descriptor instead. -func (*EntityRef) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{15} -} - -func (m *EntityRef) GetRef() isEntityRef_Ref { - if m != nil { - return m.Ref - } - return nil -} - -func (x *EntityRef) GetEntityId() string { - if x, ok := x.GetRef().(*EntityRef_EntityId); ok { - return x.EntityId - } - return "" -} - -func (x *EntityRef) GetEntityName() string { - if x, ok := x.GetRef().(*EntityRef_EntityName); ok { - return x.EntityName - } - return "" -} - -type isEntityRef_Ref interface { - isEntityRef_Ref() -} - -type EntityRef_EntityId struct { - EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3,oneof"` -} - -type EntityRef_EntityName struct { - EntityName string `protobuf:"bytes,2,opt,name=entity_name,json=entityName,proto3,oneof"` -} - -func (*EntityRef_EntityId) isEntityRef_Ref() {} - -func (*EntityRef_EntityName) isEntityRef_Ref() {} - type APITokenItem struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2048,7 +1966,7 @@ type APITokenItem struct { func (x *APITokenItem) Reset() { *x = APITokenItem{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[16] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2061,7 +1979,7 @@ func (x *APITokenItem) String() string { func (*APITokenItem) ProtoMessage() {} func (x *APITokenItem) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[16] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2074,7 +1992,7 @@ func (x *APITokenItem) ProtoReflect() protoreflect.Message { // Deprecated: Use APITokenItem.ProtoReflect.Descriptor instead. func (*APITokenItem) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{16} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{15} } func (x *APITokenItem) GetId() string { @@ -2161,7 +2079,7 @@ type AttestationItem_PolicyEvaluationStatus struct { func (x *AttestationItem_PolicyEvaluationStatus) Reset() { *x = AttestationItem_PolicyEvaluationStatus{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[19] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2174,7 +2092,7 @@ func (x *AttestationItem_PolicyEvaluationStatus) String() string { func (*AttestationItem_PolicyEvaluationStatus) ProtoMessage() {} func (x *AttestationItem_PolicyEvaluationStatus) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[19] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2230,7 +2148,7 @@ type AttestationItem_EnvVariable struct { func (x *AttestationItem_EnvVariable) Reset() { *x = AttestationItem_EnvVariable{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[20] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2243,7 +2161,7 @@ func (x *AttestationItem_EnvVariable) String() string { func (*AttestationItem_EnvVariable) ProtoMessage() {} func (x *AttestationItem_EnvVariable) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[20] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2298,7 +2216,7 @@ type AttestationItem_Material struct { func (x *AttestationItem_Material) Reset() { *x = AttestationItem_Material{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[21] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2311,7 +2229,7 @@ func (x *AttestationItem_Material) String() string { func (*AttestationItem_Material) ProtoMessage() {} func (x *AttestationItem_Material) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[21] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2402,7 +2320,7 @@ type WorkflowContractVersionItem_RawBody struct { func (x *WorkflowContractVersionItem_RawBody) Reset() { *x = WorkflowContractVersionItem_RawBody{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[26] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2415,7 +2333,7 @@ func (x *WorkflowContractVersionItem_RawBody) String() string { func (*WorkflowContractVersionItem_RawBody) ProtoMessage() {} func (x *WorkflowContractVersionItem_RawBody) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[26] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2457,7 +2375,7 @@ type CASBackendItem_Limits struct { func (x *CASBackendItem_Limits) Reset() { *x = CASBackendItem_Limits{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[27] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2470,7 +2388,7 @@ func (x *CASBackendItem_Limits) String() string { func (*CASBackendItem_Limits) ProtoMessage() {} func (x *CASBackendItem_Limits) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[27] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2914,104 +2832,90 @@ var file_controlplane_v1_response_messages_proto_rawDesc = []byte{ 0x12, 0x18, 0x0a, 0x14, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x4b, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x02, 0x22, 0xde, 0x01, 0x0a, 0x09, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x66, 0x12, 0x27, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, - 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, - 0x12, 0xa0, 0x01, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x7d, 0xba, 0x48, 0x7a, 0xba, 0x01, 0x77, 0x0a, 0x08, - 0x64, 0x6e, 0x73, 0x2d, 0x31, 0x31, 0x32, 0x33, 0x12, 0x3a, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x6c, 0x6f, 0x77, 0x65, - 0x72, 0x63, 0x61, 0x73, 0x65, 0x20, 0x6c, 0x65, 0x74, 0x74, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x6e, - 0x75, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x68, 0x79, 0x70, 0x68, - 0x65, 0x6e, 0x73, 0x2e, 0x1a, 0x2f, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, - 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x2d, - 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, - 0x29, 0x3f, 0x24, 0x27, 0x29, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4e, - 0x61, 0x6d, 0x65, 0x42, 0x05, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x22, 0x9d, 0x03, 0x0a, 0x0c, 0x41, - 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, - 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x72, - 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x5f, - 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, - 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x2a, 0xa6, 0x01, 0x0a, 0x09, 0x52, - 0x75, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, 0x16, 0x52, 0x55, 0x4e, 0x5f, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, - 0x12, 0x18, 0x0a, 0x14, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, - 0x55, 0x43, 0x43, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x52, 0x55, - 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, - 0x03, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x55, 0x4e, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, - 0x44, 0x10, 0x05, 0x2a, 0xaf, 0x01, 0x0a, 0x0e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, - 0x69, 0x70, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, - 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x45, 0x4d, 0x42, 0x45, - 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x56, - 0x49, 0x45, 0x57, 0x45, 0x52, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x4d, 0x45, 0x4d, 0x42, 0x45, - 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x41, - 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, - 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x4f, 0x57, - 0x4e, 0x45, 0x52, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, - 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x4d, 0x45, 0x4d, - 0x42, 0x45, 0x52, 0x10, 0x04, 0x2a, 0x60, 0x0a, 0x0e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x4c, 0x69, - 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x4c, 0x4c, 0x4f, 0x57, - 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x26, 0x0a, 0x1c, 0x41, 0x4c, 0x4c, - 0x4f, 0x57, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, - 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, - 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x6d, 0x0a, 0x12, 0x46, 0x65, 0x64, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x64, 0x41, 0x75, 0x74, 0x68, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x24, 0x0a, - 0x20, 0x46, 0x45, 0x44, 0x45, 0x52, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x2b, 0x0a, 0x21, 0x46, 0x45, 0x44, 0x45, 0x52, 0x41, 0x54, 0x45, 0x44, - 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x41, 0x55, - 0x54, 0x48, 0x4f, 0x52, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, - 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x84, 0x01, 0x0a, 0x19, 0x55, 0x73, 0x65, 0x72, 0x57, - 0x69, 0x74, 0x68, 0x4e, 0x6f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x29, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x57, 0x49, 0x54, - 0x48, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x32, 0x0a, 0x28, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x57, 0x49, 0x54, 0x48, + 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x02, 0x22, 0x9d, 0x03, 0x0a, 0x0c, 0x41, 0x50, + 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, + 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x72, 0x67, + 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, + 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x2a, 0xa6, 0x01, 0x0a, 0x09, 0x52, 0x75, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, 0x16, 0x52, 0x55, 0x4e, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, 0x12, + 0x18, 0x0a, 0x14, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, + 0x43, 0x43, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x52, 0x55, 0x4e, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, + 0x12, 0x16, 0x0a, 0x12, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, + 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x55, 0x4e, 0x5f, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, + 0x10, 0x05, 0x2a, 0xaf, 0x01, 0x0a, 0x0e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, + 0x70, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, + 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, + 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x56, 0x49, + 0x45, 0x57, 0x45, 0x52, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, + 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x41, 0x44, + 0x4d, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, + 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x4f, 0x57, 0x4e, + 0x45, 0x52, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, + 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x4d, 0x45, 0x4d, 0x42, + 0x45, 0x52, 0x10, 0x04, 0x2a, 0x60, 0x0a, 0x0e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x4c, 0x69, 0x73, + 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x4c, 0x4c, 0x4f, 0x57, 0x5f, + 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x26, 0x0a, 0x1c, 0x41, 0x4c, 0x4c, 0x4f, + 0x57, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, 0x54, + 0x5f, 0x49, 0x4e, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, + 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x6d, 0x0a, 0x12, 0x46, 0x65, 0x64, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x64, 0x41, 0x75, 0x74, 0x68, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x24, 0x0a, 0x20, + 0x46, 0x45, 0x44, 0x45, 0x52, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x2b, 0x0a, 0x21, 0x46, 0x45, 0x44, 0x45, 0x52, 0x41, 0x54, 0x45, 0x44, 0x5f, + 0x41, 0x55, 0x54, 0x48, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, + 0x48, 0x4f, 0x52, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, + 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x84, 0x01, 0x0a, 0x19, 0x55, 0x73, 0x65, 0x72, 0x57, 0x69, + 0x74, 0x68, 0x4e, 0x6f, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x29, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x45, - 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4f, 0x52, 0x47, 0x10, - 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x80, 0x01, - 0x0a, 0x17, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4f, - 0x66, 0x4f, 0x72, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x28, 0x55, 0x53, 0x45, - 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x4f, 0x46, 0x5f, - 0x4f, 0x52, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x31, 0x0a, 0x27, 0x55, 0x53, 0x45, 0x52, 0x5f, - 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x4f, 0x46, 0x5f, 0x4f, 0x52, - 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4f, - 0x52, 0x47, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, - 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, + 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x32, 0x0a, 0x28, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, + 0x4e, 0x4f, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4f, 0x52, 0x47, 0x10, 0x01, + 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 0x2a, 0x80, 0x01, 0x0a, + 0x17, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, + 0x4f, 0x72, 0x67, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x28, 0x55, 0x53, 0x45, 0x52, + 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x4f, 0x46, 0x5f, 0x4f, + 0x52, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x31, 0x0a, 0x27, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x4e, + 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x4f, 0x46, 0x5f, 0x4f, 0x52, 0x47, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x4f, 0x52, + 0x47, 0x10, 0x01, 0x1a, 0x04, 0xa8, 0x45, 0x93, 0x03, 0x1a, 0x04, 0xa0, 0x45, 0xf4, 0x03, 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 ( @@ -3027,7 +2931,7 @@ func file_controlplane_v1_response_messages_proto_rawDescGZIP() []byte { } var file_controlplane_v1_response_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_controlplane_v1_response_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_controlplane_v1_response_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_controlplane_v1_response_messages_proto_goTypes = []interface{}{ (RunStatus)(0), // 0: controlplane.v1.RunStatus (MembershipRole)(0), // 1: controlplane.v1.MembershipRole @@ -3053,70 +2957,69 @@ var file_controlplane_v1_response_messages_proto_goTypes = []interface{}{ (*OrgMembershipItem)(nil), // 21: controlplane.v1.OrgMembershipItem (*OrgItem)(nil), // 22: controlplane.v1.OrgItem (*CASBackendItem)(nil), // 23: controlplane.v1.CASBackendItem - (*EntityRef)(nil), // 24: controlplane.v1.EntityRef - (*APITokenItem)(nil), // 25: controlplane.v1.APITokenItem - nil, // 26: controlplane.v1.AttestationItem.AnnotationsEntry - nil, // 27: controlplane.v1.AttestationItem.PolicyEvaluationsEntry - (*AttestationItem_PolicyEvaluationStatus)(nil), // 28: controlplane.v1.AttestationItem.PolicyEvaluationStatus - (*AttestationItem_EnvVariable)(nil), // 29: controlplane.v1.AttestationItem.EnvVariable - (*AttestationItem_Material)(nil), // 30: controlplane.v1.AttestationItem.Material - nil, // 31: controlplane.v1.AttestationItem.Material.AnnotationsEntry - nil, // 32: controlplane.v1.PolicyEvaluation.AnnotationsEntry - nil, // 33: controlplane.v1.PolicyEvaluation.WithEntry - nil, // 34: controlplane.v1.PolicyReference.DigestEntry - (*WorkflowContractVersionItem_RawBody)(nil), // 35: controlplane.v1.WorkflowContractVersionItem.RawBody - (*CASBackendItem_Limits)(nil), // 36: controlplane.v1.CASBackendItem.Limits - (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp - (v1.CraftingSchema_Runner_RunnerType)(0), // 38: workflowcontract.v1.CraftingSchema.Runner.RunnerType - (*v1.CraftingSchema)(nil), // 39: workflowcontract.v1.CraftingSchema + (*APITokenItem)(nil), // 24: controlplane.v1.APITokenItem + nil, // 25: controlplane.v1.AttestationItem.AnnotationsEntry + nil, // 26: controlplane.v1.AttestationItem.PolicyEvaluationsEntry + (*AttestationItem_PolicyEvaluationStatus)(nil), // 27: controlplane.v1.AttestationItem.PolicyEvaluationStatus + (*AttestationItem_EnvVariable)(nil), // 28: controlplane.v1.AttestationItem.EnvVariable + (*AttestationItem_Material)(nil), // 29: controlplane.v1.AttestationItem.Material + nil, // 30: controlplane.v1.AttestationItem.Material.AnnotationsEntry + nil, // 31: controlplane.v1.PolicyEvaluation.AnnotationsEntry + nil, // 32: controlplane.v1.PolicyEvaluation.WithEntry + nil, // 33: controlplane.v1.PolicyReference.DigestEntry + (*WorkflowContractVersionItem_RawBody)(nil), // 34: controlplane.v1.WorkflowContractVersionItem.RawBody + (*CASBackendItem_Limits)(nil), // 35: controlplane.v1.CASBackendItem.Limits + (*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp + (v1.CraftingSchema_Runner_RunnerType)(0), // 37: workflowcontract.v1.CraftingSchema.Runner.RunnerType + (*v1.CraftingSchema)(nil), // 38: workflowcontract.v1.CraftingSchema } var file_controlplane_v1_response_messages_proto_depIdxs = []int32{ - 37, // 0: controlplane.v1.WorkflowItem.created_at:type_name -> google.protobuf.Timestamp + 36, // 0: controlplane.v1.WorkflowItem.created_at:type_name -> google.protobuf.Timestamp 10, // 1: controlplane.v1.WorkflowItem.last_run:type_name -> controlplane.v1.WorkflowRunItem - 37, // 2: controlplane.v1.WorkflowRunItem.created_at:type_name -> google.protobuf.Timestamp - 37, // 3: controlplane.v1.WorkflowRunItem.finished_at:type_name -> google.protobuf.Timestamp + 36, // 2: controlplane.v1.WorkflowRunItem.created_at:type_name -> google.protobuf.Timestamp + 36, // 3: controlplane.v1.WorkflowRunItem.finished_at:type_name -> google.protobuf.Timestamp 0, // 4: controlplane.v1.WorkflowRunItem.status:type_name -> controlplane.v1.RunStatus 9, // 5: controlplane.v1.WorkflowRunItem.workflow:type_name -> controlplane.v1.WorkflowItem - 38, // 6: controlplane.v1.WorkflowRunItem.runner_type:type_name -> workflowcontract.v1.CraftingSchema.Runner.RunnerType + 37, // 6: controlplane.v1.WorkflowRunItem.runner_type:type_name -> workflowcontract.v1.CraftingSchema.Runner.RunnerType 19, // 7: controlplane.v1.WorkflowRunItem.contract_version:type_name -> controlplane.v1.WorkflowContractVersionItem 11, // 8: controlplane.v1.WorkflowRunItem.version:type_name -> controlplane.v1.ProjectVersion - 37, // 9: controlplane.v1.ProjectVersion.created_at:type_name -> google.protobuf.Timestamp - 37, // 10: controlplane.v1.ProjectVersion.released_at:type_name -> google.protobuf.Timestamp - 29, // 11: controlplane.v1.AttestationItem.env_vars:type_name -> controlplane.v1.AttestationItem.EnvVariable - 30, // 12: controlplane.v1.AttestationItem.materials:type_name -> controlplane.v1.AttestationItem.Material - 26, // 13: controlplane.v1.AttestationItem.annotations:type_name -> controlplane.v1.AttestationItem.AnnotationsEntry - 27, // 14: controlplane.v1.AttestationItem.policy_evaluations:type_name -> controlplane.v1.AttestationItem.PolicyEvaluationsEntry - 28, // 15: controlplane.v1.AttestationItem.policy_evaluation_status:type_name -> controlplane.v1.AttestationItem.PolicyEvaluationStatus + 36, // 9: controlplane.v1.ProjectVersion.created_at:type_name -> google.protobuf.Timestamp + 36, // 10: controlplane.v1.ProjectVersion.released_at:type_name -> google.protobuf.Timestamp + 28, // 11: controlplane.v1.AttestationItem.env_vars:type_name -> controlplane.v1.AttestationItem.EnvVariable + 29, // 12: controlplane.v1.AttestationItem.materials:type_name -> controlplane.v1.AttestationItem.Material + 25, // 13: controlplane.v1.AttestationItem.annotations:type_name -> controlplane.v1.AttestationItem.AnnotationsEntry + 26, // 14: controlplane.v1.AttestationItem.policy_evaluations:type_name -> controlplane.v1.AttestationItem.PolicyEvaluationsEntry + 27, // 15: controlplane.v1.AttestationItem.policy_evaluation_status:type_name -> controlplane.v1.AttestationItem.PolicyEvaluationStatus 14, // 16: controlplane.v1.PolicyEvaluations.evaluations:type_name -> controlplane.v1.PolicyEvaluation - 32, // 17: controlplane.v1.PolicyEvaluation.annotations:type_name -> controlplane.v1.PolicyEvaluation.AnnotationsEntry - 33, // 18: controlplane.v1.PolicyEvaluation.with:type_name -> controlplane.v1.PolicyEvaluation.WithEntry + 31, // 17: controlplane.v1.PolicyEvaluation.annotations:type_name -> controlplane.v1.PolicyEvaluation.AnnotationsEntry + 32, // 18: controlplane.v1.PolicyEvaluation.with:type_name -> controlplane.v1.PolicyEvaluation.WithEntry 15, // 19: controlplane.v1.PolicyEvaluation.violations:type_name -> controlplane.v1.PolicyViolation 16, // 20: controlplane.v1.PolicyEvaluation.policy_reference:type_name -> controlplane.v1.PolicyReference 16, // 21: controlplane.v1.PolicyEvaluation.group_reference:type_name -> controlplane.v1.PolicyReference - 34, // 22: controlplane.v1.PolicyReference.digest:type_name -> controlplane.v1.PolicyReference.DigestEntry - 37, // 23: controlplane.v1.WorkflowContractItem.created_at:type_name -> google.protobuf.Timestamp - 37, // 24: controlplane.v1.WorkflowContractItem.latest_revision_created_at:type_name -> google.protobuf.Timestamp + 33, // 22: controlplane.v1.PolicyReference.digest:type_name -> controlplane.v1.PolicyReference.DigestEntry + 36, // 23: controlplane.v1.WorkflowContractItem.created_at:type_name -> google.protobuf.Timestamp + 36, // 24: controlplane.v1.WorkflowContractItem.latest_revision_created_at:type_name -> google.protobuf.Timestamp 18, // 25: controlplane.v1.WorkflowContractItem.workflow_refs:type_name -> controlplane.v1.WorkflowRef - 37, // 26: controlplane.v1.WorkflowContractVersionItem.created_at:type_name -> google.protobuf.Timestamp - 39, // 27: controlplane.v1.WorkflowContractVersionItem.v1:type_name -> workflowcontract.v1.CraftingSchema - 35, // 28: controlplane.v1.WorkflowContractVersionItem.raw_contract:type_name -> controlplane.v1.WorkflowContractVersionItem.RawBody - 37, // 29: controlplane.v1.User.created_at:type_name -> google.protobuf.Timestamp + 36, // 26: controlplane.v1.WorkflowContractVersionItem.created_at:type_name -> google.protobuf.Timestamp + 38, // 27: controlplane.v1.WorkflowContractVersionItem.v1:type_name -> workflowcontract.v1.CraftingSchema + 34, // 28: controlplane.v1.WorkflowContractVersionItem.raw_contract:type_name -> controlplane.v1.WorkflowContractVersionItem.RawBody + 36, // 29: controlplane.v1.User.created_at:type_name -> google.protobuf.Timestamp 22, // 30: controlplane.v1.OrgMembershipItem.org:type_name -> controlplane.v1.OrgItem 20, // 31: controlplane.v1.OrgMembershipItem.user:type_name -> controlplane.v1.User - 37, // 32: controlplane.v1.OrgMembershipItem.created_at:type_name -> google.protobuf.Timestamp - 37, // 33: controlplane.v1.OrgMembershipItem.updated_at:type_name -> google.protobuf.Timestamp + 36, // 32: controlplane.v1.OrgMembershipItem.created_at:type_name -> google.protobuf.Timestamp + 36, // 33: controlplane.v1.OrgMembershipItem.updated_at:type_name -> google.protobuf.Timestamp 1, // 34: controlplane.v1.OrgMembershipItem.role:type_name -> controlplane.v1.MembershipRole - 37, // 35: controlplane.v1.OrgItem.created_at:type_name -> google.protobuf.Timestamp + 36, // 35: controlplane.v1.OrgItem.created_at:type_name -> google.protobuf.Timestamp 7, // 36: controlplane.v1.OrgItem.default_policy_violation_strategy:type_name -> controlplane.v1.OrgItem.PolicyViolationBlockingStrategy - 37, // 37: controlplane.v1.CASBackendItem.created_at:type_name -> google.protobuf.Timestamp - 37, // 38: controlplane.v1.CASBackendItem.validated_at:type_name -> google.protobuf.Timestamp + 36, // 37: controlplane.v1.CASBackendItem.created_at:type_name -> google.protobuf.Timestamp + 36, // 38: controlplane.v1.CASBackendItem.validated_at:type_name -> google.protobuf.Timestamp 8, // 39: controlplane.v1.CASBackendItem.validation_status:type_name -> controlplane.v1.CASBackendItem.ValidationStatus - 36, // 40: controlplane.v1.CASBackendItem.limits:type_name -> controlplane.v1.CASBackendItem.Limits - 37, // 41: controlplane.v1.APITokenItem.created_at:type_name -> google.protobuf.Timestamp - 37, // 42: controlplane.v1.APITokenItem.revoked_at:type_name -> google.protobuf.Timestamp - 37, // 43: controlplane.v1.APITokenItem.expires_at:type_name -> google.protobuf.Timestamp + 35, // 40: controlplane.v1.CASBackendItem.limits:type_name -> controlplane.v1.CASBackendItem.Limits + 36, // 41: controlplane.v1.APITokenItem.created_at:type_name -> google.protobuf.Timestamp + 36, // 42: controlplane.v1.APITokenItem.revoked_at:type_name -> google.protobuf.Timestamp + 36, // 43: controlplane.v1.APITokenItem.expires_at:type_name -> google.protobuf.Timestamp 13, // 44: controlplane.v1.AttestationItem.PolicyEvaluationsEntry.value:type_name -> controlplane.v1.PolicyEvaluations - 31, // 45: controlplane.v1.AttestationItem.Material.annotations:type_name -> controlplane.v1.AttestationItem.Material.AnnotationsEntry + 30, // 45: controlplane.v1.AttestationItem.Material.annotations:type_name -> controlplane.v1.AttestationItem.Material.AnnotationsEntry 6, // 46: controlplane.v1.WorkflowContractVersionItem.RawBody.format:type_name -> controlplane.v1.WorkflowContractVersionItem.RawBody.Format 47, // [47:47] is the sub-list for method output_type 47, // [47:47] is the sub-list for method input_type @@ -3312,18 +3215,6 @@ func file_controlplane_v1_response_messages_proto_init() { } } file_controlplane_v1_response_messages_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EntityRef); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_controlplane_v1_response_messages_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*APITokenItem); i { case 0: return &v.state @@ -3335,7 +3226,7 @@ func file_controlplane_v1_response_messages_proto_init() { return nil } } - file_controlplane_v1_response_messages_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_controlplane_v1_response_messages_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AttestationItem_PolicyEvaluationStatus); i { case 0: return &v.state @@ -3347,7 +3238,7 @@ func file_controlplane_v1_response_messages_proto_init() { return nil } } - file_controlplane_v1_response_messages_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_controlplane_v1_response_messages_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AttestationItem_EnvVariable); i { case 0: return &v.state @@ -3359,7 +3250,7 @@ func file_controlplane_v1_response_messages_proto_init() { return nil } } - file_controlplane_v1_response_messages_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + file_controlplane_v1_response_messages_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AttestationItem_Material); i { case 0: return &v.state @@ -3371,7 +3262,7 @@ func file_controlplane_v1_response_messages_proto_init() { return nil } } - file_controlplane_v1_response_messages_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + file_controlplane_v1_response_messages_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*WorkflowContractVersionItem_RawBody); i { case 0: return &v.state @@ -3383,7 +3274,7 @@ func file_controlplane_v1_response_messages_proto_init() { return nil } } - file_controlplane_v1_response_messages_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + file_controlplane_v1_response_messages_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CASBackendItem_Limits); i { case 0: return &v.state @@ -3399,17 +3290,13 @@ func file_controlplane_v1_response_messages_proto_init() { file_controlplane_v1_response_messages_proto_msgTypes[10].OneofWrappers = []interface{}{ (*WorkflowContractVersionItem_V1)(nil), } - file_controlplane_v1_response_messages_proto_msgTypes[15].OneofWrappers = []interface{}{ - (*EntityRef_EntityId)(nil), - (*EntityRef_EntityName)(nil), - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controlplane_v1_response_messages_proto_rawDesc, NumEnums: 9, - NumMessages: 28, + NumMessages: 27, NumExtensions: 0, NumServices: 0, }, diff --git a/app/controlplane/api/controlplane/v1/response_messages.proto b/app/controlplane/api/controlplane/v1/response_messages.proto index c4d8d25ab..4c1778030 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.proto +++ b/app/controlplane/api/controlplane/v1/response_messages.proto @@ -312,21 +312,6 @@ enum UserNotMemberOfOrgError { USER_NOT_MEMBER_OF_ORG_ERROR_NOT_IN_ORG = 1 [(errors.code) = 403]; } -// EntityRef is a reference to an entity in the system that can be either by name or ID -message EntityRef { - oneof ref { - string entity_id = 1 [(buf.validate.field).string.uuid = true]; - string entity_name = 2 [(buf.validate.field) = { - // NOTE: validations can not be shared yet https://github.com/bufbuild/protovalidate/issues/51 - cel: { - message: "must contain only lowercase letters, numbers, and hyphens." - expression: "this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')" - id: "dns-1123" - } - }]; - } -} - message APITokenItem { string id = 1; string name = 7; diff --git a/app/controlplane/api/controlplane/v1/shared_message.pb.go b/app/controlplane/api/controlplane/v1/shared_message.pb.go new file mode 100644 index 000000000..8f5cedb7c --- /dev/null +++ b/app/controlplane/api/controlplane/v1/shared_message.pb.go @@ -0,0 +1,192 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: controlplane/v1/shared_message.proto + +package v1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// IdentityReference represents a reference to an identity in the system. +type IdentityReference struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ID is optional, but if provided, it must be a valid UUID. + Id *string `protobuf:"bytes,1,opt,name=id,proto3,oneof" json:"id,omitempty"` + // Name is optional, but if provided, it must be a non-empty string. + Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"` +} + +func (x *IdentityReference) Reset() { + *x = IdentityReference{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_shared_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IdentityReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdentityReference) ProtoMessage() {} + +func (x *IdentityReference) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_shared_message_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdentityReference.ProtoReflect.Descriptor instead. +func (*IdentityReference) Descriptor() ([]byte, []int) { + return file_controlplane_v1_shared_message_proto_rawDescGZIP(), []int{0} +} + +func (x *IdentityReference) GetId() string { + if x != nil && x.Id != nil { + return *x.Id + } + return "" +} + +func (x *IdentityReference) GetName() string { + if x != nil && x.Name != nil { + return *x.Name + } + return "" +} + +var File_controlplane_v1_shared_message_proto protoreflect.FileDescriptor + +var file_controlplane_v1_shared_message_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x2e, 0x70, 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, 0x22, 0x88, 0x02, 0x0a, 0x11, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0xba, 0x48, 0x08, 0xd0, 0x01, 0x01, 0x72, 0x03, + 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xd0, + 0x01, 0x01, 0x72, 0x02, 0x10, 0x01, 0x48, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, + 0x01, 0x3a, 0x9b, 0x01, 0xba, 0x48, 0x97, 0x01, 0x1a, 0x94, 0x01, 0x0a, 0x13, 0x69, 0x64, 0x5f, + 0x6f, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, + 0x12, 0x31, 0x65, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x69, 0x64, 0x20, 0x6f, 0x72, 0x20, 0x6e, + 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x64, 0x2c, 0x20, 0x62, 0x75, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x6f, + 0x74, 0x68, 0x2e, 0x1a, 0x4a, 0x21, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x64, 0x20, 0x3d, + 0x3d, 0x20, 0x27, 0x27, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x61, 0x6d, + 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x26, 0x26, 0x20, 0x21, 0x28, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x26, 0x26, 0x20, 0x74, + 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x42, + 0x05, 0x0a, 0x03, 0x5f, 0x69, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x61, 0x6d, 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 ( + file_controlplane_v1_shared_message_proto_rawDescOnce sync.Once + file_controlplane_v1_shared_message_proto_rawDescData = file_controlplane_v1_shared_message_proto_rawDesc +) + +func file_controlplane_v1_shared_message_proto_rawDescGZIP() []byte { + file_controlplane_v1_shared_message_proto_rawDescOnce.Do(func() { + file_controlplane_v1_shared_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_controlplane_v1_shared_message_proto_rawDescData) + }) + return file_controlplane_v1_shared_message_proto_rawDescData +} + +var file_controlplane_v1_shared_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controlplane_v1_shared_message_proto_goTypes = []interface{}{ + (*IdentityReference)(nil), // 0: controlplane.v1.IdentityReference +} +var file_controlplane_v1_shared_message_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_controlplane_v1_shared_message_proto_init() } +func file_controlplane_v1_shared_message_proto_init() { + if File_controlplane_v1_shared_message_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controlplane_v1_shared_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IdentityReference); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_controlplane_v1_shared_message_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controlplane_v1_shared_message_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controlplane_v1_shared_message_proto_goTypes, + DependencyIndexes: file_controlplane_v1_shared_message_proto_depIdxs, + MessageInfos: file_controlplane_v1_shared_message_proto_msgTypes, + }.Build() + File_controlplane_v1_shared_message_proto = out.File + file_controlplane_v1_shared_message_proto_rawDesc = nil + file_controlplane_v1_shared_message_proto_goTypes = nil + file_controlplane_v1_shared_message_proto_depIdxs = nil +} diff --git a/app/controlplane/api/controlplane/v1/shared_message.proto b/app/controlplane/api/controlplane/v1/shared_message.proto new file mode 100644 index 000000000..a7df1113d --- /dev/null +++ b/app/controlplane/api/controlplane/v1/shared_message.proto @@ -0,0 +1,43 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package controlplane.v1; + +import "buf/validate/validate.proto"; + +option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1"; + +// IdentityReference represents a reference to an identity in the system. +message IdentityReference { + // ID is optional, but if provided, it must be a valid UUID. + optional string id = 1 [ + (buf.validate.field).string.uuid = true, + (buf.validate.field).ignore_empty = true + ]; + // Name is optional, but if provided, it must be a non-empty string. + optional string name = 2 [ + (buf.validate.field).string.min_len = 1, + (buf.validate.field).ignore_empty = true + ]; + + // Custom validation to ensure that either id or name is provided + option (buf.validate.message).cel = { + id: "id_or_name_required" + expression: "!(this.id == '' && this.name == '') && !(this.id != '' && this.name != '')" + message: "either id or name must be provided, but not both." + }; +} diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/group.ts b/app/controlplane/api/gen/frontend/controlplane/v1/group.ts index 9b1e1fcd6..9eb51d998 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/group.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/group.ts @@ -5,6 +5,7 @@ import _m0 from "protobufjs/minimal"; import { Timestamp } from "../../google/protobuf/timestamp"; import { OffsetPaginationRequest, OffsetPaginationResponse } from "./pagination"; import { User } from "./response_messages"; +import { IdentityReference } from "./shared_message"; export const protobufPackage = "controlplane.v1"; @@ -24,8 +25,8 @@ export interface GroupServiceCreateResponse { /** GroupServiceGetRequest contains the identifier for the group to retrieve */ export interface GroupServiceGetRequest { - /** UUID of the group to retrieve */ - groupId: string; + /** IdentityReference is used to specify the group by either its ID or name */ + groupReference?: IdentityReference; } /** GroupServiceGetResponse contains the requested group information */ @@ -62,14 +63,14 @@ export interface GroupServiceListResponse { /** GroupServiceUpdateRequest contains the fields that can be updated for a group */ export interface GroupServiceUpdateRequest { - /** UUID of the group to update */ - groupId: string; + /** IdentityReference is used to specify the group by either its ID or name */ + groupReference?: IdentityReference; /** New name for the group (if provided) */ - name?: + newName?: | string | undefined; /** New description for the group (if provided) */ - description?: string | undefined; + newDescription?: string | undefined; } /** GroupServiceUpdateResponse contains the updated group information */ @@ -80,8 +81,8 @@ export interface GroupServiceUpdateResponse { /** GroupServiceDeleteRequest contains the identifier for the group to delete */ export interface GroupServiceDeleteRequest { - /** UUID of the group to delete */ - id: string; + /** IdentityReference is used to specify the group by either its ID or name */ + groupReference?: IdentityReference; } /** GroupServiceDeleteResponse is returned upon successful deletion of a group */ @@ -97,8 +98,8 @@ export interface GroupServiceListMembersResponse { /** GroupServiceListMembersRequest contains the identifier for the group whose members are to be listed */ export interface GroupServiceListMembersRequest { - /** UUID of the group whose members are to be listed */ - groupId: string; + /** IdentityReference is used to specify the group by either its ID or name */ + groupReference?: IdentityReference; /** Optional filter to search only by maintainers or not */ maintainers?: | boolean @@ -111,6 +112,32 @@ export interface GroupServiceListMembersRequest { pagination?: OffsetPaginationRequest; } +/** GroupServiceAddMemberRequest contains the information needed to add a user to a group */ +export interface GroupServiceAddMemberRequest { + /** IdentityReference is used to specify the group by either its ID or name */ + groupReference?: IdentityReference; + /** The user to add to the group */ + userEmail: string; + /** Indicates whether the user should have maintainer (admin) privileges in the group */ + isMaintainer: boolean; +} + +/** GroupServiceAddMemberResponse contains the information about the group member that was added */ +export interface GroupServiceAddMemberResponse { +} + +/** GroupServiceRemoveMemberRequest contains the information needed to remove a user from a group */ +export interface GroupServiceRemoveMemberRequest { + /** IdentityReference is used to specify the group by either its ID or name */ + groupReference?: IdentityReference; + /** The user to remove from the group */ + userEmail: string; +} + +/** GroupServiceRemoveMemberResponse is returned upon successful removal of a user from a group */ +export interface GroupServiceRemoveMemberResponse { +} + /** Group represents a collection of users with shared access to resources */ export interface Group { /** Unique identifier for the group */ @@ -267,13 +294,13 @@ export const GroupServiceCreateResponse = { }; function createBaseGroupServiceGetRequest(): GroupServiceGetRequest { - return { groupId: "" }; + return { groupReference: undefined }; } export const GroupServiceGetRequest = { encode(message: GroupServiceGetRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.groupId !== "") { - writer.uint32(10).string(message.groupId); + if (message.groupReference !== undefined) { + IdentityReference.encode(message.groupReference, writer.uint32(10).fork()).ldelim(); } return writer; }, @@ -290,7 +317,7 @@ export const GroupServiceGetRequest = { break; } - message.groupId = reader.string(); + message.groupReference = IdentityReference.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { @@ -302,12 +329,15 @@ export const GroupServiceGetRequest = { }, fromJSON(object: any): GroupServiceGetRequest { - return { groupId: isSet(object.groupId) ? String(object.groupId) : "" }; + return { + groupReference: isSet(object.groupReference) ? IdentityReference.fromJSON(object.groupReference) : undefined, + }; }, toJSON(message: GroupServiceGetRequest): unknown { const obj: any = {}; - message.groupId !== undefined && (obj.groupId = message.groupId); + message.groupReference !== undefined && + (obj.groupReference = message.groupReference ? IdentityReference.toJSON(message.groupReference) : undefined); return obj; }, @@ -317,7 +347,9 @@ export const GroupServiceGetRequest = { fromPartial, I>>(object: I): GroupServiceGetRequest { const message = createBaseGroupServiceGetRequest(); - message.groupId = object.groupId ?? ""; + message.groupReference = (object.groupReference !== undefined && object.groupReference !== null) + ? IdentityReference.fromPartial(object.groupReference) + : undefined; return message; }, }; @@ -557,19 +589,19 @@ export const GroupServiceListResponse = { }; function createBaseGroupServiceUpdateRequest(): GroupServiceUpdateRequest { - return { groupId: "", name: undefined, description: undefined }; + return { groupReference: undefined, newName: undefined, newDescription: undefined }; } export const GroupServiceUpdateRequest = { encode(message: GroupServiceUpdateRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.groupId !== "") { - writer.uint32(10).string(message.groupId); + if (message.groupReference !== undefined) { + IdentityReference.encode(message.groupReference, writer.uint32(10).fork()).ldelim(); } - if (message.name !== undefined) { - writer.uint32(18).string(message.name); + if (message.newName !== undefined) { + writer.uint32(26).string(message.newName); } - if (message.description !== undefined) { - writer.uint32(26).string(message.description); + if (message.newDescription !== undefined) { + writer.uint32(34).string(message.newDescription); } return writer; }, @@ -586,21 +618,21 @@ export const GroupServiceUpdateRequest = { break; } - message.groupId = reader.string(); + message.groupReference = IdentityReference.decode(reader, reader.uint32()); continue; - case 2: - if (tag !== 18) { + case 3: + if (tag !== 26) { break; } - message.name = reader.string(); + message.newName = reader.string(); continue; - case 3: - if (tag !== 26) { + case 4: + if (tag !== 34) { break; } - message.description = reader.string(); + message.newDescription = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { @@ -613,17 +645,18 @@ export const GroupServiceUpdateRequest = { fromJSON(object: any): GroupServiceUpdateRequest { return { - groupId: isSet(object.groupId) ? String(object.groupId) : "", - name: isSet(object.name) ? String(object.name) : undefined, - description: isSet(object.description) ? String(object.description) : undefined, + groupReference: isSet(object.groupReference) ? IdentityReference.fromJSON(object.groupReference) : undefined, + newName: isSet(object.newName) ? String(object.newName) : undefined, + newDescription: isSet(object.newDescription) ? String(object.newDescription) : undefined, }; }, toJSON(message: GroupServiceUpdateRequest): unknown { const obj: any = {}; - message.groupId !== undefined && (obj.groupId = message.groupId); - message.name !== undefined && (obj.name = message.name); - message.description !== undefined && (obj.description = message.description); + message.groupReference !== undefined && + (obj.groupReference = message.groupReference ? IdentityReference.toJSON(message.groupReference) : undefined); + message.newName !== undefined && (obj.newName = message.newName); + message.newDescription !== undefined && (obj.newDescription = message.newDescription); return obj; }, @@ -633,9 +666,11 @@ export const GroupServiceUpdateRequest = { fromPartial, I>>(object: I): GroupServiceUpdateRequest { const message = createBaseGroupServiceUpdateRequest(); - message.groupId = object.groupId ?? ""; - message.name = object.name ?? undefined; - message.description = object.description ?? undefined; + message.groupReference = (object.groupReference !== undefined && object.groupReference !== null) + ? IdentityReference.fromPartial(object.groupReference) + : undefined; + message.newName = object.newName ?? undefined; + message.newDescription = object.newDescription ?? undefined; return message; }, }; @@ -697,13 +732,13 @@ export const GroupServiceUpdateResponse = { }; function createBaseGroupServiceDeleteRequest(): GroupServiceDeleteRequest { - return { id: "" }; + return { groupReference: undefined }; } export const GroupServiceDeleteRequest = { encode(message: GroupServiceDeleteRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.id !== "") { - writer.uint32(10).string(message.id); + if (message.groupReference !== undefined) { + IdentityReference.encode(message.groupReference, writer.uint32(10).fork()).ldelim(); } return writer; }, @@ -720,7 +755,7 @@ export const GroupServiceDeleteRequest = { break; } - message.id = reader.string(); + message.groupReference = IdentityReference.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { @@ -732,12 +767,15 @@ export const GroupServiceDeleteRequest = { }, fromJSON(object: any): GroupServiceDeleteRequest { - return { id: isSet(object.id) ? String(object.id) : "" }; + return { + groupReference: isSet(object.groupReference) ? IdentityReference.fromJSON(object.groupReference) : undefined, + }; }, toJSON(message: GroupServiceDeleteRequest): unknown { const obj: any = {}; - message.id !== undefined && (obj.id = message.id); + message.groupReference !== undefined && + (obj.groupReference = message.groupReference ? IdentityReference.toJSON(message.groupReference) : undefined); return obj; }, @@ -747,7 +785,9 @@ export const GroupServiceDeleteRequest = { fromPartial, I>>(object: I): GroupServiceDeleteRequest { const message = createBaseGroupServiceDeleteRequest(); - message.id = object.id ?? ""; + message.groupReference = (object.groupReference !== undefined && object.groupReference !== null) + ? IdentityReference.fromPartial(object.groupReference) + : undefined; return message; }, }; @@ -877,22 +917,22 @@ export const GroupServiceListMembersResponse = { }; function createBaseGroupServiceListMembersRequest(): GroupServiceListMembersRequest { - return { groupId: "", maintainers: undefined, memberEmail: undefined, pagination: undefined }; + return { groupReference: undefined, maintainers: undefined, memberEmail: undefined, pagination: undefined }; } export const GroupServiceListMembersRequest = { encode(message: GroupServiceListMembersRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.groupId !== "") { - writer.uint32(10).string(message.groupId); + if (message.groupReference !== undefined) { + IdentityReference.encode(message.groupReference, writer.uint32(10).fork()).ldelim(); } if (message.maintainers !== undefined) { - writer.uint32(16).bool(message.maintainers); + writer.uint32(24).bool(message.maintainers); } if (message.memberEmail !== undefined) { - writer.uint32(26).string(message.memberEmail); + writer.uint32(34).string(message.memberEmail); } if (message.pagination !== undefined) { - OffsetPaginationRequest.encode(message.pagination, writer.uint32(34).fork()).ldelim(); + OffsetPaginationRequest.encode(message.pagination, writer.uint32(42).fork()).ldelim(); } return writer; }, @@ -909,24 +949,24 @@ export const GroupServiceListMembersRequest = { break; } - message.groupId = reader.string(); + message.groupReference = IdentityReference.decode(reader, reader.uint32()); continue; - case 2: - if (tag !== 16) { + case 3: + if (tag !== 24) { break; } message.maintainers = reader.bool(); continue; - case 3: - if (tag !== 26) { + case 4: + if (tag !== 34) { break; } message.memberEmail = reader.string(); continue; - case 4: - if (tag !== 34) { + case 5: + if (tag !== 42) { break; } @@ -943,7 +983,7 @@ export const GroupServiceListMembersRequest = { fromJSON(object: any): GroupServiceListMembersRequest { return { - groupId: isSet(object.groupId) ? String(object.groupId) : "", + groupReference: isSet(object.groupReference) ? IdentityReference.fromJSON(object.groupReference) : undefined, maintainers: isSet(object.maintainers) ? Boolean(object.maintainers) : undefined, memberEmail: isSet(object.memberEmail) ? String(object.memberEmail) : undefined, pagination: isSet(object.pagination) ? OffsetPaginationRequest.fromJSON(object.pagination) : undefined, @@ -952,7 +992,8 @@ export const GroupServiceListMembersRequest = { toJSON(message: GroupServiceListMembersRequest): unknown { const obj: any = {}; - message.groupId !== undefined && (obj.groupId = message.groupId); + message.groupReference !== undefined && + (obj.groupReference = message.groupReference ? IdentityReference.toJSON(message.groupReference) : undefined); message.maintainers !== undefined && (obj.maintainers = message.maintainers); message.memberEmail !== undefined && (obj.memberEmail = message.memberEmail); message.pagination !== undefined && @@ -968,7 +1009,9 @@ export const GroupServiceListMembersRequest = { object: I, ): GroupServiceListMembersRequest { const message = createBaseGroupServiceListMembersRequest(); - message.groupId = object.groupId ?? ""; + message.groupReference = (object.groupReference !== undefined && object.groupReference !== null) + ? IdentityReference.fromPartial(object.groupReference) + : undefined; message.maintainers = object.maintainers ?? undefined; message.memberEmail = object.memberEmail ?? undefined; message.pagination = (object.pagination !== undefined && object.pagination !== null) @@ -978,6 +1021,261 @@ export const GroupServiceListMembersRequest = { }, }; +function createBaseGroupServiceAddMemberRequest(): GroupServiceAddMemberRequest { + return { groupReference: undefined, userEmail: "", isMaintainer: false }; +} + +export const GroupServiceAddMemberRequest = { + encode(message: GroupServiceAddMemberRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.groupReference !== undefined) { + IdentityReference.encode(message.groupReference, writer.uint32(10).fork()).ldelim(); + } + if (message.userEmail !== "") { + writer.uint32(26).string(message.userEmail); + } + if (message.isMaintainer === true) { + writer.uint32(32).bool(message.isMaintainer); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): GroupServiceAddMemberRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGroupServiceAddMemberRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.groupReference = IdentityReference.decode(reader, reader.uint32()); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.userEmail = reader.string(); + continue; + case 4: + if (tag !== 32) { + break; + } + + message.isMaintainer = reader.bool(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): GroupServiceAddMemberRequest { + return { + groupReference: isSet(object.groupReference) ? IdentityReference.fromJSON(object.groupReference) : undefined, + userEmail: isSet(object.userEmail) ? String(object.userEmail) : "", + isMaintainer: isSet(object.isMaintainer) ? Boolean(object.isMaintainer) : false, + }; + }, + + toJSON(message: GroupServiceAddMemberRequest): unknown { + const obj: any = {}; + message.groupReference !== undefined && + (obj.groupReference = message.groupReference ? IdentityReference.toJSON(message.groupReference) : undefined); + message.userEmail !== undefined && (obj.userEmail = message.userEmail); + message.isMaintainer !== undefined && (obj.isMaintainer = message.isMaintainer); + return obj; + }, + + create, I>>(base?: I): GroupServiceAddMemberRequest { + return GroupServiceAddMemberRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): GroupServiceAddMemberRequest { + const message = createBaseGroupServiceAddMemberRequest(); + message.groupReference = (object.groupReference !== undefined && object.groupReference !== null) + ? IdentityReference.fromPartial(object.groupReference) + : undefined; + message.userEmail = object.userEmail ?? ""; + message.isMaintainer = object.isMaintainer ?? false; + return message; + }, +}; + +function createBaseGroupServiceAddMemberResponse(): GroupServiceAddMemberResponse { + return {}; +} + +export const GroupServiceAddMemberResponse = { + encode(_: GroupServiceAddMemberResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): GroupServiceAddMemberResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGroupServiceAddMemberResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(_: any): GroupServiceAddMemberResponse { + return {}; + }, + + toJSON(_: GroupServiceAddMemberResponse): unknown { + const obj: any = {}; + return obj; + }, + + create, I>>(base?: I): GroupServiceAddMemberResponse { + return GroupServiceAddMemberResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>(_: I): GroupServiceAddMemberResponse { + const message = createBaseGroupServiceAddMemberResponse(); + return message; + }, +}; + +function createBaseGroupServiceRemoveMemberRequest(): GroupServiceRemoveMemberRequest { + return { groupReference: undefined, userEmail: "" }; +} + +export const GroupServiceRemoveMemberRequest = { + encode(message: GroupServiceRemoveMemberRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.groupReference !== undefined) { + IdentityReference.encode(message.groupReference, writer.uint32(10).fork()).ldelim(); + } + if (message.userEmail !== "") { + writer.uint32(26).string(message.userEmail); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): GroupServiceRemoveMemberRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGroupServiceRemoveMemberRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.groupReference = IdentityReference.decode(reader, reader.uint32()); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.userEmail = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): GroupServiceRemoveMemberRequest { + return { + groupReference: isSet(object.groupReference) ? IdentityReference.fromJSON(object.groupReference) : undefined, + userEmail: isSet(object.userEmail) ? String(object.userEmail) : "", + }; + }, + + toJSON(message: GroupServiceRemoveMemberRequest): unknown { + const obj: any = {}; + message.groupReference !== undefined && + (obj.groupReference = message.groupReference ? IdentityReference.toJSON(message.groupReference) : undefined); + message.userEmail !== undefined && (obj.userEmail = message.userEmail); + return obj; + }, + + create, I>>(base?: I): GroupServiceRemoveMemberRequest { + return GroupServiceRemoveMemberRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): GroupServiceRemoveMemberRequest { + const message = createBaseGroupServiceRemoveMemberRequest(); + message.groupReference = (object.groupReference !== undefined && object.groupReference !== null) + ? IdentityReference.fromPartial(object.groupReference) + : undefined; + message.userEmail = object.userEmail ?? ""; + return message; + }, +}; + +function createBaseGroupServiceRemoveMemberResponse(): GroupServiceRemoveMemberResponse { + return {}; +} + +export const GroupServiceRemoveMemberResponse = { + encode(_: GroupServiceRemoveMemberResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): GroupServiceRemoveMemberResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGroupServiceRemoveMemberResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(_: any): GroupServiceRemoveMemberResponse { + return {}; + }, + + toJSON(_: GroupServiceRemoveMemberResponse): unknown { + const obj: any = {}; + return obj; + }, + + create, I>>( + base?: I, + ): GroupServiceRemoveMemberResponse { + return GroupServiceRemoveMemberResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + _: I, + ): GroupServiceRemoveMemberResponse { + const message = createBaseGroupServiceRemoveMemberResponse(); + return message; + }, +}; + function createBaseGroup(): Group { return { id: "", name: "", description: "", organizationId: "", createdAt: undefined, updatedAt: undefined }; } @@ -1224,6 +1522,16 @@ export interface GroupService { request: DeepPartial, metadata?: grpc.Metadata, ): Promise; + /** AddMember adds a user to a group with an optional maintainer role */ + AddMember( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + /** RemoveMember removes a user from a group */ + RemoveMember( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; } export class GroupServiceClientImpl implements GroupService { @@ -1237,6 +1545,8 @@ export class GroupServiceClientImpl implements GroupService { this.Update = this.Update.bind(this); this.Delete = this.Delete.bind(this); this.ListMembers = this.ListMembers.bind(this); + this.AddMember = this.AddMember.bind(this); + this.RemoveMember = this.RemoveMember.bind(this); } Create( @@ -1274,6 +1584,20 @@ export class GroupServiceClientImpl implements GroupService { ): Promise { return this.rpc.unary(GroupServiceListMembersDesc, GroupServiceListMembersRequest.fromPartial(request), metadata); } + + AddMember( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary(GroupServiceAddMemberDesc, GroupServiceAddMemberRequest.fromPartial(request), metadata); + } + + RemoveMember( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary(GroupServiceRemoveMemberDesc, GroupServiceRemoveMemberRequest.fromPartial(request), metadata); + } } export const GroupServiceDesc = { serviceName: "controlplane.v1.GroupService" }; @@ -1416,6 +1740,52 @@ export const GroupServiceListMembersDesc: UnaryMethodDefinitionish = { } as any, }; +export const GroupServiceAddMemberDesc: UnaryMethodDefinitionish = { + methodName: "AddMember", + service: GroupServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return GroupServiceAddMemberRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = GroupServiceAddMemberResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const GroupServiceRemoveMemberDesc: UnaryMethodDefinitionish = { + methodName: "RemoveMember", + service: GroupServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return GroupServiceRemoveMemberRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = GroupServiceRemoveMemberResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + interface UnaryMethodDefinitionishR extends grpc.UnaryMethodDefinition { requestStream: any; responseStream: any; diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts index 0a210d10c..bef14b489 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts @@ -643,12 +643,6 @@ export interface CASBackendItem_Limits { maxBytes: number; } -/** EntityRef is a reference to an entity in the system that can be either by name or ID */ -export interface EntityRef { - entityId?: string | undefined; - entityName?: string | undefined; -} - export interface APITokenItem { id: string; name: string; @@ -3864,77 +3858,6 @@ export const CASBackendItem_Limits = { }, }; -function createBaseEntityRef(): EntityRef { - return { entityId: undefined, entityName: undefined }; -} - -export const EntityRef = { - encode(message: EntityRef, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.entityId !== undefined) { - writer.uint32(10).string(message.entityId); - } - if (message.entityName !== undefined) { - writer.uint32(18).string(message.entityName); - } - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): EntityRef { - const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseEntityRef(); - while (reader.pos < end) { - const tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - if (tag !== 10) { - break; - } - - message.entityId = reader.string(); - continue; - case 2: - if (tag !== 18) { - break; - } - - message.entityName = reader.string(); - continue; - } - if ((tag & 7) === 4 || tag === 0) { - break; - } - reader.skipType(tag & 7); - } - return message; - }, - - fromJSON(object: any): EntityRef { - return { - entityId: isSet(object.entityId) ? String(object.entityId) : undefined, - entityName: isSet(object.entityName) ? String(object.entityName) : undefined, - }; - }, - - toJSON(message: EntityRef): unknown { - const obj: any = {}; - message.entityId !== undefined && (obj.entityId = message.entityId); - message.entityName !== undefined && (obj.entityName = message.entityName); - return obj; - }, - - create, I>>(base?: I): EntityRef { - return EntityRef.fromPartial(base ?? {}); - }, - - fromPartial, I>>(object: I): EntityRef { - const message = createBaseEntityRef(); - message.entityId = object.entityId ?? undefined; - message.entityName = object.entityName ?? undefined; - return message; - }, -}; - function createBaseAPITokenItem(): APITokenItem { return { id: "", diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/shared_message.ts b/app/controlplane/api/gen/frontend/controlplane/v1/shared_message.ts new file mode 100644 index 000000000..f8ccfe269 --- /dev/null +++ b/app/controlplane/api/gen/frontend/controlplane/v1/shared_message.ts @@ -0,0 +1,100 @@ +/* eslint-disable */ +import _m0 from "protobufjs/minimal"; + +export const protobufPackage = "controlplane.v1"; + +/** IdentityReference represents a reference to an identity in the system. */ +export interface IdentityReference { + /** ID is optional, but if provided, it must be a valid UUID. */ + id?: + | string + | undefined; + /** Name is optional, but if provided, it must be a non-empty string. */ + name?: string | undefined; +} + +function createBaseIdentityReference(): IdentityReference { + return { id: undefined, name: undefined }; +} + +export const IdentityReference = { + encode(message: IdentityReference, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.id !== undefined) { + writer.uint32(10).string(message.id); + } + if (message.name !== undefined) { + writer.uint32(18).string(message.name); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): IdentityReference { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseIdentityReference(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.name = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): IdentityReference { + return { + id: isSet(object.id) ? String(object.id) : undefined, + name: isSet(object.name) ? String(object.name) : undefined, + }; + }, + + toJSON(message: IdentityReference): unknown { + const obj: any = {}; + message.id !== undefined && (obj.id = message.id); + message.name !== undefined && (obj.name = message.name); + return obj; + }, + + create, I>>(base?: I): IdentityReference { + return IdentityReference.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): IdentityReference { + const message = createBaseIdentityReference(); + message.id = object.id ?? undefined; + message.name = object.name ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupReference.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupReference.jsonschema.json new file mode 100644 index 000000000..8765fc3a8 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupReference.jsonschema.json @@ -0,0 +1,32 @@ +{ + "$id": "controlplane.v1.GroupReference.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupReference is used to specify a group by either its ID or name when performing operations", + "patternProperties": { + "^(group_id)$": { + "description": "UUID of the group from which the user will be removed", + "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" + }, + "^(group_name)$": { + "description": "Name of the group from which the user will be removed, if group_id is not provided", + "minLength": 1, + "type": "string" + } + }, + "properties": { + "groupId": { + "description": "UUID of the group from which the user will be removed", + "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" + }, + "groupName": { + "description": "Name of the group from which the user will be removed, if group_id is not provided", + "minLength": 1, + "type": "string" + } + }, + "title": "Group Reference", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupReference.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupReference.schema.json new file mode 100644 index 000000000..b1880244f --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupReference.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "controlplane.v1.GroupReference.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupReference is used to specify a group by either its ID or name when performing operations", + "patternProperties": { + "^(groupId)$": { + "description": "UUID of the group from which the user will be removed", + "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" + }, + "^(groupName)$": { + "description": "Name of the group from which the user will be removed, if group_id is not provided", + "minLength": 1, + "type": "string" + } + }, + "properties": { + "group_id": { + "description": "UUID of the group from which the user will be removed", + "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" + }, + "group_name": { + "description": "Name of the group from which the user will be removed, if group_id is not provided", + "minLength": 1, + "type": "string" + } + }, + "title": "Group Reference", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberRequest.jsonschema.json new file mode 100644 index 000000000..2d64c2d0b --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberRequest.jsonschema.json @@ -0,0 +1,41 @@ +{ + "$id": "controlplane.v1.GroupServiceAddMemberRequest.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupServiceAddMemberRequest contains the information needed to add a user to a group", + "patternProperties": { + "^(group_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "^(is_maintainer)$": { + "description": "Indicates whether the user should have maintainer (admin) privileges in the group", + "type": "boolean" + }, + "^(user_email)$": { + "description": "The user to add to the group", + "format": "email", + "type": "string" + } + }, + "properties": { + "groupReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "isMaintainer": { + "description": "Indicates whether the user should have maintainer (admin) privileges in the group", + "type": "boolean" + }, + "userEmail": { + "description": "The user to add to the group", + "format": "email", + "type": "string" + } + }, + "required": [ + "group_reference" + ], + "title": "Group Service Add Member Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberRequest.schema.json new file mode 100644 index 000000000..519876a57 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberRequest.schema.json @@ -0,0 +1,41 @@ +{ + "$id": "controlplane.v1.GroupServiceAddMemberRequest.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupServiceAddMemberRequest contains the information needed to add a user to a group", + "patternProperties": { + "^(groupReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "^(isMaintainer)$": { + "description": "Indicates whether the user should have maintainer (admin) privileges in the group", + "type": "boolean" + }, + "^(userEmail)$": { + "description": "The user to add to the group", + "format": "email", + "type": "string" + } + }, + "properties": { + "group_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "is_maintainer": { + "description": "Indicates whether the user should have maintainer (admin) privileges in the group", + "type": "boolean" + }, + "user_email": { + "description": "The user to add to the group", + "format": "email", + "type": "string" + } + }, + "required": [ + "group_reference" + ], + "title": "Group Service Add Member Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberResponse.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberResponse.jsonschema.json new file mode 100644 index 000000000..7a9983540 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberResponse.jsonschema.json @@ -0,0 +1,9 @@ +{ + "$id": "controlplane.v1.GroupServiceAddMemberResponse.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupServiceAddMemberResponse contains the information about the group member that was added", + "properties": {}, + "title": "Group Service Add Member Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberResponse.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberResponse.schema.json new file mode 100644 index 000000000..7c1c4449f --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceAddMemberResponse.schema.json @@ -0,0 +1,9 @@ +{ + "$id": "controlplane.v1.GroupServiceAddMemberResponse.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupServiceAddMemberResponse contains the information about the group member that was added", + "properties": {}, + "title": "Group Service Add Member Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceDeleteRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceDeleteRequest.jsonschema.json index 7e27236a3..fe71ad0bc 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceDeleteRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceDeleteRequest.jsonschema.json @@ -3,13 +3,21 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "GroupServiceDeleteRequest contains the identifier for the group to delete", + "patternProperties": { + "^(group_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + } + }, "properties": { - "id": { - "description": "UUID of the group to delete", - "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" + "groupReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" } }, + "required": [ + "group_reference" + ], "title": "Group Service Delete Request", "type": "object" } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceDeleteRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceDeleteRequest.schema.json index 929ecd341..35fe88141 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceDeleteRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceDeleteRequest.schema.json @@ -3,13 +3,21 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "GroupServiceDeleteRequest contains the identifier for the group to delete", + "patternProperties": { + "^(groupReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + } + }, "properties": { - "id": { - "description": "UUID of the group to delete", - "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" + "group_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" } }, + "required": [ + "group_reference" + ], "title": "Group Service Delete Request", "type": "object" } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceGetRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceGetRequest.jsonschema.json index cb617441d..65112e562 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceGetRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceGetRequest.jsonschema.json @@ -4,23 +4,19 @@ "additionalProperties": false, "description": "GroupServiceGetRequest contains the identifier for the group to retrieve", "patternProperties": { - "^(group_id)$": { - "description": "UUID of the group to retrieve", - "minLength": 1, - "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" + "^(group_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" } }, "properties": { - "groupId": { - "description": "UUID of the group to retrieve", - "minLength": 1, - "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" + "groupReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" } }, "required": [ - "group_id" + "group_reference" ], "title": "Group Service Get Request", "type": "object" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceGetRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceGetRequest.schema.json index e6a81743c..fc3567061 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceGetRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceGetRequest.schema.json @@ -4,23 +4,19 @@ "additionalProperties": false, "description": "GroupServiceGetRequest contains the identifier for the group to retrieve", "patternProperties": { - "^(groupId)$": { - "description": "UUID of the group to retrieve", - "minLength": 1, - "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" + "^(groupReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" } }, "properties": { - "group_id": { - "description": "UUID of the group to retrieve", - "minLength": 1, - "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" + "group_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" } }, "required": [ - "group_id" + "group_reference" ], "title": "Group Service Get Request", "type": "object" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceListMembersRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceListMembersRequest.jsonschema.json index 3de3cabb1..687e52683 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceListMembersRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceListMembersRequest.jsonschema.json @@ -4,11 +4,9 @@ "additionalProperties": false, "description": "GroupServiceListMembersRequest contains the identifier for the group whose members are to be listed", "patternProperties": { - "^(group_id)$": { - "description": "UUID of the group whose members are to be listed", - "minLength": 1, - "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" + "^(group_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" }, "^(member_email)$": { "description": "Optional filter to search by member email address", @@ -16,11 +14,9 @@ } }, "properties": { - "groupId": { - "description": "UUID of the group whose members are to be listed", - "minLength": 1, - "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" + "groupReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" }, "maintainers": { "description": "Optional filter to search only by maintainers or not", @@ -36,7 +32,7 @@ } }, "required": [ - "group_id" + "group_reference" ], "title": "Group Service List Members Request", "type": "object" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceListMembersRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceListMembersRequest.schema.json index c47837205..1dd31a58f 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceListMembersRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceListMembersRequest.schema.json @@ -4,11 +4,9 @@ "additionalProperties": false, "description": "GroupServiceListMembersRequest contains the identifier for the group whose members are to be listed", "patternProperties": { - "^(groupId)$": { - "description": "UUID of the group whose members are to be listed", - "minLength": 1, - "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" + "^(groupReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" }, "^(memberEmail)$": { "description": "Optional filter to search by member email address", @@ -16,11 +14,9 @@ } }, "properties": { - "group_id": { - "description": "UUID of the group whose members are to be listed", - "minLength": 1, - "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" + "group_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" }, "maintainers": { "description": "Optional filter to search only by maintainers or not", @@ -36,7 +32,7 @@ } }, "required": [ - "group_id" + "group_reference" ], "title": "Group Service List Members Request", "type": "object" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberRequest.jsonschema.json new file mode 100644 index 000000000..282a7371f --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberRequest.jsonschema.json @@ -0,0 +1,33 @@ +{ + "$id": "controlplane.v1.GroupServiceRemoveMemberRequest.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupServiceRemoveMemberRequest contains the information needed to remove a user from a group", + "patternProperties": { + "^(group_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "^(user_email)$": { + "description": "The user to remove from the group", + "format": "email", + "type": "string" + } + }, + "properties": { + "groupReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "userEmail": { + "description": "The user to remove from the group", + "format": "email", + "type": "string" + } + }, + "required": [ + "group_reference" + ], + "title": "Group Service Remove Member Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberRequest.schema.json new file mode 100644 index 000000000..43de70b71 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberRequest.schema.json @@ -0,0 +1,33 @@ +{ + "$id": "controlplane.v1.GroupServiceRemoveMemberRequest.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupServiceRemoveMemberRequest contains the information needed to remove a user from a group", + "patternProperties": { + "^(groupReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "^(userEmail)$": { + "description": "The user to remove from the group", + "format": "email", + "type": "string" + } + }, + "properties": { + "group_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "user_email": { + "description": "The user to remove from the group", + "format": "email", + "type": "string" + } + }, + "required": [ + "group_reference" + ], + "title": "Group Service Remove Member Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberResponse.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberResponse.jsonschema.json new file mode 100644 index 000000000..7a776ff1f --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberResponse.jsonschema.json @@ -0,0 +1,9 @@ +{ + "$id": "controlplane.v1.GroupServiceRemoveMemberResponse.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupServiceRemoveMemberResponse is returned upon successful removal of a user from a group", + "properties": {}, + "title": "Group Service Remove Member Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberResponse.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberResponse.schema.json new file mode 100644 index 000000000..e8f26e4c9 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceRemoveMemberResponse.schema.json @@ -0,0 +1,9 @@ +{ + "$id": "controlplane.v1.GroupServiceRemoveMemberResponse.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "GroupServiceRemoveMemberResponse is returned upon successful removal of a user from a group", + "properties": {}, + "title": "Group Service Remove Member Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceUpdateRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceUpdateRequest.jsonschema.json index 421043cae..162ed1642 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceUpdateRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceUpdateRequest.jsonschema.json @@ -4,31 +4,35 @@ "additionalProperties": false, "description": "GroupServiceUpdateRequest contains the fields that can be updated for a group", "patternProperties": { - "^(group_id)$": { - "description": "UUID of the group to update", - "minLength": 1, - "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}$", + "^(group_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "^(new_description)$": { + "description": "New description for the group (if provided)", + "type": "string" + }, + "^(new_name)$": { + "description": "New name for the group (if provided)", "type": "string" } }, "properties": { - "description": { - "description": "New description for the group (if provided)", - "type": "string" + "groupReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" }, - "groupId": { - "description": "UUID of the group to update", - "minLength": 1, - "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}$", + "newDescription": { + "description": "New description for the group (if provided)", "type": "string" }, - "name": { + "newName": { "description": "New name for the group (if provided)", "type": "string" } }, "required": [ - "group_id" + "group_reference" ], "title": "Group Service Update Request", "type": "object" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceUpdateRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceUpdateRequest.schema.json index c5c880547..f483fe6f1 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceUpdateRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.GroupServiceUpdateRequest.schema.json @@ -4,31 +4,35 @@ "additionalProperties": false, "description": "GroupServiceUpdateRequest contains the fields that can be updated for a group", "patternProperties": { - "^(groupId)$": { - "description": "UUID of the group to update", - "minLength": 1, - "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}$", + "^(groupReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" + }, + "^(newDescription)$": { + "description": "New description for the group (if provided)", + "type": "string" + }, + "^(newName)$": { + "description": "New name for the group (if provided)", "type": "string" } }, "properties": { - "description": { - "description": "New description for the group (if provided)", - "type": "string" + "group_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the group by either its ID or name" }, - "group_id": { - "description": "UUID of the group to update", - "minLength": 1, - "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}$", + "new_description": { + "description": "New description for the group (if provided)", "type": "string" }, - "name": { + "new_name": { "description": "New name for the group (if provided)", "type": "string" } }, "required": [ - "group_id" + "group_reference" ], "title": "Group Service Update Request", "type": "object" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.IdentityReference.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.IdentityReference.jsonschema.json new file mode 100644 index 000000000..25f57c282 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.IdentityReference.jsonschema.json @@ -0,0 +1,20 @@ +{ + "$id": "controlplane.v1.IdentityReference.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "IdentityReference represents a reference to an identity in the system.", + "properties": { + "id": { + "description": "ID is optional, but if provided, it must be a valid UUID.", + "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": "Name is optional, but if provided, it must be a non-empty string.", + "minLength": 1, + "type": "string" + } + }, + "title": "Identity Reference", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.IdentityReference.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.IdentityReference.schema.json new file mode 100644 index 000000000..9889fafb5 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.IdentityReference.schema.json @@ -0,0 +1,20 @@ +{ + "$id": "controlplane.v1.IdentityReference.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "IdentityReference represents a reference to an identity in the system.", + "properties": { + "id": { + "description": "ID is optional, but if provided, it must be a valid UUID.", + "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": "Name is optional, but if provided, it must be a non-empty string.", + "minLength": 1, + "type": "string" + } + }, + "title": "Identity Reference", + "type": "object" +} diff --git a/app/controlplane/cmd/wire.go b/app/controlplane/cmd/wire.go index 96c060462..9c03ecb53 100644 --- a/app/controlplane/cmd/wire.go +++ b/app/controlplane/cmd/wire.go @@ -95,11 +95,12 @@ func newPolicyProviderConfig(in []*conf.PolicyProvider) []*policies.NewRegistryC return out } -func serviceOpts(l log.Logger, enforcer *authz.Enforcer, pUC *biz.ProjectUseCase) []service.NewOpt { +func serviceOpts(l log.Logger, enforcer *authz.Enforcer, pUC *biz.ProjectUseCase, gUC *biz.GroupUseCase) []service.NewOpt { return []service.NewOpt{ service.WithLogger(l), service.WithEnforcer(enforcer), service.WithProjectUseCase(pUC), + service.WithGroupUseCase(gUC), } } diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index d775453d8..9cb7fbc64 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -129,7 +129,9 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l workflowContractUseCase := biz.NewWorkflowContractUseCase(workflowContractRepo, registry, auditorUseCase, logger) workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, projectsRepo, workflowContractUseCase, auditorUseCase, membershipUseCase, logger) projectUseCase := biz.NewProjectsUseCase(logger, projectsRepo) - v5 := serviceOpts(logger, enforcer, projectUseCase) + groupRepo := data.NewGroupRepo(dataData, logger) + groupUseCase := biz.NewGroupUseCase(logger, groupRepo, membershipRepo, auditorUseCase) + v5 := serviceOpts(logger, enforcer, projectUseCase, groupUseCase) workflowService := service.NewWorkflowService(workflowUseCase, workflowContractUseCase, projectUseCase, v5...) orgInvitationRepo := data.NewOrgInvitation(dataData, logger) orgInvitationUseCase, err := biz.NewOrgInvitationUseCase(orgInvitationRepo, membershipRepo, userRepo, auditorUseCase, logger) @@ -228,8 +230,6 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l userService := service.NewUserService(membershipUseCase, organizationUseCase, v5...) signingService := service.NewSigningService(signingUseCase, v5...) prometheusService := service.NewPrometheusService(organizationUseCase, prometheusUseCase, v5...) - groupRepo := data.NewGroupRepo(dataData, logger) - groupUseCase := biz.NewGroupUseCase(logger, groupRepo, membershipRepo, auditorUseCase) groupService := service.NewGroupService(groupUseCase, v5...) projectService := service.NewProjectService(apiTokenUseCase, v5...) federatedAuthentication := bootstrap.FederatedAuthentication @@ -339,8 +339,8 @@ func newPolicyProviderConfig(in []*conf.PolicyProvider) []*policies.NewRegistryC return out } -func serviceOpts(l log.Logger, enforcer *authz.Enforcer, pUC *biz.ProjectUseCase) []service.NewOpt { - return []service.NewOpt{service.WithLogger(l), service.WithEnforcer(enforcer), service.WithProjectUseCase(pUC)} +func serviceOpts(l log.Logger, enforcer *authz.Enforcer, pUC *biz.ProjectUseCase, gUC *biz.GroupUseCase) []service.NewOpt { + return []service.NewOpt{service.WithLogger(l), service.WithEnforcer(enforcer), service.WithProjectUseCase(pUC), service.WithGroupUseCase(gUC)} } func newCASServerOptions(in *conf.Bootstrap_CASServer) *biz.CASServerDefaultOpts { diff --git a/app/controlplane/internal/service/group.go b/app/controlplane/internal/service/group.go index cb4eafe1c..e2c72d524 100644 --- a/app/controlplane/internal/service/group.go +++ b/app/controlplane/internal/service/group.go @@ -20,7 +20,6 @@ import ( pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/go-kratos/kratos/v2/errors" "github.com/google/uuid" @@ -42,28 +41,29 @@ func NewGroupService(groupUseCase *biz.GroupUseCase, opts ...NewOpt) *GroupServi } // Create creates a new group in the organization. -func (g GroupService) Create(ctx context.Context, req *pb.GroupServiceCreateRequest) (*pb.GroupServiceCreateResponse, error) { - currentUser, err := requireCurrentUser(ctx) - if err != nil { - return nil, err - } +func (g *GroupService) Create(ctx context.Context, req *pb.GroupServiceCreateRequest) (*pb.GroupServiceCreateResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err } - // Parse orgID - orgUUID, err := uuid.Parse(currentOrg.ID) + currentUser, err := requireCurrentUser(ctx) if err != nil { - return nil, errors.BadRequest("invalid", "invalid organization ID") + return nil, err } - // Parse userID + // Parse userUUID (current user) userUUID, err := uuid.Parse(currentUser.ID) if err != nil { return nil, errors.BadRequest("invalid", "invalid user ID") } + // Parse orgID + orgUUID, err := uuid.Parse(currentOrg.ID) + if err != nil { + return nil, errors.BadRequest("invalid", "invalid organization ID") + } + gr, err := g.groupUseCase.Create(ctx, orgUUID, req.Name, req.Description, userUUID) if err != nil { return nil, handleUseCaseErr(err, g.log) @@ -75,7 +75,7 @@ func (g GroupService) Create(ctx context.Context, req *pb.GroupServiceCreateRequ } // Get retrieves a group by its ID within the current organization. -func (g GroupService) Get(ctx context.Context, req *pb.GroupServiceGetRequest) (*pb.GroupServiceGetResponse, error) { +func (g *GroupService) Get(ctx context.Context, req *pb.GroupServiceGetRequest) (*pb.GroupServiceGetResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err @@ -87,13 +87,19 @@ func (g GroupService) Get(ctx context.Context, req *pb.GroupServiceGetRequest) ( return nil, errors.BadRequest("invalid", "invalid organization ID") } - // Parse groupID - groupUUID, err := uuid.Parse(req.GetGroupId()) + // Parse groupID and groupName from the request + id, name, err := g.parseIdentityReference(req.GetGroupReference()) if err != nil { - return nil, errors.BadRequest("invalid", "invalid group ID") + return nil, err } - gr, err := g.groupUseCase.FindByOrgAndID(ctx, orgUUID, groupUUID) + // Initialize the options for getting the group + opts := &biz.IdentityReference{ + ID: id, + Name: name, + } + + gr, err := g.groupUseCase.Get(ctx, orgUUID, opts) if err != nil { return nil, handleUseCaseErr(err, g.log) } @@ -104,7 +110,7 @@ func (g GroupService) Get(ctx context.Context, req *pb.GroupServiceGetRequest) ( } // List retrieves a list of groups within the current organization, with optional filters and pagination. -func (g GroupService) List(ctx context.Context, req *pb.GroupServiceListRequest) (*pb.GroupServiceListResponse, error) { +func (g *GroupService) List(ctx context.Context, req *pb.GroupServiceListRequest) (*pb.GroupServiceListResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err @@ -117,17 +123,9 @@ func (g GroupService) List(ctx context.Context, req *pb.GroupServiceListRequest) } // Initialize the pagination options, with default values - paginationOpts := pagination.NewDefaultOffsetPaginationOpts() - - // Override the pagination options if they are provided - if req.GetPagination() != nil { - paginationOpts, err = pagination.NewOffsetPaginationOpts( - int(req.GetPagination().GetPage()), - int(req.GetPagination().GetPageSize()), - ) - if err != nil { - return nil, handleUseCaseErr(err, g.log) - } + paginationOpts, err := initializePaginationOpts(req.GetPagination()) + if err != nil { + return nil, handleUseCaseErr(err, g.log) } // Initialize the filters @@ -162,7 +160,7 @@ func (g GroupService) List(ctx context.Context, req *pb.GroupServiceListRequest) } // Update updates an existing group in the organization. -func (g GroupService) Update(ctx context.Context, req *pb.GroupServiceUpdateRequest) (*pb.GroupServiceUpdateResponse, error) { +func (g *GroupService) Update(ctx context.Context, req *pb.GroupServiceUpdateRequest) (*pb.GroupServiceUpdateResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err @@ -174,14 +172,20 @@ func (g GroupService) Update(ctx context.Context, req *pb.GroupServiceUpdateRequ return nil, errors.BadRequest("invalid", "invalid organization ID") } - // Parse groupID - groupUUID, err := uuid.Parse(req.GetGroupId()) + // Parse groupID and groupName from the request + id, name, err := g.parseIdentityReference(req.GetGroupReference()) if err != nil { - return nil, errors.BadRequest("invalid", "invalid group ID") + return nil, err } // Update the group with the provided options - gr, err := g.groupUseCase.Update(ctx, orgUUID, groupUUID, req.Description, req.Name) + gr, err := g.groupUseCase.Update(ctx, orgUUID, &biz.IdentityReference{ + Name: name, + ID: id, + }, &biz.UpdateGroupOpts{ + NewDescription: req.NewDescription, + NewName: req.NewName, + }) if err != nil { return nil, handleUseCaseErr(err, g.log) } @@ -192,7 +196,7 @@ func (g GroupService) Update(ctx context.Context, req *pb.GroupServiceUpdateRequ } // Delete soft-deletes a group by its ID within the current organization. -func (g GroupService) Delete(ctx context.Context, req *pb.GroupServiceDeleteRequest) (*pb.GroupServiceDeleteResponse, error) { +func (g *GroupService) Delete(ctx context.Context, req *pb.GroupServiceDeleteRequest) (*pb.GroupServiceDeleteResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err @@ -204,13 +208,16 @@ func (g GroupService) Delete(ctx context.Context, req *pb.GroupServiceDeleteRequ return nil, errors.BadRequest("invalid", "invalid organization ID") } - // Parse groupID - groupUUID, err := uuid.Parse(req.Id) + // Initialize the options for deleting the group + idReference := &biz.IdentityReference{} + + // Parse groupID and groupName from the request + idReference.ID, idReference.Name, err = g.parseIdentityReference(req.GetGroupReference()) if err != nil { - return nil, errors.BadRequest("invalid", "invalid group ID") + return nil, err } - err = g.groupUseCase.SoftDelete(ctx, orgUUID, groupUUID) + err = g.groupUseCase.Delete(ctx, orgUUID, idReference) if err != nil { return nil, handleUseCaseErr(err, g.log) } @@ -219,7 +226,7 @@ func (g GroupService) Delete(ctx context.Context, req *pb.GroupServiceDeleteRequ } // ListMembers retrieves a list of members in a group within the current organization, with optional filters and pagination. -func (g GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceListMembersRequest) (*pb.GroupServiceListMembersResponse, error) { +func (g *GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceListMembersRequest) (*pb.GroupServiceListMembersResponse, error) { currentOrg, err := requireCurrentOrg(ctx) if err != nil { return nil, err @@ -231,27 +238,26 @@ func (g GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceListM return nil, errors.BadRequest("invalid", "invalid organization ID") } - // Initialize the pagination options, with default values - paginationOpts := pagination.NewDefaultOffsetPaginationOpts() + // Initialize the options for listing members + opts := &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{}, + Maintainers: req.Maintainers, + MemberEmail: req.MemberEmail, + } - // Override the pagination options if they are provided - if req.GetPagination() != nil { - paginationOpts, err = pagination.NewOffsetPaginationOpts( - int(req.GetPagination().GetPage()), - int(req.GetPagination().GetPageSize()), - ) - if err != nil { - return nil, handleUseCaseErr(err, g.log) - } + // Parse groupID and groupName from the request + opts.ID, opts.Name, err = g.parseIdentityReference(req.GetGroupReference()) + if err != nil { + return nil, err } - // Parse groupID - groupUUID, err := uuid.Parse(req.GetGroupId()) + // Initialize the pagination options, with default values + paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, errors.BadRequest("invalid", "invalid group ID") + return nil, handleUseCaseErr(err, g.log) } - grs, count, err := g.groupUseCase.ListMembers(ctx, orgUUID, groupUUID, req.Maintainers, req.MemberEmail, paginationOpts) + grs, count, err := g.groupUseCase.ListMembers(ctx, orgUUID, opts, paginationOpts) if err != nil { return nil, handleUseCaseErr(err, g.log) } @@ -267,6 +273,106 @@ func (g GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceListM }, nil } +// AddMember adds a member to a group within the current organization. +func (g *GroupService) AddMember(ctx context.Context, req *pb.GroupServiceAddMemberRequest) (*pb.GroupServiceAddMemberResponse, error) { + currentUser, err := requireCurrentUser(ctx) + if err != nil { + return nil, err + } + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + if err := g.userHasPermissionToAddGroupMember(ctx, currentOrg.ID, req.GetGroupReference()); err != nil { + return nil, err + } + + // Parse orgID + orgUUID, err := uuid.Parse(currentOrg.ID) + if err != nil { + return nil, errors.BadRequest("invalid", "invalid organization ID") + } + + // Parse requesterID (current user) + requesterUUID, err := uuid.Parse(currentUser.ID) + if err != nil { + return nil, errors.BadRequest("invalid", "invalid user ID") + } + + // Create options for adding the member + addOpts := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{}, + UserEmail: req.GetUserEmail(), + RequesterID: requesterUUID, + Maintainer: req.GetIsMaintainer(), + } + + // Parse groupID and groupName from the request + addOpts.ID, addOpts.Name, err = g.parseIdentityReference(req.GetGroupReference()) + if err != nil { + return nil, err + } + + // Call the business logic to add the member + _, err = g.groupUseCase.AddMemberToGroup(ctx, orgUUID, addOpts) + if err != nil { + return nil, handleUseCaseErr(err, g.log) + } + + return &pb.GroupServiceAddMemberResponse{}, nil +} + +// RemoveMember removes a member from a group within the current organization. +func (g *GroupService) RemoveMember(ctx context.Context, req *pb.GroupServiceRemoveMemberRequest) (*pb.GroupServiceRemoveMemberResponse, error) { + currentUser, err := requireCurrentUser(ctx) + if err != nil { + return nil, err + } + + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + if err := g.userHasPermissionToRemoveGroupMember(ctx, currentOrg.ID, req.GetGroupReference()); err != nil { + return nil, err + } + + // Parse orgID + orgUUID, err := uuid.Parse(currentOrg.ID) + if err != nil { + return nil, errors.BadRequest("invalid", "invalid organization ID") + } + + // Parse requesterID (current user) + requesterUUID, err := uuid.Parse(currentUser.ID) + if err != nil { + return nil, errors.BadRequest("invalid", "invalid user ID") + } + + // Create options for removing the member + removeOpts := &biz.RemoveMemberFromGroupOpts{ + IdentityReference: &biz.IdentityReference{}, + UserEmail: req.GetUserEmail(), + RequesterID: requesterUUID, + } + + // Parse groupID and groupName from the request + removeOpts.ID, removeOpts.Name, err = g.parseIdentityReference(req.GetGroupReference()) + if err != nil { + return nil, err + } + + // Call the business logic to remove the member + err = g.groupUseCase.RemoveMemberFromGroup(ctx, orgUUID, removeOpts) + if err != nil { + return nil, handleUseCaseErr(err, g.log) + } + + return &pb.GroupServiceRemoveMemberResponse{}, nil +} + // bizGroupToPb converts a biz.Group to a pb.Group protobuf message. func bizGroupToPb(gr *biz.Group) *pb.Group { base := &pb.Group{ diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index e60a7e9f3..a0daff18d 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -20,6 +20,8 @@ import ( "fmt" "io" + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" @@ -133,6 +135,7 @@ type service struct { log *log.Helper enforcer *authz.Enforcer projectUseCase *biz.ProjectUseCase + groupUseCase *biz.GroupUseCase } type NewOpt func(s *service) @@ -155,6 +158,12 @@ func WithProjectUseCase(projectUseCase *biz.ProjectUseCase) NewOpt { } } +func WithGroupUseCase(groupUseCase *biz.GroupUseCase) NewOpt { + return func(s *service) { + s.groupUseCase = groupUseCase + } +} + // authorizeResource is a helper that checks if the user has a particular `op` permission policy on a particular resource // For example: `s.authorizeResource(ctx, authz.PolicyAttachedIntegrationDetach, authz.ResourceTypeProject, projectUUID);` // checks if the user has a role in the project that allows to detach integrations on it. @@ -221,6 +230,83 @@ func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, return p, nil } +// userHasPermissionToAddGroupMember checks if the user has permission to add members to a group +func (s *service) userHasPermissionToAddGroupMember(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference) error { + return s.userHasPermissionOnGroupMembershipsWithPolicy(ctx, orgID, groupIdentifier, authz.PolicyGroupAddMemberships) +} + +// userHasPermissionToRemoveGroupMember checks if the user has permission to remove members from a group +func (s *service) userHasPermissionToRemoveGroupMember(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference) error { + return s.userHasPermissionOnGroupMembershipsWithPolicy(ctx, orgID, groupIdentifier, authz.PolicyGroupRemoveMemberships) +} + +// userHasPermissionOnGroupMembershipsWithPolicy is the core implementation that checks if a user has permission on a group +// with an optional specific policy check. If the policy is nil, it falls back to the basic permission check. +func (s *service) userHasPermissionOnGroupMembershipsWithPolicy(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference, policy *authz.Policy) error { + groupID, groupName, err := s.parseIdentityReference(groupIdentifier) + if err != nil { + return handleUseCaseErr(err, s.log) + } + + orgUUID, err := uuid.Parse(orgID) + if err != nil { + return errors.BadRequest("invalid", "invalid organization ID") + } + + // Resolve the group identifier to a valid group ID + resolvedGroupID, err := s.groupUseCase.ValidateGroupIdentifier(ctx, orgUUID, groupID, groupName) + if err != nil { + return handleUseCaseErr(err, s.log) + } + + // Get the current user from the context + user := entities.CurrentUser(ctx) + if user == nil { + return errors.NotFound("not found", "logged in user not found") + } + + userUUID, err := uuid.Parse(user.ID) + if err != nil { + return errors.BadRequest("invalid", "invalid user ID") + } + + // Check if the user is a maintainer of the group + isMaintainer, err := s.groupUseCase.IsUserGroupMaintainer(ctx, orgUUID, resolvedGroupID, userUUID) + if err != nil { + return handleUseCaseErr(err, s.log) + } + + // If the user is a maintainer, they can perform any operation on the group + if isMaintainer { + return nil + } + + // Check if the user has admin or owner role in the organization + userRole := usercontext.CurrentAuthzSubject(ctx) + if userRole == "" { + return errors.NotFound("not found", "current membership not found") + } + + // Allow if user has admin or owner role + if userRole == string(authz.RoleAdmin) || userRole == string(authz.RoleOwner) { + return nil + } + + // If a specific policy was provided, check if the user's role allows that policy + if policy != nil && s.enforcer != nil { + pass, err := s.enforcer.Enforce(userRole, policy) + if err != nil { + return handleUseCaseErr(err, s.log) + } + if pass { + return nil + } + } + + // If neither a maintainer nor admin/owner, nor has specific policy permission, forbid the operation + return errors.Forbidden("forbidden", "operation not allowed") +} + // visibleProjects returns projects where the user has any role (currently ProjectAdmin and ProjectViewer) func (s *service) visibleProjects(ctx context.Context) []uuid.UUID { if !rbacEnabled(ctx) { @@ -249,6 +335,45 @@ func (s *service) visibleProjects(ctx context.Context) []uuid.UUID { return projects } +// parseIdentityReference is a helper method to parse an IdentityReference from the protobuf message. +func (s *service) parseIdentityReference(groupRef *pb.IdentityReference) (*uuid.UUID, *string, error) { + if groupRef.GetId() != "" && groupRef.GetName() != "" { + return nil, nil, errors.BadRequest("invalid", "cannot provide both ID and name") + } + + if groupRef.GetId() != "" { + groupUUID, err := uuid.Parse(groupRef.GetId()) + if err != nil { + return nil, nil, errors.BadRequest("invalid", "invalid group ID") + } + return &groupUUID, nil, nil + } else if groupRef.GetName() != "" { + groupName := groupRef.GetName() + return nil, &groupName, nil + } + return nil, nil, nil +} + +// initializePaginationOpts initializes the pagination options with the provided request pagination options. +func initializePaginationOpts(reqPagination *pb.OffsetPaginationRequest) (*pagination.OffsetPaginationOpts, error) { + // Initialize the pagination options, with default values + paginationOpts := pagination.NewDefaultOffsetPaginationOpts() + + var err error + // Override the pagination options if they are provided + if reqPagination != nil { + paginationOpts, err = pagination.NewOffsetPaginationOpts( + int(reqPagination.GetPage()), + int(reqPagination.GetPageSize()), + ) + if err != nil { + return nil, fmt.Errorf("failed to create pagination options: %w", err) + } + } + + return paginationOpts, nil +} + // RBAC feature is enabled if we are using a project scoped token or // it is a user with org role member func rbacEnabled(ctx context.Context) bool { diff --git a/app/controlplane/pkg/auditor/events/group.go b/app/controlplane/pkg/auditor/events/group.go index 1319bef31..4125e744f 100644 --- a/app/controlplane/pkg/auditor/events/group.go +++ b/app/controlplane/pkg/auditor/events/group.go @@ -28,13 +28,17 @@ var ( _ auditor.LogEntry = (*GroupCreated)(nil) _ auditor.LogEntry = (*GroupUpdated)(nil) _ auditor.LogEntry = (*GroupDeleted)(nil) + _ auditor.LogEntry = (*GroupMemberAdded)(nil) + _ auditor.LogEntry = (*GroupMemberRemoved)(nil) ) const ( - GroupType auditor.TargetType = "Group" - GroupCreatedActionType string = "GroupCreated" - GroupUpdatedActionType string = "GroupUpdated" - GroupDeletedActionType string = "GroupDeleted" + GroupType auditor.TargetType = "Group" + GroupCreatedActionType string = "GroupCreated" + GroupUpdatedActionType string = "GroupUpdated" + GroupDeletedActionType string = "GroupDeleted" + GroupMemberAddedActionType string = "GroupMemberAdded" + GroupMemberRemovedActionType string = "GroupMemberRemoved" ) // GroupBase is the base struct for group events @@ -132,3 +136,65 @@ func (g *GroupDeleted) ActionInfo() (json.RawMessage, error) { func (g *GroupDeleted) Description() string { return fmt.Sprintf("{{ if .ActorEmail }}{{ .ActorEmail }}{{ else }}API Token {{ .ActorID }}{{ end }} has deleted the group %s", g.GroupName) } + +// GroupMemberAdded represents the addition of a member to a group +type GroupMemberAdded struct { + *GroupBase + UserID *uuid.UUID `json:"user_id,omitempty"` + UserEmail string `json:"user_email,omitempty"` + Maintainer bool `json:"maintainer,omitempty"` +} + +func (g *GroupMemberAdded) ActionType() string { + return GroupMemberAddedActionType +} + +func (g *GroupMemberAdded) ActionInfo() (json.RawMessage, error) { + if _, err := g.GroupBase.ActionInfo(); err != nil { + return nil, err + } + + if g.UserID == nil { + return nil, fmt.Errorf("user ID is required") + } + + return json.Marshal(&g) +} + +func (g *GroupMemberAdded) Description() string { + maintainerStatus := "" + if g.Maintainer { + maintainerStatus = " as a maintainer" + } + + return fmt.Sprintf("{{ if .ActorEmail }}{{ .ActorEmail }}{{ else }}API Token {{ .ActorID }}{{ end }} has added user %s to the group %s%s", + g.UserEmail, g.GroupName, maintainerStatus) +} + +// GroupMemberRemoved represents the removal of a member from a group +type GroupMemberRemoved struct { + *GroupBase + UserID *uuid.UUID `json:"user_id,omitempty"` + UserEmail string `json:"user_email,omitempty"` +} + +func (g *GroupMemberRemoved) ActionType() string { + return GroupMemberRemovedActionType +} + +func (g *GroupMemberRemoved) ActionInfo() (json.RawMessage, error) { + if _, err := g.GroupBase.ActionInfo(); err != nil { + return nil, err + } + + if g.UserID == nil { + return nil, fmt.Errorf("user ID is required") + } + + return json.Marshal(&g) +} + +func (g *GroupMemberRemoved) Description() string { + return fmt.Sprintf("{{ if .ActorEmail }}{{ .ActorEmail }}{{ else }}API Token {{ .ActorID }}{{ end }} has removed user %s from the group %s", + g.UserEmail, g.GroupName) +} diff --git a/app/controlplane/pkg/auditor/events/group_test.go b/app/controlplane/pkg/auditor/events/group_test.go index 0255281db..523d3b6e4 100644 --- a/app/controlplane/pkg/auditor/events/group_test.go +++ b/app/controlplane/pkg/auditor/events/group_test.go @@ -36,10 +36,11 @@ func TestGroupEvents(t *testing.T) { require.NoError(t, err) groupUUID, err := uuid.Parse("3089bb36-e27b-428b-8009-d015c8737c56") require.NoError(t, err) + memberUUID, err := uuid.Parse("4089bb36-e27b-428b-8009-d015c8737c57") + require.NoError(t, err) groupName := "test-group" groupDescription := "test description" - oldGroupName := "old-group-name" - newGroupName := "new-group-name" + userEmail := "test@example.com" tests := []struct { name string @@ -88,28 +89,43 @@ func TestGroupEvents(t *testing.T) { actorID: userUUID, }, { - name: "Group updated with name change by user", - event: &events.GroupUpdated{ + name: "Group deleted by user", + event: &events.GroupDeleted{ GroupBase: &events.GroupBase{ GroupID: &groupUUID, - GroupName: newGroupName, + GroupName: groupName, }, - OldName: &oldGroupName, - NewName: &newGroupName, }, - expected: "testdata/groups/group_renamed.json", + expected: "testdata/groups/group_deleted.json", actor: auditor.ActorTypeUser, actorID: userUUID, }, { - name: "Group deleted by user", - event: &events.GroupDeleted{ + name: "Group member added by user", + event: &events.GroupMemberAdded{ GroupBase: &events.GroupBase{ GroupID: &groupUUID, GroupName: groupName, }, + UserID: &memberUUID, + UserEmail: userEmail, + Maintainer: true, }, - expected: "testdata/groups/group_deleted.json", + expected: "testdata/groups/group_member_added.json", + actor: auditor.ActorTypeUser, + actorID: userUUID, + }, + { + name: "Group member removed by user", + event: &events.GroupMemberRemoved{ + GroupBase: &events.GroupBase{ + GroupID: &groupUUID, + GroupName: groupName, + }, + UserID: &memberUUID, + UserEmail: userEmail, + }, + expected: "testdata/groups/group_member_removed.json", actor: auditor.ActorTypeUser, actorID: userUUID, }, @@ -157,6 +173,8 @@ func TestGroupEvents(t *testing.T) { func TestGroupEventsFailed(t *testing.T) { groupUUID, err := uuid.Parse("3089bb36-e27b-428b-8009-d015c8737c56") require.NoError(t, err) + memberUUID, err := uuid.Parse("4089bb36-e27b-428b-8009-d015c8737c57") + require.NoError(t, err) groupDescription := "test description" tests := []struct { @@ -222,6 +240,75 @@ func TestGroupEventsFailed(t *testing.T) { }, expectedErr: "group id and name are required", }, + { + name: "Group member added with missing GroupID", + event: &events.GroupMemberAdded{ + GroupBase: &events.GroupBase{ + GroupName: "test-group", + }, + UserID: &memberUUID, + UserEmail: "test@example.com", + Maintainer: true, + }, + expectedErr: "group id and name are required", + }, + { + name: "Group member added with missing GroupName", + event: &events.GroupMemberAdded{ + GroupBase: &events.GroupBase{ + GroupID: &groupUUID, + }, + UserID: &memberUUID, + UserEmail: "test@example.com", + Maintainer: true, + }, + expectedErr: "group id and name are required", + }, + { + name: "Group member added with missing UserID", + event: &events.GroupMemberAdded{ + GroupBase: &events.GroupBase{ + GroupID: &groupUUID, + GroupName: "test-group", + }, + UserEmail: "test@example.com", + Maintainer: true, + }, + expectedErr: "user ID is required", + }, + { + name: "Group member removed with missing GroupID", + event: &events.GroupMemberRemoved{ + GroupBase: &events.GroupBase{ + GroupName: "test-group", + }, + UserID: &memberUUID, + UserEmail: "test@example.com", + }, + expectedErr: "group id and name are required", + }, + { + name: "Group member removed with missing GroupName", + event: &events.GroupMemberRemoved{ + GroupBase: &events.GroupBase{ + GroupID: &groupUUID, + }, + UserID: &memberUUID, + UserEmail: "test@example.com", + }, + expectedErr: "group id and name are required", + }, + { + name: "Group member removed with missing UserID", + event: &events.GroupMemberRemoved{ + GroupBase: &events.GroupBase{ + GroupID: &groupUUID, + GroupName: "test-group", + }, + UserEmail: "test@example.com", + }, + expectedErr: "user ID is required", + }, } for _, tt := range tests { diff --git a/app/controlplane/pkg/auditor/events/testdata/groups/group_member_added.json b/app/controlplane/pkg/auditor/events/testdata/groups/group_member_added.json new file mode 100644 index 000000000..0ce776e3b --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/groups/group_member_added.json @@ -0,0 +1,18 @@ +{ + "ActionType": "GroupMemberAdded", + "TargetType": "Group", + "TargetID": "3089bb36-e27b-428b-8009-d015c8737c56", + "ActorType": "USER", + "ActorID": "1089bb36-e27b-428b-8009-d015c8737c54", + "ActorEmail": "john@cyberdyne.io", + "OrgID": "1089bb36-e27b-428b-8009-d015c8737c54", + "Description": "john@cyberdyne.io has added user test@example.com to the group test-group as a maintainer", + "Info": { + "group_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "group_name": "test-group", + "user_id": "4089bb36-e27b-428b-8009-d015c8737c57", + "user_email": "test@example.com", + "maintainer": true + }, + "Digest": "sha256:49a9a6fc6e737fcce1135c1add8fc6f4ff9d114a199547089b5f1aa61b994b79" +} \ No newline at end of file diff --git a/app/controlplane/pkg/auditor/events/testdata/groups/group_member_removed.json b/app/controlplane/pkg/auditor/events/testdata/groups/group_member_removed.json new file mode 100644 index 000000000..252b6fd06 --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/groups/group_member_removed.json @@ -0,0 +1,17 @@ +{ + "ActionType": "GroupMemberRemoved", + "TargetType": "Group", + "TargetID": "3089bb36-e27b-428b-8009-d015c8737c56", + "ActorType": "USER", + "ActorID": "1089bb36-e27b-428b-8009-d015c8737c54", + "ActorEmail": "john@cyberdyne.io", + "OrgID": "1089bb36-e27b-428b-8009-d015c8737c54", + "Description": "john@cyberdyne.io has removed user test@example.com from the group test-group", + "Info": { + "group_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "group_name": "test-group", + "user_id": "4089bb36-e27b-428b-8009-d015c8737c57", + "user_email": "test@example.com" + }, + "Digest": "sha256:6ba5c5f01dd8fd312e5cb50ba7e947ba7c478a1663857d234817772be609b256" +} \ No newline at end of file diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 757eaf5b6..c719e6aad 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -71,6 +71,9 @@ const ( RoleProjectAdmin Role = "role:project:admin" RoleProjectViewer Role = "role:project:viewer" + + // RoleGroupMaintainer is a role that can manage groups in an organization. + RoleGroupMaintainer Role = "role:group:maintainer" ) // ManagedResources are the resources that are managed by Chainloop, considered during permissions sync @@ -141,7 +144,9 @@ var ( PolicyGroupList = &Policy{ResourceGroup, ActionList} PolicyGroupRead = &Policy{ResourceGroup, ActionRead} // Group Memberships - PolicyGroupListMemberships = &Policy{ResourceGroupMembership, ActionList} + PolicyGroupListMemberships = &Policy{ResourceGroupMembership, ActionList} + PolicyGroupAddMemberships = &Policy{ResourceGroupMembership, ActionCreate} + PolicyGroupRemoveMemberships = &Policy{ResourceGroupMembership, ActionDelete} // Project API Token PolicyProjectAPITokenList = &Policy{ResourceProjectAPIToken, ActionList} PolicyProjectAPITokenCreate = &Policy{ResourceProjectAPIToken, ActionCreate} @@ -182,6 +187,11 @@ var RolesMap = map[Role][]*Policy{ PolicyWorkflowRead, // Organization PolicyOrganizationRead, + // Groups + PolicyGroupList, + PolicyGroupRead, + // Group Memberships + PolicyGroupListMemberships, }, // RoleAdmin is an org-scoped role that provides super admin privileges (it's the higher role) RoleAdmin: { @@ -271,6 +281,11 @@ var RolesMap = map[Role][]*Policy{ PolicyProjectAPITokenCreate, PolicyProjectAPITokenRevoke, }, + // RoleGroupMaintainer: represents a group maintainer role. + RoleGroupMaintainer: { + PolicyGroupAddMemberships, + PolicyGroupRemoveMemberships, + }, } // ServerOperationsMap is a map of server operations to the ResourceAction tuples that are @@ -334,13 +349,13 @@ var ServerOperationsMap = map[string][]*Policy{ "/controlplane.v1.OrganizationService/ListMemberships": {PolicyOrganizationListMemberships}, // Groups - "/controlplane.v1.GroupService/List": {PolicyGroupList}, - "/controlplane.v1.GroupService/Get": {PolicyGroupRead}, - "/controlplane.v1.GroupService/Create": {PolicyGroupCreate}, - "/controlplane.v1.GroupService/Update": {PolicyGroupUpdate}, - "/controlplane.v1.GroupService/Delete": {PolicyGroupDelete}, + "/controlplane.v1.GroupService/List": {PolicyGroupList}, // Group Memberships "/controlplane.v1.GroupService/ListMembers": {PolicyGroupListMemberships}, + // For the following endpoints, we rely on the service layer to check the permissions + // That's why we let everyone access them (empty policies) + "/controlplane.v1.GroupService/AddMember": {}, + "/controlplane.v1.GroupService/RemoveMember": {}, // Project API Token "/controlplane.v1.ProjectService/APITokenCreate": {PolicyProjectAPITokenCreate}, "/controlplane.v1.ProjectService/APITokenList": {PolicyProjectAPITokenList}, @@ -359,6 +374,7 @@ func (Role) Values() (roles []string) { RoleOrgMember, RoleProjectAdmin, RoleProjectViewer, + RoleGroupMaintainer, } { roles = append(roles, string(s)) } diff --git a/app/controlplane/pkg/authz/membership.go b/app/controlplane/pkg/authz/membership.go index f59a102f9..2006689e8 100644 --- a/app/controlplane/pkg/authz/membership.go +++ b/app/controlplane/pkg/authz/membership.go @@ -27,6 +27,7 @@ const ( ResourceTypeOrganization ResourceType = "organization" ResourceTypeProject ResourceType = "project" + ResourceTypeGroup ResourceType = "group" ) // Values implement https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues @@ -44,6 +45,7 @@ func (ResourceType) Values() (values []string) { values = append(values, string(ResourceTypeOrganization), string(ResourceTypeProject), + string(ResourceTypeGroup), ) return diff --git a/app/controlplane/pkg/biz/biz.go b/app/controlplane/pkg/biz/biz.go index 675a56282..32501690b 100644 --- a/app/controlplane/pkg/biz/biz.go +++ b/app/controlplane/pkg/biz/biz.go @@ -22,6 +22,8 @@ import ( "regexp" "strings" + "github.com/google/uuid" + "github.com/google/wire" "k8s.io/apimachinery/pkg/util/validation" ) @@ -60,6 +62,14 @@ var ProviderSet = wire.NewSet( wire.Struct(new(NewUserUseCaseParams), "*"), ) +// IdentityReference represents a reference to an identity, which can be any entity in the system. +type IdentityReference struct { + // ID is the unique identifier of the identity + ID *uuid.UUID + // Name is the name of the identity + Name *string +} + // generate a DNS1123-valid random name using moby's namesgenerator // plus an additional random number func generateValidDNS1123WithSuffix(prefix string) (string, error) { diff --git a/app/controlplane/pkg/biz/group.go b/app/controlplane/pkg/biz/group.go index fcd1d289f..19c9de140 100644 --- a/app/controlplane/pkg/biz/group.go +++ b/app/controlplane/pkg/biz/group.go @@ -36,10 +36,16 @@ type GroupRepo interface { Update(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, opts *UpdateGroupOpts) (*Group, error) // FindByOrgAndID finds a group by its organization ID and group ID. FindByOrgAndID(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID) (*Group, error) + // FindGroupMembershipByGroupAndID finds a group membership by group ID and user ID. + FindGroupMembershipByGroupAndID(ctx context.Context, groupID uuid.UUID, userID uuid.UUID) (*GroupMembership, error) // SoftDelete soft-deletes a group by marking it as deleted. SoftDelete(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID) error // ListMembers retrieves a list of members in a group, optionally filtered by maintainer status. ListMembers(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, opts *ListMembersOpts, paginationOpts *pagination.OffsetPaginationOpts) ([]*GroupMembership, int, error) + // AddMemberToGroup adds a user to a group, optionally specifying if they are a maintainer. + AddMemberToGroup(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID, maintainer bool) (*GroupMembership, error) + // RemoveMemberFromGroup removes a user from a group. + RemoveMemberFromGroup(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID) error } // GroupMembership represents a membership of a user in a group. @@ -84,13 +90,15 @@ type CreateGroupOpts struct { UserID uuid.UUID } +// UpdateGroupOpts defines options for updating a group. type UpdateGroupOpts struct { - // Description is the new description of the group. - Description *string - // Name is the new name of the group. - Name *string + // NewDescription is the new description of the group. + NewDescription *string + // NewName is the new name of the group. + NewName *string } +// ListGroupOpts defines options for listing groups. type ListGroupOpts struct { // Name is the name of the group to filter by. Name string @@ -102,12 +110,33 @@ type ListGroupOpts struct { // ListMembersOpts defines options for listing members of a group. type ListMembersOpts struct { + *IdentityReference // Maintainers indicate whether to filter the members by their maintainer status. Maintainers *bool // MemberEmail is the email of the member to filter by. MemberEmail *string } +// AddMemberToGroupOpts defines options for adding a member to a group. +type AddMemberToGroupOpts struct { + *IdentityReference + // UserEmail is the email of the user to add to the group. + UserEmail string + // RequesterID is the ID of the user who is requesting to add the member. Must be a maintainer. + RequesterID uuid.UUID + // Maintainer indicates if the new member should be a maintainer. + Maintainer bool +} + +// RemoveMemberFromGroupOpts defines options for removing a member from a group. +type RemoveMemberFromGroupOpts struct { + *IdentityReference + // UserEmail is the email of the user to remove from the group. + UserEmail string + // RequesterID is the ID of the user who is requesting to remove the member. Must be a maintainer. + RequesterID uuid.UUID +} + type GroupUseCase struct { // logger is used to log messages. logger *log.Helper @@ -136,13 +165,23 @@ func (uc *GroupUseCase) List(ctx context.Context, orgID uuid.UUID, filterOpts *L return uc.groupRepo.List(ctx, orgID, filterOpts, pgOpts) } -func (uc *GroupUseCase) ListMembers(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, maintainers *bool, memberEmail *string, paginationOpts *pagination.OffsetPaginationOpts) ([]*GroupMembership, int, error) { +// ListMembers retrieves a list of members in a group, optionally filtered by maintainer status and email. +func (uc *GroupUseCase) ListMembers(ctx context.Context, orgID uuid.UUID, opts *ListMembersOpts, paginationOpts *pagination.OffsetPaginationOpts) ([]*GroupMembership, int, error) { + if opts == nil { + return nil, 0, NewErrValidationStr("options cannot be nil") + } + + resolvedGroupID, err := uc.ValidateGroupIdentifier(ctx, orgID, opts.ID, opts.Name) + if err != nil { + return nil, 0, err + } + pgOpts := pagination.NewDefaultOffsetPaginationOpts() if paginationOpts != nil { pgOpts = paginationOpts } - return uc.groupRepo.ListMembers(ctx, orgID, groupID, &ListMembersOpts{Maintainers: maintainers, MemberEmail: memberEmail}, pgOpts) + return uc.groupRepo.ListMembers(ctx, orgID, resolvedGroupID, opts, pgOpts) } // Create creates a new group in the organization. @@ -184,14 +223,48 @@ func (uc *GroupUseCase) Create(ctx context.Context, orgID uuid.UUID, name string return group, nil } -// Update updates an existing group in the organization. -func (uc *GroupUseCase) Update(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, description *string, name *string) (*Group, error) { - if orgID == uuid.Nil || groupID == uuid.Nil { - return nil, NewErrValidationStr("organization ID and group ID cannot be empty") +// Get retrieves a group by its organization ID and either group ID or group name. +func (uc *GroupUseCase) Get(ctx context.Context, orgID uuid.UUID, opts *IdentityReference) (*Group, error) { + if opts == nil { + return nil, NewErrValidationStr("options cannot be nil") + } + + if orgID == uuid.Nil { + return nil, NewErrValidationStr("organization ID cannot be empty") + } + + resolvedGroupID, err := uc.ValidateGroupIdentifier(ctx, orgID, opts.ID, opts.Name) + if err != nil { + return nil, err + } + + group, err := uc.groupRepo.FindByOrgAndID(ctx, orgID, resolvedGroupID) + if err != nil { + return nil, fmt.Errorf("failed to find group: %w", err) + } else if group == nil { + return nil, NewErrNotFound("group") + } + + return group, nil +} + +// Update updates an existing group in the organization using the provided options. +func (uc *GroupUseCase) Update(ctx context.Context, orgID uuid.UUID, idReference *IdentityReference, opts *UpdateGroupOpts) (*Group, error) { + if opts == nil { + return nil, NewErrValidationStr("options cannot be nil") + } + + if orgID == uuid.Nil { + return nil, NewErrValidationStr("organization ID cannot be empty") + } + + resolvedGroupID, err := uc.ValidateGroupIdentifier(ctx, orgID, idReference.ID, idReference.Name) + if err != nil { + return nil, err } // Check the group exists - existingGroup, err := uc.groupRepo.FindByOrgAndID(ctx, orgID, groupID) + existingGroup, err := uc.groupRepo.FindByOrgAndID(ctx, orgID, resolvedGroupID) if err != nil { return nil, fmt.Errorf("failed to find group: %w", err) } @@ -200,9 +273,9 @@ func (uc *GroupUseCase) Update(ctx context.Context, orgID uuid.UUID, groupID uui return nil, NewErrNotFound("group") } - updatedGroup, err := uc.groupRepo.Update(ctx, orgID, groupID, &UpdateGroupOpts{ - Description: description, - Name: name, + updatedGroup, err := uc.groupRepo.Update(ctx, orgID, resolvedGroupID, &UpdateGroupOpts{ + NewDescription: opts.NewDescription, + NewName: opts.NewName, }) if err != nil { return nil, fmt.Errorf("failed to update group: %w", err) @@ -214,13 +287,13 @@ func (uc *GroupUseCase) Update(ctx context.Context, orgID uuid.UUID, groupID uui GroupID: &updatedGroup.ID, GroupName: updatedGroup.Name, }, - NewDescription: description, + NewDescription: opts.NewDescription, } // Add old and new name only if the name was changed - if name != nil && existingGroup.Name != *name { + if opts.NewName != nil && existingGroup.Name != *opts.NewName { event.OldName = &existingGroup.Name - event.NewName = name + event.NewName = opts.NewName } uc.auditorUC.Dispatch(ctx, event, &orgID) @@ -228,30 +301,129 @@ func (uc *GroupUseCase) Update(ctx context.Context, orgID uuid.UUID, groupID uui return updatedGroup, nil } -// FindByOrgAndID retrieves a group by its organization ID and group ID. -func (uc *GroupUseCase) FindByOrgAndID(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID) (*Group, error) { - if orgID == uuid.Nil || groupID == uuid.Nil { - return nil, NewErrValidationStr("organization ID and group ID cannot be empty") +// Delete soft-deletes a group by marking it as deleted using the provided options. +func (uc *GroupUseCase) Delete(ctx context.Context, orgID uuid.UUID, opts *IdentityReference) error { + if opts == nil { + return NewErrValidationStr("options cannot be nil") + } + + if orgID == uuid.Nil { + return NewErrValidationStr("organization ID cannot be empty") + } + + resolvedGroupID, err := uc.ValidateGroupIdentifier(ctx, orgID, opts.ID, opts.Name) + if err != nil { + return err + } + + // Check the group exists + existingGroup, err := uc.groupRepo.FindByOrgAndID(ctx, orgID, resolvedGroupID) + if err != nil { + return fmt.Errorf("failed to find group: %w", err) + } + + if existingGroup == nil { + return NewErrNotFound("group") + } + + if err := uc.groupRepo.SoftDelete(ctx, orgID, resolvedGroupID); err != nil { + return fmt.Errorf("failed to soft-delete group: %w", err) + } + + // Dispatch event to the audit log for group deletion + uc.auditorUC.Dispatch(ctx, &events.GroupDeleted{ + GroupBase: &events.GroupBase{ + GroupID: &existingGroup.ID, + GroupName: existingGroup.Name, + }, + }, &orgID) + + return nil +} + +// AddMemberToGroup adds a user to a group. +// The requester must be either a maintainer of the group or have RoleOwner/RoleAdmin in the organization. +func (uc *GroupUseCase) AddMemberToGroup(ctx context.Context, orgID uuid.UUID, opts *AddMemberToGroupOpts) (*GroupMembership, error) { + if opts == nil { + return nil, NewErrValidationStr("options cannot be nil") + } + + if orgID == uuid.Nil || opts.UserEmail == "" || opts.RequesterID == uuid.Nil { + return nil, NewErrValidationStr("organization ID, user email, and requester ID cannot be empty") } - group, err := uc.groupRepo.FindByOrgAndID(ctx, orgID, groupID) + resolvedGroupID, err := uc.ValidateGroupIdentifier(ctx, orgID, opts.ID, opts.Name) + if err != nil { + return nil, err + } + + // Check the group exists + existingGroup, err := uc.groupRepo.FindByOrgAndID(ctx, orgID, resolvedGroupID) if err != nil { return nil, fmt.Errorf("failed to find group: %w", err) - } else if group == nil { + } + + if existingGroup == nil { return nil, NewErrNotFound("group") } - return group, nil + // Find the user by email in the organization + userMembership, err := uc.membershipRepo.FindByOrgIDAndUserEmail(ctx, orgID, opts.UserEmail) + if err != nil && !IsNotFound(err) { + return nil, fmt.Errorf("failed to find user by email: %w", err) + } + if userMembership == nil { + return nil, NewErrValidationStr("user with the provided email is not a member of the organization") + } + + userUUID := uuid.MustParse(userMembership.User.ID) + + // Check if the user is already a member of the group + existingMembership, err := uc.groupRepo.FindGroupMembershipByGroupAndID(ctx, resolvedGroupID, userUUID) + if err != nil && !IsNotFound(err) { + return nil, fmt.Errorf("failed to check existing membership: %w", err) + } + if existingMembership != nil { + return nil, NewErrAlreadyExistsStr("user is already a member of this group") + } + + // Add the user to the group + membership, err := uc.groupRepo.AddMemberToGroup(ctx, orgID, resolvedGroupID, userUUID, opts.Maintainer) + if err != nil { + return nil, fmt.Errorf("failed to add member to group: %w", err) + } + + // Dispatch event to the audit log for group membership addition + uc.auditorUC.Dispatch(ctx, &events.GroupMemberAdded{ + GroupBase: &events.GroupBase{ + GroupID: &existingGroup.ID, + GroupName: existingGroup.Name, + }, + UserID: &userUUID, + Maintainer: opts.Maintainer, + }, &orgID) + + return membership, nil } -// SoftDelete marks a group as deleted by setting the DeletedAt timestamp. -func (uc *GroupUseCase) SoftDelete(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID) error { - if orgID == uuid.Nil || groupID == uuid.Nil { - return NewErrValidationStr("organization ID and group ID cannot be empty") +// RemoveMemberFromGroup removes a user from a group. +// The requester must be either a maintainer of the group or have RoleOwner/RoleAdmin in the organization. +func (uc *GroupUseCase) RemoveMemberFromGroup(ctx context.Context, orgID uuid.UUID, opts *RemoveMemberFromGroupOpts) error { + if opts == nil { + return NewErrValidationStr("options cannot be nil") + } + + if orgID == uuid.Nil || opts.UserEmail == "" || opts.RequesterID == uuid.Nil { + return NewErrValidationStr("organization ID, user email, and requester ID cannot be empty") + } + + resolvedGroupID, err := uc.ValidateGroupIdentifier(ctx, orgID, opts.ID, opts.Name) + if err != nil { + return err } // Check the group exists - existingGroup, err := uc.groupRepo.FindByOrgAndID(ctx, orgID, groupID) + existingGroup, err := uc.groupRepo.FindByOrgAndID(ctx, orgID, resolvedGroupID) if err != nil { return fmt.Errorf("failed to find group: %w", err) } @@ -260,17 +432,95 @@ func (uc *GroupUseCase) SoftDelete(ctx context.Context, orgID uuid.UUID, groupID return NewErrNotFound("group") } - if err := uc.groupRepo.SoftDelete(ctx, orgID, groupID); err != nil { - return fmt.Errorf("failed to soft-delete group: %w", err) + // Find the user by email in the organization + userMembership, err := uc.membershipRepo.FindByOrgIDAndUserEmail(ctx, orgID, opts.UserEmail) + if err != nil && !IsNotFound(err) { + return fmt.Errorf("failed to find user by email: %w", err) + } + if userMembership == nil { + return NewErrValidationStr("user with the provided email is not a member of the organization") } - // Dispatch event to the audit log for group deletion - uc.auditorUC.Dispatch(ctx, &events.GroupDeleted{ + // Check if the requester is part of the organization + membership, err := uc.groupRepo.FindGroupMembershipByGroupAndID(ctx, resolvedGroupID, opts.RequesterID) + if err != nil && !IsNotFound(err) { + return NewErrValidationStr("failed to check existing membership") + } + + if membership == nil { + return NewErrValidationStr("requester is not a member of the group") + } + + userUUID := uuid.MustParse(userMembership.User.ID) + // Check if the user is a member of the group + existingMembership, err := uc.groupRepo.FindGroupMembershipByGroupAndID(ctx, resolvedGroupID, userUUID) + if err != nil && !IsNotFound(err) { + return fmt.Errorf("failed to check existing membership: %w", err) + } + if existingMembership == nil { + return NewErrValidationStr("user is not a member of this group") + } + + // Remove the user from the group + if err := uc.groupRepo.RemoveMemberFromGroup(ctx, orgID, resolvedGroupID, userUUID); err != nil { + return fmt.Errorf("failed to remove member from group: %w", err) + } + + // Dispatch event to the audit log for group membership removal + uc.auditorUC.Dispatch(ctx, &events.GroupMemberRemoved{ GroupBase: &events.GroupBase{ GroupID: &existingGroup.ID, GroupName: existingGroup.Name, }, + UserID: &userUUID, }, &orgID) return nil } + +// IsUserGroupMaintainer checks if a user is a maintainer of a group. +func (uc *GroupUseCase) IsUserGroupMaintainer(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID) (bool, error) { + if orgID == uuid.Nil || groupID == uuid.Nil || userID == uuid.Nil { + return false, NewErrValidationStr("organization ID, group ID, and user ID cannot be empty") + } + + membership, err := uc.groupRepo.FindGroupMembershipByGroupAndID(ctx, groupID, userID) + if err != nil && !IsNotFound(err) { + return false, fmt.Errorf("failed to check group membership: %w", err) + } + + if membership == nil { + return false, nil // User is not a member of the group + } + + return membership.Maintainer, nil +} + +// ValidateGroupIdentifier validates and resolves the group ID or name to a group ID. +// Returns an error if both are nil or if the resolved group does not exist. +func (uc *GroupUseCase) ValidateGroupIdentifier(ctx context.Context, orgID uuid.UUID, groupID *uuid.UUID, groupName *string) (uuid.UUID, error) { + if groupID == nil && groupName == nil { + return uuid.Nil, NewErrValidationStr("either group ID or group name must be provided") + } + + if groupID != nil { + return *groupID, nil + } + + // If group ID is not provided, try to find the group by name + groups, _, err := uc.groupRepo.List(ctx, orgID, &ListGroupOpts{Name: *groupName}, pagination.NewDefaultOffsetPaginationOpts()) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to list groups: %w", err) + } + + if len(groups) == 0 { + return uuid.Nil, NewErrNotFound("group") + } + + // If the group name is not unique, return an error + if len(groups) > 1 { + return uuid.Nil, NewErrValidationStr("group name is not unique") + } + + return groups[0].ID, nil +} diff --git a/app/controlplane/pkg/biz/group_integration_test.go b/app/controlplane/pkg/biz/group_integration_test.go index fb23b9ace..dc80c13b9 100644 --- a/app/controlplane/pkg/biz/group_integration_test.go +++ b/app/controlplane/pkg/biz/group_integration_test.go @@ -181,7 +181,9 @@ func (s *groupIntegrationTestSuite) TestFindByID() { // Test finding the group by ID s.Run("find existing group", func() { - foundGroup, err := s.Group.FindByOrgAndID(ctx, uuid.MustParse(s.org.ID), group.ID) + foundGroup, err := s.Group.Get(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &group.ID, + }) s.NoError(err) s.NotNil(foundGroup) s.Equal(group.ID, foundGroup.ID) @@ -193,13 +195,18 @@ func (s *groupIntegrationTestSuite) TestFindByID() { org2, org2Err := s.Organization.CreateWithRandomName(ctx) require.NoError(s.T(), org2Err) - _, expectedErr := s.Group.FindByOrgAndID(ctx, uuid.MustParse(org2.ID), group.ID) + _, expectedErr := s.Group.Get(ctx, uuid.MustParse(org2.ID), &biz.IdentityReference{ + ID: &group.ID, + }) s.Error(expectedErr) s.True(biz.IsNotFound(expectedErr)) }) s.Run("try to find non-existent group", func() { - _, err := s.Group.FindByOrgAndID(ctx, uuid.MustParse(s.org.ID), uuid.New()) + id := uuid.New() // Generate a new UUID for a non-existent group + _, err := s.Group.Get(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &id, + }) s.Error(err) s.True(biz.IsNotFound(err)) }) @@ -217,12 +224,21 @@ func (s *groupIntegrationTestSuite) TestUpdate() { s.NoError(err) s.NotNil(group) + // Create a second group to test name uniqueness constraint + secondGroupName := "existing-group-name" + secondGroup, err := s.Group.Create(ctx, uuid.MustParse(s.org.ID), secondGroupName, "Second group description", uuid.MustParse(s.user.ID)) + s.NoError(err) + s.NotNil(secondGroup) + // Test updating the group s.Run("update description", func() { newDescription := "Updated description" - descPtr := &newDescription - updatedGroup, err := s.Group.Update(ctx, uuid.MustParse(s.org.ID), group.ID, descPtr, nil) + updatedGroup, err := s.Group.Update(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &group.ID, + }, &biz.UpdateGroupOpts{ + NewDescription: &newDescription, + }) s.NoError(err) s.NotNil(updatedGroup) @@ -230,14 +246,37 @@ func (s *groupIntegrationTestSuite) TestUpdate() { s.Equal(name, updatedGroup.Name) // Name should not change }) + s.Run("try to update name to an existing group name", func() { + // Try to update the first group's name to match the second group's name + _, dupErr := s.Group.Update(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &group.ID, + }, &biz.UpdateGroupOpts{ + NewName: &secondGroupName, + }) + + s.Error(dupErr) + s.True(biz.IsErrAlreadyExists(dupErr), "Expected an 'already exists' error") + s.Contains(dupErr.Error(), "already exists", "Error should indicate name already exists") + + // Verify the group name wasn't changed + unchangedGroup, err := s.Group.Get(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &group.ID, + }) + s.NoError(err) + s.Equal(name, unchangedGroup.Name, "Group name should remain unchanged after failed update") + }) + s.Run("try to update in wrong organization", func() { org2, err := s.Organization.CreateWithRandomName(ctx) require.NoError(s.T(), err) newDescription := "Updated description" - descPtr := &newDescription - _, err = s.Group.Update(ctx, uuid.MustParse(org2.ID), group.ID, descPtr, nil) + _, err = s.Group.Update(ctx, uuid.MustParse(org2.ID), &biz.IdentityReference{ + ID: &group.ID, + }, &biz.UpdateGroupOpts{ + NewDescription: &newDescription, + }) s.Error(err) s.True(biz.IsNotFound(err)) @@ -246,9 +285,12 @@ func (s *groupIntegrationTestSuite) TestUpdate() { s.Run("try to update non-existent group", func() { nonExistentGroupID := uuid.New() newDescription := "Updated description for non-existent group" - descPtr := &newDescription - _, err := s.Group.Update(ctx, uuid.MustParse(s.org.ID), nonExistentGroupID, descPtr, nil) + _, err := s.Group.Update(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &nonExistentGroupID, + }, &biz.UpdateGroupOpts{ + NewDescription: &newDescription, + }) s.Error(err) s.True(biz.IsNotFound(err)) @@ -269,11 +311,15 @@ func (s *groupIntegrationTestSuite) TestSoftDelete() { // Test deleting the group s.Run("delete existing group", func() { - err := s.Group.SoftDelete(ctx, uuid.MustParse(s.org.ID), group.ID) + err := s.Group.Delete(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &group.ID, + }) s.NoError(err) // Try to find it after deletion - _, err = s.Group.FindByOrgAndID(ctx, uuid.MustParse(s.org.ID), group.ID) + _, err = s.Group.Get(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &group.ID, + }) s.Error(err) s.True(biz.IsNotFound(err)) @@ -290,13 +336,18 @@ func (s *groupIntegrationTestSuite) TestSoftDelete() { group, err := s.Group.Create(ctx, uuid.MustParse(s.org.ID), "org-specific-group", description, uuid.MustParse(s.user.ID)) s.NoError(err) - err = s.Group.SoftDelete(ctx, uuid.MustParse(org2.ID), group.ID) + err = s.Group.Delete(ctx, uuid.MustParse(org2.ID), &biz.IdentityReference{ + ID: &group.ID, + }) s.Error(err) s.True(biz.IsNotFound(err)) }) s.Run("try to delete non-existent group", func() { - err := s.Group.SoftDelete(ctx, uuid.MustParse(s.org.ID), uuid.New()) + nonExistentGroupID := uuid.New() + err := s.Group.Delete(ctx, uuid.MustParse(s.org.ID), &biz.IdentityReference{ + ID: &nonExistentGroupID, + }) s.Error(err) s.True(biz.IsNotFound(err)) }) @@ -484,12 +535,6 @@ func (s *groupMembersIntegrationTestSuite) SetupTest() { assert.NoError(err) } -func (s *groupMembersIntegrationTestSuite) TearDownTest() { - ctx := context.Background() - // Clean up the database after each test - _, _ = s.Data.DB.Group.Delete().Exec(ctx) -} - // Test group membership operations func (s *groupMembersIntegrationTestSuite) TestListMembers() { ctx := context.Background() @@ -508,7 +553,13 @@ func (s *groupMembersIntegrationTestSuite) TestListMembers() { require.NoError(s.T(), err) s.Run("initial group has creator as maintainer", func() { - members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), s.group.ID, nil, nil, nil) + groupID := &s.group.ID + opts := &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: groupID, + }, + } + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), opts, nil) s.NoError(err) s.Equal(1, len(members)) s.Equal(1, count) @@ -516,33 +567,47 @@ func (s *groupMembersIntegrationTestSuite) TestListMembers() { s.True(members[0].Maintainer) }) - // TODO: Add tests for adding members to groups once that functionality is implemented - s.Run("filter members by maintainer status", func() { + groupID := &s.group.ID isTrue := true - members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), s.group.ID, &isTrue, nil, nil) + opts := &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: groupID, + }, + Maintainers: &isTrue, + } + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), opts, nil) s.NoError(err) s.Equal(1, len(members)) s.Equal(1, count) s.True(members[0].Maintainer) isFalse := false - members, count, err = s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), s.group.ID, &isFalse, nil, nil) + opts.Maintainers = &isFalse + members, count, err = s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), opts, nil) s.NoError(err) s.Equal(0, len(members)) s.Equal(0, count) }) s.Run("filter members by email", func() { + groupID := &s.group.ID email := s.user.Email - members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), s.group.ID, nil, &email, nil) + opts := &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: groupID, + }, + MemberEmail: &email, + } + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), opts, nil) s.NoError(err) s.Equal(1, len(members)) s.Equal(1, count) s.Equal(s.user.Email, members[0].User.Email) nonExistentEmail := "nonexistent@example.com" - members, count, err = s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), s.group.ID, nil, &nonExistentEmail, nil) + opts.MemberEmail = &nonExistentEmail + members, count, err = s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), opts, nil) s.NoError(err) s.Equal(0, len(members)) s.Equal(0, count) @@ -550,13 +615,433 @@ func (s *groupMembersIntegrationTestSuite) TestListMembers() { s.Run("list members with pagination", func() { // TODO: Add more members to the group once that functionality is implemented - + groupID := &s.group.ID + opts := &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: groupID, + }, + } paginationOpts, err := pagination.NewOffsetPaginationOpts(0, 1) require.NoError(s.T(), err) - members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), s.group.ID, nil, nil, paginationOpts) + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), opts, paginationOpts) s.NoError(err) s.Equal(1, len(members)) s.Equal(1, count) }) + + s.Run("list members with group name", func() { + groupName := s.group.Name + opts := &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + Name: &groupName, + }, + } + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), opts, nil) + s.NoError(err) + s.Equal(1, len(members)) + s.Equal(1, count) + s.Equal(s.user.ID, members[0].User.ID) + }) +} + +// Test adding members to groups +func (s *groupMembersIntegrationTestSuite) TestAddMemberToGroup() { + ctx := context.Background() + + // Create additional users + user2, err := s.User.UpsertByEmail(ctx, "add-user2@example.com", nil) + require.NoError(s.T(), err) + + user3, err := s.User.UpsertByEmail(ctx, "add-user3@example.com", nil) + require.NoError(s.T(), err) + + // Add users to organization + _, err = s.Membership.Create(ctx, s.org.ID, user2.ID) + require.NoError(s.T(), err) + _, err = s.Membership.Create(ctx, s.org.ID, user3.ID) + require.NoError(s.T(), err) + + s.Run("add member using group ID", func() { + // Add user2 as a regular member + // Create options for adding member + opts := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "add-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), // The creator is a maintainer + Maintainer: false, + } + + membership, err := s.Group.AddMemberToGroup(ctx, uuid.MustParse(s.org.ID), opts) + s.NoError(err) + s.NotNil(membership) + s.Equal(user2.ID, membership.User.ID) + s.False(membership.Maintainer) + + // Verify the member was added by listing members + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + }, nil) + s.NoError(err) + s.Equal(2, len(members)) + s.Equal(2, count) + }) + + s.Run("add member using group name", func() { + // Add user3 as a maintainer + groupName := s.group.Name + opts := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + Name: &groupName, + }, + UserEmail: "add-user3@example.com", + RequesterID: uuid.MustParse(s.user.ID), // The creator is a maintainer + Maintainer: true, + } + + membership, err := s.Group.AddMemberToGroup(ctx, uuid.MustParse(s.org.ID), opts) + s.NoError(err) + s.NotNil(membership) + s.Equal(user3.ID, membership.User.ID) + s.True(membership.Maintainer) + + // Verify the member was added by listing members + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + }, nil) + s.NoError(err) + s.Equal(3, len(members)) + s.Equal(3, count) + }) + + s.Run("add member to group in wrong organization", func() { + // Create a new organization + org2, err := s.Organization.CreateWithRandomName(ctx) + require.NoError(s.T(), err) + + // Attempt to add user2 to a group in the wrong organization + opts := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "add-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Maintainer: false, + } + + _, err = s.Group.AddMemberToGroup(ctx, uuid.MustParse(org2.ID), opts) + s.Error(err) + s.True(biz.IsNotFound(err)) + }) + + s.Run("add member to non-existent group", func() { + nonExistentGroupID := uuid.New() + opts := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &nonExistentGroupID, + }, + UserEmail: "add-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Maintainer: false, + } + + _, err := s.Group.AddMemberToGroup(ctx, uuid.MustParse(s.org.ID), opts) + s.Error(err) + s.True(biz.IsNotFound(err)) + }) + + s.Run("add member who is not in the organization", func() { + // Create user who is not in the organization + _, err := s.User.UpsertByEmail(ctx, "not-in-org@example.com", nil) + require.NoError(s.T(), err) + // Note: not adding this user to the organization + + opts := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "not-in-org@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Maintainer: false, + } + + _, err = s.Group.AddMemberToGroup(ctx, uuid.MustParse(s.org.ID), opts) + s.Error(err) + s.ErrorContains(err, "user with the provided email is not a member of the organization") + }) + + s.Run("add member who is already in the group", func() { + // Try to add user2 again (who we added in the first test) + opts := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "add-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Maintainer: true, + } + + _, err := s.Group.AddMemberToGroup(ctx, uuid.MustParse(s.org.ID), opts) + s.Error(err) + s.True(biz.IsErrAlreadyExists(err)) + + // Verify the number of members hasn't changed + _, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + }, nil) + s.NoError(err) + s.Equal(3, count) // still the original 3 members + }) +} + +// Test removing members from groups +func (s *groupMembersIntegrationTestSuite) TestRemoveMemberFromGroup() { + ctx := context.Background() + + // Create additional users + user2, err := s.User.UpsertByEmail(ctx, "remove-user2@example.com", nil) + require.NoError(s.T(), err) + + user3, err := s.User.UpsertByEmail(ctx, "remove-user3@example.com", nil) + require.NoError(s.T(), err) + + user4, err := s.User.UpsertByEmail(ctx, "remove-user4@example.com", nil) + require.NoError(s.T(), err) + + // Add users to organization + _, err = s.Membership.Create(ctx, s.org.ID, user2.ID) + require.NoError(s.T(), err) + _, err = s.Membership.Create(ctx, s.org.ID, user3.ID) + require.NoError(s.T(), err) + _, err = s.Membership.Create(ctx, s.org.ID, user4.ID) + require.NoError(s.T(), err) + + // Add users to the group + opts1 := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "remove-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Maintainer: false, + } + _, err = s.Group.AddMemberToGroup(ctx, uuid.MustParse(s.org.ID), opts1) + require.NoError(s.T(), err) + + opts2 := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "remove-user3@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Maintainer: true, + } + _, err = s.Group.AddMemberToGroup(ctx, uuid.MustParse(s.org.ID), opts2) + require.NoError(s.T(), err) + + opts3 := &biz.AddMemberToGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "remove-user4@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Maintainer: false, + } + _, err = s.Group.AddMemberToGroup(ctx, uuid.MustParse(s.org.ID), opts3) + require.NoError(s.T(), err) + + // Verify initial member count + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + }, nil) + s.NoError(err) + s.Equal(4, len(members)) // creator + 3 added users + s.Equal(4, count) + + s.Run("remove a regular member from group", func() { + // Remove user2 (regular member) + removeOpts := &biz.RemoveMemberFromGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "remove-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), // Creator is a maintainer + } + + err := s.Group.RemoveMemberFromGroup(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.NoError(err) + + // Verify member was removed + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + }, nil) + s.NoError(err) + s.Equal(3, len(members)) + s.Equal(3, count) + + // Verify the removed user is not in the list + for _, member := range members { + s.NotEqual(user2.ID, member.User.ID) + } + }) + + s.Run("remove a maintainer from group", func() { + // Remove user3 (maintainer) + removeOpts := &biz.RemoveMemberFromGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "remove-user3@example.com", + RequesterID: uuid.MustParse(s.user.ID), // Creator is a maintainer + } + + err := s.Group.RemoveMemberFromGroup(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.NoError(err) + + // Verify member was removed + members, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + }, nil) + s.NoError(err) + s.Equal(2, len(members)) + s.Equal(2, count) + + // Check remaining members - user3 should not be present + for _, member := range members { + s.NotEqual(user3.ID, member.User.ID) + } + + // Verify we still have at least one maintainer (the original creator) + foundMaintainer := false + for _, member := range members { + if member.Maintainer { + foundMaintainer = true + break + } + } + s.True(foundMaintainer, "Group should still have at least one maintainer") + }) + + s.Run("try to remove non-existent member", func() { + // Create a user who's not in the group + nonMemberUser, err := s.User.UpsertByEmail(ctx, "non-member@example.com", nil) + require.NoError(s.T(), err) + _, err = s.Membership.Create(ctx, s.org.ID, nonMemberUser.ID) + require.NoError(s.T(), err) + + // Try to remove a user who's not in the group + removeOpts := &biz.RemoveMemberFromGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "non-member@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err = s.Group.RemoveMemberFromGroup(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.True(biz.IsErrValidation(err)) + + // Member count should remain unchanged + _, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + }, nil) + s.NoError(err) + s.Equal(2, count) + }) + + s.Run("remove member from wrong organization", func() { + // Create a new organization and group + org2, err := s.Organization.CreateWithRandomName(ctx) + require.NoError(s.T(), err) + + // Try to remove user4 using the wrong org ID + removeOpts := &biz.RemoveMemberFromGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "remove-user4@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err = s.Group.RemoveMemberFromGroup(ctx, uuid.MustParse(org2.ID), removeOpts) + s.Error(err) + s.True(biz.IsNotFound(err)) + + // Member count should remain unchanged + _, count, err := s.Group.ListMembers(ctx, uuid.MustParse(s.org.ID), &biz.ListMembersOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + }, nil) + s.NoError(err) + s.Equal(2, count) + }) + + s.Run("remove member from non-existent group", func() { + nonExistentGroupID := uuid.New() + removeOpts := &biz.RemoveMemberFromGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &nonExistentGroupID, + }, + UserEmail: "remove-user4@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err = s.Group.RemoveMemberFromGroup(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.True(biz.IsNotFound(err)) + }) + + s.Run("requester not part of organization", func() { + // Create a user who is not in any organization + externalUser, err := s.User.UpsertByEmail(ctx, "external-user@example.com", nil) + require.NoError(s.T(), err) + // Intentionally not adding to organization + + // Try to remove a member with an external user as requester + removeOpts := &biz.RemoveMemberFromGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "remove-user4@example.com", + RequesterID: uuid.MustParse(externalUser.ID), + } + + err = s.Group.RemoveMemberFromGroup(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + // The error message has changed with the new permissions logic + s.Contains(err.Error(), "requester is not a member of the group") + }) + + s.Run("non-existent user email", func() { + // Try to remove a non-existent user + removeOpts := &biz.RemoveMemberFromGroupOpts{ + IdentityReference: &biz.IdentityReference{ + ID: &s.group.ID, + }, + UserEmail: "non-existent-user@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err = s.Group.RemoveMemberFromGroup(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.Contains(err.Error(), "not a member of the organization") + }) } diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 383e609c8..86dbe2cb8 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -42,6 +42,7 @@ type Membership struct { type MembershipRepo interface { FindByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) + FindByOrgIDAndUserEmail(ctx context.Context, orgID uuid.UUID, userEmail string) (*Membership, error) FindByOrg(ctx context.Context, orgID uuid.UUID) ([]*Membership, error) FindByIDInUser(ctx context.Context, userID, ID uuid.UUID) (*Membership, error) FindByIDInOrg(ctx context.Context, orgID, ID uuid.UUID) (*Membership, error) diff --git a/app/controlplane/pkg/data/ent/membership/membership.go b/app/controlplane/pkg/data/ent/membership/membership.go index 80ba909a0..db3c87067 100644 --- a/app/controlplane/pkg/data/ent/membership/membership.go +++ b/app/controlplane/pkg/data/ent/membership/membership.go @@ -104,7 +104,7 @@ var ( // RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. func RoleValidator(r authz.Role) error { switch r { - case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer": + case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer", "role:group:maintainer": return nil default: return fmt.Errorf("membership: invalid enum value for role field: %q", r) @@ -124,7 +124,7 @@ func MembershipTypeValidator(mt authz.MembershipType) error { // ResourceTypeValidator is a validator for the "resource_type" field enum values. It is called by the builders before save. func ResourceTypeValidator(rt authz.ResourceType) error { switch rt { - case "organization", "project": + case "organization", "project", "group": return nil default: return fmt.Errorf("membership: invalid enum value for resource_type field: %q", rt) diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250627143634.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250627143634.sql new file mode 100644 index 000000000..ea75d13b1 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250627143634.sql @@ -0,0 +1,4 @@ +-- Drop index "groupmembership_group_id_user_id" from table: "group_memberships" +DROP INDEX "groupmembership_group_id_user_id"; +-- Create index "groupmembership_group_id_user_id" to table: "group_memberships" +CREATE UNIQUE INDEX "groupmembership_group_id_user_id" ON "group_memberships" ("group_id", "user_id") WHERE (deleted_at IS NULL); diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index e15137ba8..56e4015db 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:3SNfqyafKS1ph2bqLSbHRifLLf44uI81RtUuCMSkqKU= +h1:ec2ktM2lVwIXCIbc6AzBzRuLEdlmysXZxmhsAP8Z2P0= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -91,3 +91,4 @@ h1:3SNfqyafKS1ph2bqLSbHRifLLf44uI81RtUuCMSkqKU= 20250625150654.sql h1:bFDraxRNN2xnv3MwjGi0jiBbRNdqkrGSXFhyBFruEIw= 20250626061546.sql h1:9L1HkO9ReYaCRaeaA1gW4tRL3zlMztbgsn52Z4JzRog= 20250626100818.sql h1:MMCQid88eEs4uyCVBgkf5b0VYjyBd1EYRqRISco0rOI= +20250627143634.sql h1:9UdcOm4HdWyJ8bvU/vYkzCslXIpKSn7pM0l/MJHl8n4= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index 9f7bf175b..c4376371f 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -241,6 +241,9 @@ var ( Name: "groupmembership_group_id_user_id", Unique: true, Columns: []*schema.Column{GroupMembershipsColumns[5], GroupMembershipsColumns[6]}, + Annotation: &entsql.IndexAnnotation{ + Where: "deleted_at IS NULL", + }, }, }, } @@ -315,10 +318,10 @@ var ( {Name: "current", Type: field.TypeBool, Default: false}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "updated_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, - {Name: "role", Type: field.TypeEnum, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer"}}, + {Name: "role", Type: field.TypeEnum, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer", "role:group:maintainer"}}, {Name: "membership_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"user", "group"}}, {Name: "member_id", Type: field.TypeUUID, Nullable: true}, - {Name: "resource_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"organization", "project"}}, + {Name: "resource_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"organization", "project", "group"}}, {Name: "resource_id", Type: field.TypeUUID, Nullable: true}, {Name: "organization_memberships", Type: field.TypeUUID, Nullable: true}, {Name: "user_memberships", Type: field.TypeUUID, Nullable: true}, @@ -362,7 +365,7 @@ var ( {Name: "status", Type: field.TypeEnum, Enums: []string{"accepted", "pending"}, Default: "pending"}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "deleted_at", Type: field.TypeTime, Nullable: true}, - {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer"}}, + {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer", "role:group:maintainer"}}, {Name: "organization_id", Type: field.TypeUUID}, {Name: "sender_id", Type: field.TypeUUID}, } diff --git a/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go b/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go index 728230de1..c0cc5f531 100644 --- a/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go +++ b/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go @@ -98,7 +98,7 @@ func StatusValidator(s biz.OrgInvitationStatus) error { // RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. func RoleValidator(r authz.Role) error { switch r { - case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer": + case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer", "role:group:maintainer": return nil default: return fmt.Errorf("orginvitation: invalid enum value for role field: %q", r) diff --git a/app/controlplane/pkg/data/ent/schema/group_membership.go b/app/controlplane/pkg/data/ent/schema/group_membership.go index 4301f3c6d..c1100fae7 100644 --- a/app/controlplane/pkg/data/ent/schema/group_membership.go +++ b/app/controlplane/pkg/data/ent/schema/group_membership.go @@ -18,6 +18,8 @@ package schema import ( "time" + "entgo.io/ent/schema/index" + "entgo.io/ent" "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema/edge" @@ -67,3 +69,12 @@ func (GroupMembership) Edges() []ent.Edge { Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), } } + +// Indexes of the GroupMembership. +func (GroupMembership) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("group_id", "user_id").Unique().Annotations( + entsql.IndexWhere("deleted_at IS NULL"), + ), + } +} diff --git a/app/controlplane/pkg/data/group.go b/app/controlplane/pkg/data/group.go index f9b17fc0a..01cdfc79f 100644 --- a/app/controlplane/pkg/data/group.go +++ b/app/controlplane/pkg/data/group.go @@ -20,10 +20,13 @@ import ( "fmt" "time" + "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/data/ent" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/group" "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/user" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" @@ -160,7 +163,7 @@ func (g GroupRepo) Create(ctx context.Context, orgID uuid.UUID, opts *biz.Create Save(ctx) if err != nil { if ent.IsConstraintError(err) { - return biz.NewErrAlreadyExists(err) + return biz.NewErrAlreadyExistsStr("group with the same name already exists") } return err } @@ -179,6 +182,20 @@ func (g GroupRepo) Create(ctx context.Context, orgID uuid.UUID, opts *biz.Create return grUerr } + // Update the user membership with the role of maintainer + _, err = tx.Membership.Create(). + SetUserID(opts.UserID). + SetOrganizationID(orgID). + SetRole(authz.RoleGroupMaintainer). + SetMembershipType(authz.MembershipTypeUser). + SetMemberID(opts.UserID). + SetResourceType(authz.ResourceTypeGroup). + SetResourceID(gr.ID). + Save(ctx) + if err != nil { + return fmt.Errorf("failed to create membership for user %s in group %s: %w", opts.UserID, gr.ID, err) + } + entGroup = *gr return nil @@ -206,6 +223,27 @@ func (g GroupRepo) FindByOrgAndID(ctx context.Context, orgID uuid.UUID, groupID return entGroupToBiz(entGroup), nil } +// FindGroupMembershipByGroupAndID retrieves a group membership for a specific user in a group. +func (g GroupRepo) FindGroupMembershipByGroupAndID(ctx context.Context, groupID uuid.UUID, userID uuid.UUID) (*biz.GroupMembership, error) { + // Query the group user membership for the specified user in the group + groupUser, err := g.data.DB.GroupMembership.Query(). + Where( + groupmembership.GroupIDEQ(groupID), + groupmembership.UserIDEQ(userID), + groupmembership.DeletedAtIsNil(), + ). + WithUser(). + Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return nil, biz.NewErrNotFound("group membership") + } + return nil, err + } + + return entGroupMembershipToBiz(groupUser), nil +} + // Update updates an existing group in the specified organization. func (g GroupRepo) Update(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, opts *biz.UpdateGroupOpts) (*biz.Group, error) { if opts == nil { @@ -214,8 +252,8 @@ func (g GroupRepo) Update(ctx context.Context, orgID uuid.UUID, groupID uuid.UUI // Update the group with the provided options entGroup, err := g.data.DB.Group.UpdateOneID(groupID). - SetNillableName(opts.Name). - SetNillableDescription(opts.Description). + SetNillableName(opts.NewName). + SetNillableDescription(opts.NewDescription). Where(group.OrganizationIDEQ(orgID), group.DeletedAtIsNil()). Save(ctx) if err != nil { @@ -223,7 +261,7 @@ func (g GroupRepo) Update(ctx context.Context, orgID uuid.UUID, groupID uuid.UUI return nil, biz.NewErrNotFound("group") } if ent.IsConstraintError(err) { - return nil, biz.NewErrAlreadyExists(err) + return nil, biz.NewErrAlreadyExistsStr("group with the same name already exists") } return nil, err @@ -249,6 +287,105 @@ func (g GroupRepo) SoftDelete(ctx context.Context, orgID uuid.UUID, groupID uuid return nil } +// AddMemberToGroup adds a user to a group, creating a new membership if they are not already a member. +func (g GroupRepo) AddMemberToGroup(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID, maintainer bool) (*biz.GroupMembership, error) { + if err := WithTx(ctx, g.data.DB, func(tx *ent.Tx) error { + // Check if the user is already a member of this group + existingMember, err := tx.GroupMembership.Query(). + Where(groupmembership.UserIDEQ(userID), groupmembership.GroupIDEQ(groupID), groupmembership.DeletedAtIsNil()). + Exist(ctx) + if err != nil && !ent.IsNotFound(err) { + return fmt.Errorf("failed to check existing group membership: %w", err) + } + + // If the user is already a member, return an error + if existingMember { + return biz.NewErrAlreadyExistsStr("user is already a member of this group") + } + + // Create a new group-user relationship + if _, err := tx.GroupMembership.Create(). + SetGroupID(groupID). + SetUserID(userID). + SetMaintainer(maintainer). + Save(ctx); err != nil { + return fmt.Errorf("failed to add user to group: %w", err) + } + + // Update the user membership with the role of maintainer + if maintainer { + _, err = tx.Membership.Create(). + SetUserID(userID). + SetOrganizationID(orgID). + SetRole(authz.RoleGroupMaintainer). + SetMembershipType(authz.MembershipTypeUser). + SetMemberID(userID). + SetResourceType(authz.ResourceTypeGroup). + SetResourceID(groupID). + Save(ctx) + if err != nil { + return fmt.Errorf("failed to create membership for user %s in group %s: %w", userID, groupID, err) + } + } + + return nil + }); err != nil { + return nil, fmt.Errorf("failed to add member to group: %w", err) + } + + // Return the newly created membership + return g.FindGroupMembershipByGroupAndID(ctx, groupID, userID) +} + +// RemoveMemberFromGroup removes a user from a group. +func (g GroupRepo) RemoveMemberFromGroup(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID) error { + err := WithTx(ctx, g.data.DB, func(tx *ent.Tx) error { + // Check if the user is a member of this group + existingMembership, err := tx.GroupMembership.Query(). + Where(groupmembership.UserIDEQ(userID), groupmembership.GroupIDEQ(groupID), groupmembership.DeletedAtIsNil()). + Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return biz.NewErrNotFound("group membership") + } + return fmt.Errorf("failed to check existing group membership: %w", err) + } + + now := time.Now() + + // Mark the membership as deleted + _, err = tx.GroupMembership.UpdateOne(existingMembership). + SetDeletedAt(now). + SetUpdatedAt(now). + Save(ctx) + if err != nil { + return fmt.Errorf("failed to remove user from group: %w", err) + } + + if existingMembership.Maintainer { + // Also remove the user membership if it exists + if _, err := tx.Membership.Delete().Where( + membership.MemberID(userID), + membership.ResourceID(groupID), + membership.ResourceTypeEQ(authz.ResourceTypeGroup), + membership.HasOrganizationWith( + organization.ID(orgID), + ), + ).Exec(ctx); err != nil { + return fmt.Errorf("failed to remove user from group: %w", err) + } + } + + return nil + }) + + if err != nil { + return err + } + + return nil +} + // entGroupToBiz converts an ent.Group to a biz.Group. func entGroupToBiz(gr *ent.Group) *biz.Group { grp := &biz.Group{ diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index 944a7a1fc..011811915 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -24,6 +24,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent" "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/user" "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" ) @@ -109,6 +110,35 @@ func (r *MembershipRepo) FindByOrgAndUser(ctx context.Context, orgID, userID uui return entMembershipToBiz(m), nil } + +// FindByOrgIDAndUserEmail finds the membership for a given organization and user email. +func (r *MembershipRepo) FindByOrgIDAndUserEmail(ctx context.Context, orgID uuid.UUID, userEmail string) (*biz.Membership, error) { + // Find the user by email + u, err := r.data.DB.User.Query().Where(user.Email(userEmail)).Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return nil, biz.NewErrNotFound(fmt.Sprintf("user with email %s not found", userEmail)) + } + return nil, fmt.Errorf("failed to find user by email %s: %w", userEmail, err) + } + + // Now find the membership for that user in the organization + mem, err := r.data.DB.Membership.Query().Where( + membership.MembershipTypeEQ(authz.MembershipTypeUser), + membership.MemberID(u.ID), + membership.ResourceTypeEQ(authz.ResourceTypeOrganization), + membership.ResourceIDEQ(orgID), + ).WithOrganization().WithUser().Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return nil, biz.NewErrNotFound(fmt.Sprintf("membership for user %s in organization %s not found", userEmail, orgID)) + } + return nil, fmt.Errorf("failed to query memberships: %w", err) + } + + return entMembershipToBiz(mem), nil +} + func (r *MembershipRepo) FindByOrgNameAndUser(ctx context.Context, orgName string, userID uuid.UUID) (*biz.Membership, error) { org, err := r.data.DB.Organization.Query().Where(organization.Name(orgName)).First(ctx) if err != nil { diff --git a/app/controlplane/pkg/pagination/offset.go b/app/controlplane/pkg/pagination/offset.go index 87460408e..83461ded9 100644 --- a/app/controlplane/pkg/pagination/offset.go +++ b/app/controlplane/pkg/pagination/offset.go @@ -23,8 +23,7 @@ const ( // DefaultPage defines the default page number DefaultPage = 1 // DefaultPageSize defines the default number of items per page - // TODO: change to 15 when all clients are updated - DefaultPageSize = 50 + DefaultPageSize = 100 ) // OffsetPaginationError is the error type for page-based pagination From df150d38858b88b6ca331a10ffd5f835f01a9dc5 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 30 Jun 2025 09:08:33 +0200 Subject: [PATCH 2/6] make linter happy Signed-off-by: Javier Rodriguez --- app/cli/internal/action/group_member_list.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/cli/internal/action/group_member_list.go b/app/cli/internal/action/group_member_list.go index b0eef9ad6..779d2882e 100644 --- a/app/cli/internal/action/group_member_list.go +++ b/app/cli/internal/action/group_member_list.go @@ -123,7 +123,6 @@ func pbGroupMemberToAction(member *pb.GroupMember) *GroupMemberItem { func getRoleName(maintainer bool) string { if maintainer { return "Maintainer" - } else { - return "Member" } + return "Member" } From 5c8a978147d672a4997a9b93cd9dac6aefa4de65 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 30 Jun 2025 09:25:33 +0200 Subject: [PATCH 3/6] change order of checks Signed-off-by: Javier Rodriguez --- app/controlplane/internal/service/service.go | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index a0daff18d..162b6f401 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -243,6 +243,17 @@ func (s *service) userHasPermissionToRemoveGroupMember(ctx context.Context, orgI // userHasPermissionOnGroupMembershipsWithPolicy is the core implementation that checks if a user has permission on a group // with an optional specific policy check. If the policy is nil, it falls back to the basic permission check. func (s *service) userHasPermissionOnGroupMembershipsWithPolicy(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference, policy *authz.Policy) error { + // Check if the user has admin or owner role in the organization + userRole := usercontext.CurrentAuthzSubject(ctx) + if userRole == "" { + return errors.NotFound("not found", "current membership not found") + } + + // Allow if user has admin or owner role + if userRole == string(authz.RoleAdmin) || userRole == string(authz.RoleOwner) { + return nil + } + groupID, groupName, err := s.parseIdentityReference(groupIdentifier) if err != nil { return handleUseCaseErr(err, s.log) @@ -281,17 +292,6 @@ func (s *service) userHasPermissionOnGroupMembershipsWithPolicy(ctx context.Cont return nil } - // Check if the user has admin or owner role in the organization - userRole := usercontext.CurrentAuthzSubject(ctx) - if userRole == "" { - return errors.NotFound("not found", "current membership not found") - } - - // Allow if user has admin or owner role - if userRole == string(authz.RoleAdmin) || userRole == string(authz.RoleOwner) { - return nil - } - // If a specific policy was provided, check if the user's role allows that policy if policy != nil && s.enforcer != nil { pass, err := s.enforcer.Enforce(userRole, policy) From 7d4871a9e0b035995c153a8cfa604420e7dbd3b8 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 30 Jun 2025 12:08:41 +0200 Subject: [PATCH 4/6] include update command and feedback Signed-off-by: Javier Rodriguez --- app/cli/cmd/group.go | 2 +- app/cli/cmd/group_update.go | 99 +++++++++++++++++++ app/cli/cmd/output.go | 3 +- app/cli/internal/action/group_update.go | 87 ++++++++++++++++ app/controlplane/internal/service/service.go | 41 +++----- .../currentorganization_middleware.go | 12 +-- 6 files changed, 204 insertions(+), 40 deletions(-) create mode 100644 app/cli/cmd/group_update.go create mode 100644 app/cli/internal/action/group_update.go diff --git a/app/cli/cmd/group.go b/app/cli/cmd/group.go index f35e6e103..78aec221f 100644 --- a/app/cli/cmd/group.go +++ b/app/cli/cmd/group.go @@ -27,7 +27,7 @@ func newGroupCmd() *cobra.Command { Hidden: true, } - cmd.AddCommand(newGroupCreateCmd(), newGroupDescribeCmd(), newGroupListCmd(), newGroupDeleteCmd(), newGroupMembersCmd()) + cmd.AddCommand(newGroupCreateCmd(), newGroupDescribeCmd(), newGroupUpdateCmd(), newGroupListCmd(), newGroupDeleteCmd(), newGroupMembersCmd()) return cmd } diff --git a/app/cli/cmd/group_update.go b/app/cli/cmd/group_update.go new file mode 100644 index 000000000..8dd4a1507 --- /dev/null +++ b/app/cli/cmd/group_update.go @@ -0,0 +1,99 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/jedib0t/go-pretty/v6/table" + + "github.com/spf13/cobra" +) + +func newGroupUpdateCmd() *cobra.Command { + var newName, newDescription string + + cmd := &cobra.Command{ + Use: "update [groupName]", + Short: "Update an existing group", + Args: cobra.ExactArgs(1), + Example: ` # Update a group name + chainloop group update my-group --name new-name + + # Update a group description + chainloop group update my-group --description "New description" + + # Update both name and description + chainloop group update my-group --name new-name --description "New description" + `, + RunE: func(cmd *cobra.Command, args []string) error { + groupName := args[0] + + // Check if at least one field is being updated + if newName == "" && newDescription == "" { + return fmt.Errorf("at least one of --name or --description must be provided") + } + + // Prepare the arguments + var namePtr, descPtr *string + if newName != "" { + namePtr = &newName + } + if newDescription != "" { + descPtr = &newDescription + } + + resp, err := action.NewGroupUpdate(actionOpts).Run(cmd.Context(), groupName, namePtr, descPtr) + if err != nil { + return err + } + + // Print the updated group details + if err := encodeOutput(resp, groupUpdateTableOutput); err != nil { + return fmt.Errorf("failed to print group: %w", err) + } + + logger.Info().Msg("Group updated successfully") + + return nil + }, + } + + cmd.Flags().StringVar(&newName, "name", "", "new group name") + cmd.Flags().StringVar(&newDescription, "description", "", "new group description") + cmd.Flags().SortFlags = false + + return cmd +} + +// Format function for group update output +func groupUpdateTableOutput(data *action.GroupUpdateItem) error { + t := newTableWriter() + + t.AppendHeader(table.Row{"ID", "Name", "Description", "Created At", "Updated At"}) + t.AppendRow(table.Row{ + data.ID, + data.Name, + data.Description, + data.CreatedAt, + data.UpdatedAt, + }) + + t.Render() + + return nil +} diff --git a/app/cli/cmd/output.go b/app/cli/cmd/output.go index e2fa4e010..3f9da562a 100644 --- a/app/cli/cmd/output.go +++ b/app/cli/cmd/output.go @@ -56,7 +56,8 @@ type tabulatedData interface { *action.AttestationStatusMaterial | *action.GroupCreateItem | *action.GroupListResult | - *action.GroupMemberListResult + *action.GroupMemberListResult | + *action.GroupUpdateItem } var ErrOutputFormatNotImplemented = errors.New("format not implemented") diff --git a/app/cli/internal/action/group_update.go b/app/cli/internal/action/group_update.go new file mode 100644 index 000000000..5d6291f39 --- /dev/null +++ b/app/cli/internal/action/group_update.go @@ -0,0 +1,87 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + "time" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" +) + +// GroupUpdateItem represents the response structure for an updated group +type GroupUpdateItem struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` +} + +// GroupUpdate handles the update of an existing group +type GroupUpdate struct { + cfg *ActionsOpts +} + +// NewGroupUpdate creates a new instance of GroupUpdate +func NewGroupUpdate(cfg *ActionsOpts) *GroupUpdate { + return &GroupUpdate{cfg} +} + +// Run executes the group update operation +func (action *GroupUpdate) Run(ctx context.Context, groupName string, newName, newDescription *string) (*GroupUpdateItem, error) { + client := pb.NewGroupServiceClient(action.cfg.CPConnection) + + // Create the group reference using the name + groupRef := &pb.IdentityReference{ + Name: &groupName, + } + + // Create the update request + req := &pb.GroupServiceUpdateRequest{ + GroupReference: groupRef, + NewName: newName, + NewDescription: newDescription, + } + + // Make the update request + resp, err := client.Update(ctx, req) + if err != nil { + return nil, err + } + + return pbGroupUpdateItemToAction(resp.GetGroup()), nil +} + +// pbGroupUpdateItemToAction converts a protobuf group item to the action model +func pbGroupUpdateItemToAction(group *pb.Group) *GroupUpdateItem { + createdAt := "" + if group.CreatedAt != nil { + createdAt = group.CreatedAt.AsTime().Format(time.RFC3339) + } + updatedAt := "" + if group.UpdatedAt != nil { + updatedAt = group.UpdatedAt.AsTime().Format(time.RFC3339) + } + + return &GroupUpdateItem{ + ID: group.Id, + Name: group.Name, + Description: group.Description, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 162b6f401..f2688743a 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -270,36 +270,17 @@ func (s *service) userHasPermissionOnGroupMembershipsWithPolicy(ctx context.Cont return handleUseCaseErr(err, s.log) } - // Get the current user from the context - user := entities.CurrentUser(ctx) - if user == nil { - return errors.NotFound("not found", "logged in user not found") - } - - userUUID, err := uuid.Parse(user.ID) - if err != nil { - return errors.BadRequest("invalid", "invalid user ID") - } - - // Check if the user is a maintainer of the group - isMaintainer, err := s.groupUseCase.IsUserGroupMaintainer(ctx, orgUUID, resolvedGroupID, userUUID) - if err != nil { - return handleUseCaseErr(err, s.log) - } - - // If the user is a maintainer, they can perform any operation on the group - if isMaintainer { - return nil - } - - // If a specific policy was provided, check if the user's role allows that policy - if policy != nil && s.enforcer != nil { - pass, err := s.enforcer.Enforce(userRole, policy) - if err != nil { - return handleUseCaseErr(err, s.log) - } - if pass { - return nil + // Check the user's membership in the organization + m := entities.CurrentMembership(ctx) + for _, rm := range m.Resources { + if rm.ResourceType == authz.ResourceTypeGroup && rm.ResourceID == resolvedGroupID { + pass, err := s.enforcer.Enforce(string(rm.Role), policy) + if err != nil { + return handleUseCaseErr(err, s.log) + } + if pass { + return nil + } } } diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware.go b/app/controlplane/internal/usercontext/currentorganization_middleware.go index 94f54c18a..1f1f17414 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware.go @@ -23,7 +23,6 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/middleware" @@ -62,13 +61,10 @@ func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membership } } - orgRole := CurrentAuthzSubject(ctx) - if orgRole == string(authz.RoleOrgMember) { - // Org member enables the new RBAC behavior. Let's store all memberships in the context. - ctx, err = setCurrentMembershipsForUser(ctx, u, membershipUC) - if err != nil { - return nil, fmt.Errorf("error setting current org membership: %w", err) - } + // Let's store all memberships in the context. + ctx, err = setCurrentMembershipsForUser(ctx, u, membershipUC) + if err != nil { + return nil, fmt.Errorf("error setting current org membership: %w", err) } org := entities.CurrentOrg(ctx) From 9f65b4ab16e7596d518f50bb1e05eeda9dce98df Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 30 Jun 2025 12:51:13 +0200 Subject: [PATCH 5/6] fix tests Signed-off-by: Javier Rodriguez --- .../currentorganization_middleware_test.go | 7 +- app/controlplane/pkg/biz/group.go | 85 +++++++++++++------ .../pkg/biz/group_integration_test.go | 4 +- app/controlplane/pkg/biz/membership.go | 1 + app/controlplane/pkg/data/membership.go | 16 ++++ 5 files changed, 79 insertions(+), 34 deletions(-) diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware_test.go b/app/controlplane/internal/usercontext/currentorganization_middleware_test.go index 06cd0c908..c67dd8ea0 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware_test.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware_test.go @@ -25,9 +25,11 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" bizMocks "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/mocks" userjwtbuilder "github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/user" + "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestWithCurrentOrganizationMiddleware(t *testing.T) { @@ -69,16 +71,15 @@ func TestWithCurrentOrganizationMiddleware(t *testing.T) { ctx := context.Background() if tc.loggedIn { ctx = entities.WithCurrentUser(ctx, &entities.User{ID: wantUser.ID}) - } - - if tc.loggedIn { usecase.On("FindByID", ctx, wantUser.ID).Maybe().Return(nil, nil) } if tc.orgExist { usecase.On("CurrentMembership", ctx, wantUser.ID).Return(wantMembership, nil) + msMock.On("ListAllMembershipsForUser", mock.Anything, mock.Anything).Return([]*biz.Membership{wantMembership}, nil) } else if tc.loggedIn { usecase.On("CurrentMembership", ctx, wantUser.ID).Maybe().Return(nil, nil) + msMock.On("ListAllMembershipsForUser", mock.Anything, mock.Anything).Return([]*biz.Membership{}, nil) } m := WithCurrentOrganizationMiddleware(usecase, msMock, logger) diff --git a/app/controlplane/pkg/biz/group.go b/app/controlplane/pkg/biz/group.go index 19c9de140..deb1be99a 100644 --- a/app/controlplane/pkg/biz/group.go +++ b/app/controlplane/pkg/biz/group.go @@ -21,6 +21,7 @@ import ( "time" "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" @@ -367,6 +368,34 @@ func (uc *GroupUseCase) AddMemberToGroup(ctx context.Context, orgID uuid.UUID, o return nil, NewErrNotFound("group") } + // Check if the requester is part of the organization + requesterMembership, err := uc.membershipRepo.FindByOrgAndUser(ctx, orgID, opts.RequesterID) + if err != nil && !IsNotFound(err) { + return nil, NewErrValidationStr("failed to check existing membership") + } + + if requesterMembership == nil { + return nil, NewErrValidationStr("requester is not a member of the organization") + } + + // Check if the requester has sufficient permissions + // Allow if the requester is an org owner or admin + isAdminOrOwner := requesterMembership.Role == authz.RoleOwner || requesterMembership.Role == authz.RoleAdmin + + // If not an admin/owner, check if the requester is a maintainer of this group + if !isAdminOrOwner { + // Check if the requester is a maintainer of this group + requesterGroupMembership, err := uc.membershipRepo.FindByUserAndResourceID(ctx, opts.RequesterID, resolvedGroupID) + if err != nil && !IsNotFound(err) { + return nil, fmt.Errorf("failed to check requester's group membership: %w", err) + } + + // If not a maintainer of this group, deny access + if requesterGroupMembership == nil || requesterGroupMembership.Role != authz.RoleGroupMaintainer { + return nil, NewErrValidationStr("requester does not have permission to add members to this group") + } + } + // Find the user by email in the organization userMembership, err := uc.membershipRepo.FindByOrgIDAndUserEmail(ctx, orgID, opts.UserEmail) if err != nil && !IsNotFound(err) { @@ -432,6 +461,34 @@ func (uc *GroupUseCase) RemoveMemberFromGroup(ctx context.Context, orgID uuid.UU return NewErrNotFound("group") } + // Check if the requester is part of the organization + requesterMembership, err := uc.membershipRepo.FindByOrgAndUser(ctx, orgID, opts.RequesterID) + if err != nil && !IsNotFound(err) { + return NewErrValidationStr("failed to check existing membership") + } + + if requesterMembership == nil { + return NewErrValidationStr("requester is not a member of the organization") + } + + // Check if the requester has sufficient permissions + // Allow if the requester is an org owner or admin + isAdminOrOwner := requesterMembership.Role == authz.RoleOwner || requesterMembership.Role == authz.RoleAdmin + + // If not an admin/owner, check if the requester is a maintainer of this group + if !isAdminOrOwner { + // Check if the requester is a maintainer of this group + requesterGroupMembership, err := uc.membershipRepo.FindByUserAndResourceID(ctx, opts.RequesterID, resolvedGroupID) + if err != nil && !IsNotFound(err) { + return fmt.Errorf("failed to check requester's group membership: %w", err) + } + + // If not a maintainer of this group, deny access + if requesterGroupMembership == nil || requesterGroupMembership.Role != authz.RoleGroupMaintainer { + return NewErrValidationStr("requester does not have permission to add members to this group") + } + } + // Find the user by email in the organization userMembership, err := uc.membershipRepo.FindByOrgIDAndUserEmail(ctx, orgID, opts.UserEmail) if err != nil && !IsNotFound(err) { @@ -441,16 +498,6 @@ func (uc *GroupUseCase) RemoveMemberFromGroup(ctx context.Context, orgID uuid.UU return NewErrValidationStr("user with the provided email is not a member of the organization") } - // Check if the requester is part of the organization - membership, err := uc.groupRepo.FindGroupMembershipByGroupAndID(ctx, resolvedGroupID, opts.RequesterID) - if err != nil && !IsNotFound(err) { - return NewErrValidationStr("failed to check existing membership") - } - - if membership == nil { - return NewErrValidationStr("requester is not a member of the group") - } - userUUID := uuid.MustParse(userMembership.User.ID) // Check if the user is a member of the group existingMembership, err := uc.groupRepo.FindGroupMembershipByGroupAndID(ctx, resolvedGroupID, userUUID) @@ -478,24 +525,6 @@ func (uc *GroupUseCase) RemoveMemberFromGroup(ctx context.Context, orgID uuid.UU return nil } -// IsUserGroupMaintainer checks if a user is a maintainer of a group. -func (uc *GroupUseCase) IsUserGroupMaintainer(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID) (bool, error) { - if orgID == uuid.Nil || groupID == uuid.Nil || userID == uuid.Nil { - return false, NewErrValidationStr("organization ID, group ID, and user ID cannot be empty") - } - - membership, err := uc.groupRepo.FindGroupMembershipByGroupAndID(ctx, groupID, userID) - if err != nil && !IsNotFound(err) { - return false, fmt.Errorf("failed to check group membership: %w", err) - } - - if membership == nil { - return false, nil // User is not a member of the group - } - - return membership.Maintainer, nil -} - // ValidateGroupIdentifier validates and resolves the group ID or name to a group ID. // Returns an error if both are nil or if the resolved group does not exist. func (uc *GroupUseCase) ValidateGroupIdentifier(ctx context.Context, orgID uuid.UUID, groupID *uuid.UUID, groupName *string) (uuid.UUID, error) { diff --git a/app/controlplane/pkg/biz/group_integration_test.go b/app/controlplane/pkg/biz/group_integration_test.go index dc80c13b9..fb45ea032 100644 --- a/app/controlplane/pkg/biz/group_integration_test.go +++ b/app/controlplane/pkg/biz/group_integration_test.go @@ -1013,7 +1013,6 @@ func (s *groupMembersIntegrationTestSuite) TestRemoveMemberFromGroup() { // Create a user who is not in any organization externalUser, err := s.User.UpsertByEmail(ctx, "external-user@example.com", nil) require.NoError(s.T(), err) - // Intentionally not adding to organization // Try to remove a member with an external user as requester removeOpts := &biz.RemoveMemberFromGroupOpts{ @@ -1026,8 +1025,7 @@ func (s *groupMembersIntegrationTestSuite) TestRemoveMemberFromGroup() { err = s.Group.RemoveMemberFromGroup(ctx, uuid.MustParse(s.org.ID), removeOpts) s.Error(err) - // The error message has changed with the new permissions logic - s.Contains(err.Error(), "requester is not a member of the group") + s.Contains(err.Error(), "requester is not a member of the organization") }) s.Run("non-existent user email", func() { diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 86dbe2cb8..c87584241 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -43,6 +43,7 @@ type Membership struct { 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) FindByIDInUser(ctx context.Context, userID, ID uuid.UUID) (*Membership, error) FindByIDInOrg(ctx context.Context, orgID, ID uuid.UUID) (*Membership, error) diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index 011811915..f30584726 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -290,6 +290,22 @@ func (r *MembershipRepo) AddResourceRole(ctx context.Context, resourceType authz return nil } +// FindByUserAndResourceID finds a membership by user ID and resource ID. +func (r *MembershipRepo) FindByUserAndResourceID(ctx context.Context, userID, resourceID uuid.UUID) (*biz.Membership, error) { + m, err := r.data.DB.Membership.Query().Where( + membership.MemberID(userID), + membership.ResourceID(resourceID), + ).WithUser().WithOrganization().Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return nil, biz.NewErrNotFound(fmt.Sprintf("membership for user %s and resource %s not found", userID, resourceID)) + } + return nil, fmt.Errorf("failed to query memberships: %w", err) + } + + return entMembershipToBiz(m), nil +} + func entMembershipsToBiz(memberships []*ent.Membership) []*biz.Membership { result := make([]*biz.Membership, 0, len(memberships)) for _, m := range memberships { From c306f157020be5176082daf5bd1f4069988fd73f Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 30 Jun 2025 12:58:20 +0200 Subject: [PATCH 6/6] fix test Signed-off-by: Javier Rodriguez --- .../internal/usercontext/currentorganization_middleware_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware_test.go b/app/controlplane/internal/usercontext/currentorganization_middleware_test.go index c67dd8ea0..9c2b0bf84 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware_test.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware_test.go @@ -79,7 +79,6 @@ func TestWithCurrentOrganizationMiddleware(t *testing.T) { msMock.On("ListAllMembershipsForUser", mock.Anything, mock.Anything).Return([]*biz.Membership{wantMembership}, nil) } else if tc.loggedIn { usecase.On("CurrentMembership", ctx, wantUser.ID).Maybe().Return(nil, nil) - msMock.On("ListAllMembershipsForUser", mock.Anything, mock.Anything).Return([]*biz.Membership{}, nil) } m := WithCurrentOrganizationMiddleware(usecase, msMock, logger)