From 6aeeec3fdf69521eb193e5a2599755feca2fcacf Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Tue, 1 Jul 2025 13:00:57 +0200 Subject: [PATCH 1/2] feat(projects): Allow to include users to projects Signed-off-by: Javier Rodriguez --- 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/group_update.go | 99 -- app/cli/cmd/output.go | 6 +- 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 | 128 -- app/cli/internal/action/group_update.go | 87 -- .../api/controlplane/v1/project.pb.go | 1079 +++++++++++++++-- .../api/controlplane/v1/project.proto | 101 +- .../api/controlplane/v1/project_grpc.pb.go | 113 ++ .../api/controlplane/v1/shared_message.go} | 29 +- .../gen/frontend/controlplane/v1/project.ts | 916 +++++++++++++- ...trolplane.v1.ProjectMember.jsonschema.json | 55 + .../controlplane.v1.ProjectMember.schema.json | 55 + ...ProjectMembershipReference.jsonschema.json | 22 + ....v1.ProjectMembershipReference.schema.json | 22 + ...ServiceAPITokenListRequest.jsonschema.json | 18 +- ...jectServiceAPITokenListRequest.schema.json | 18 +- ...rviceAPITokenRevokeRequest.jsonschema.json | 18 +- ...ctServiceAPITokenRevokeRequest.schema.json | 18 +- ...viceAPITokenRevokeResponse.jsonschema.json | 1 + ...tServiceAPITokenRevokeResponse.schema.json | 1 + ...ectServiceAddMemberRequest.jsonschema.json | 52 + ...ProjectServiceAddMemberRequest.schema.json | 52 + ...ctServiceAddMemberResponse.jsonschema.json | 9 + ...rojectServiceAddMemberResponse.schema.json | 9 + ...tServiceListMembersRequest.jsonschema.json | 27 + ...ojectServiceListMembersRequest.schema.json | 27 + ...ServiceListMembersResponse.jsonschema.json | 21 + ...jectServiceListMembersResponse.schema.json | 21 + ...ServiceRemoveMemberRequest.jsonschema.json | 31 + ...jectServiceRemoveMemberRequest.schema.json | 31 + ...erviceRemoveMemberResponse.jsonschema.json | 9 + ...ectServiceRemoveMemberResponse.schema.json | 9 + app/controlplane/cmd/wire_gen.go | 2 +- .../internal/service/attestation.go | 10 +- .../internal/service/attestationstate.go | 8 +- app/controlplane/internal/service/group.go | 25 +- app/controlplane/internal/service/project.go | 261 +++- app/controlplane/internal/service/service.go | 34 +- app/controlplane/internal/service/workflow.go | 8 +- .../pkg/auditor/events/project.go | 129 ++ .../pkg/auditor/events/project_test.go | 256 ++++ .../projects/project_member_added.json | 18 + .../project_member_added_as_admin.json | 18 + .../project_member_added_with_api_token.json | 18 + .../projects/project_member_removed.json | 17 + ...project_member_removed_with_api_token.json | 17 + app/controlplane/pkg/authz/authz.go | 25 +- app/controlplane/pkg/biz/biz.go | 8 - app/controlplane/pkg/biz/project.go | 311 ++++- .../pkg/biz/project_integration_test.go | 1002 +++++++++++++++ .../pkg/biz/testhelpers/wire_gen.go | 2 +- app/controlplane/pkg/data/project.go | 164 +++ 66 files changed, 4834 insertions(+), 1582 deletions(-) delete mode 100644 app/cli/cmd/group_create.go delete mode 100644 app/cli/cmd/group_delete.go delete mode 100644 app/cli/cmd/group_describe.go delete mode 100644 app/cli/cmd/group_list.go delete mode 100644 app/cli/cmd/group_member.go delete mode 100644 app/cli/cmd/group_member_add.go delete mode 100644 app/cli/cmd/group_member_delete.go delete mode 100644 app/cli/cmd/group_member_list.go delete mode 100644 app/cli/cmd/group_update.go delete mode 100644 app/cli/internal/action/group_create.go delete mode 100644 app/cli/internal/action/group_delete.go delete mode 100644 app/cli/internal/action/group_describe.go delete mode 100644 app/cli/internal/action/group_list.go delete mode 100644 app/cli/internal/action/group_member_add.go delete mode 100644 app/cli/internal/action/group_member_delete.go delete mode 100644 app/cli/internal/action/group_member_list.go delete mode 100644 app/cli/internal/action/group_update.go rename app/{cli/cmd/group.go => controlplane/api/controlplane/v1/shared_message.go} (50%) create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMember.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMember.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMembershipReference.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMembershipReference.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberRequest.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberRequest.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberResponse.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberResponse.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersRequest.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersRequest.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersResponse.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersResponse.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberRequest.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberRequest.schema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberResponse.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberResponse.schema.json create mode 100644 app/controlplane/pkg/auditor/events/project.go create mode 100644 app/controlplane/pkg/auditor/events/project_test.go create mode 100644 app/controlplane/pkg/auditor/events/testdata/projects/project_member_added.json create mode 100644 app/controlplane/pkg/auditor/events/testdata/projects/project_member_added_as_admin.json create mode 100644 app/controlplane/pkg/auditor/events/testdata/projects/project_member_added_with_api_token.json create mode 100644 app/controlplane/pkg/auditor/events/testdata/projects/project_member_removed.json create mode 100644 app/controlplane/pkg/auditor/events/testdata/projects/project_member_removed_with_api_token.json create mode 100644 app/controlplane/pkg/biz/project_integration_test.go diff --git a/app/cli/cmd/group_create.go b/app/cli/cmd/group_create.go deleted file mode 100644 index 9a25909e0..000000000 --- a/app/cli/cmd/group_create.go +++ /dev/null @@ -1,80 +0,0 @@ -// -// 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 deleted file mode 100644 index b03d94fd1..000000000 --- a/app/cli/cmd/group_delete.go +++ /dev/null @@ -1,59 +0,0 @@ -// -// 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 deleted file mode 100644 index d99664967..000000000 --- a/app/cli/cmd/group_describe.go +++ /dev/null @@ -1,51 +0,0 @@ -// -// 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 deleted file mode 100644 index d54ee1ddd..000000000 --- a/app/cli/cmd/group_list.go +++ /dev/null @@ -1,158 +0,0 @@ -// -// 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 deleted file mode 100644 index 220cdf5e8..000000000 --- a/app/cli/cmd/group_member.go +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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 deleted file mode 100644 index a1bf3a7fa..000000000 --- a/app/cli/cmd/group_member_add.go +++ /dev/null @@ -1,58 +0,0 @@ -// -// 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 deleted file mode 100644 index e7232989d..000000000 --- a/app/cli/cmd/group_member_delete.go +++ /dev/null @@ -1,60 +0,0 @@ -// -// 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 deleted file mode 100644 index 1285a69a0..000000000 --- a/app/cli/cmd/group_member_list.go +++ /dev/null @@ -1,133 +0,0 @@ -// -// 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/group_update.go b/app/cli/cmd/group_update.go deleted file mode 100644 index 8dd4a1507..000000000 --- a/app/cli/cmd/group_update.go +++ /dev/null @@ -1,99 +0,0 @@ -// -// 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 3f9da562a..5a0c05b59 100644 --- a/app/cli/cmd/output.go +++ b/app/cli/cmd/output.go @@ -53,11 +53,7 @@ type tabulatedData interface { []*action.OrgInvitationItem | *action.APITokenItem | []*action.APITokenItem | - *action.AttestationStatusMaterial | - *action.GroupCreateItem | - *action.GroupListResult | - *action.GroupMemberListResult | - *action.GroupUpdateItem + *action.AttestationStatusMaterial } var ErrOutputFormatNotImplemented = errors.New("format not implemented") diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index ec5a20d94..850ffb1de 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -241,7 +241,7 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { rootCmd.AddCommand(newWorkflowCmd(), newAuthCmd(), NewVersionCmd(), newAttestationCmd(), newArtifactCmd(), newConfigCmd(), newIntegrationCmd(), newOrganizationCmd(), newCASBackendCmd(), - newReferrerDiscoverCmd(), newGroupCmd(), + newReferrerDiscoverCmd(), ) return rootCmd diff --git a/app/cli/internal/action/group_create.go b/app/cli/internal/action/group_create.go deleted file mode 100644 index aec759fe0..000000000 --- a/app/cli/internal/action/group_create.go +++ /dev/null @@ -1,76 +0,0 @@ -// -// 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 deleted file mode 100644 index 37ef69ce5..000000000 --- a/app/cli/internal/action/group_delete.go +++ /dev/null @@ -1,47 +0,0 @@ -// -// 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 deleted file mode 100644 index 2b2442fdc..000000000 --- a/app/cli/internal/action/group_describe.go +++ /dev/null @@ -1,52 +0,0 @@ -// -// 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 deleted file mode 100644 index 002d8019b..000000000 --- a/app/cli/internal/action/group_list.go +++ /dev/null @@ -1,92 +0,0 @@ -// -// 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 deleted file mode 100644 index ffbad6101..000000000 --- a/app/cli/internal/action/group_member_add.go +++ /dev/null @@ -1,53 +0,0 @@ -// -// 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 deleted file mode 100644 index 341f9baf1..000000000 --- a/app/cli/internal/action/group_member_delete.go +++ /dev/null @@ -1,48 +0,0 @@ -// -// 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 deleted file mode 100644 index 779d2882e..000000000 --- a/app/cli/internal/action/group_member_list.go +++ /dev/null @@ -1,128 +0,0 @@ -// -// 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" - } - return "Member" -} diff --git a/app/cli/internal/action/group_update.go b/app/cli/internal/action/group_update.go deleted file mode 100644 index 5d6291f39..000000000 --- a/app/cli/internal/action/group_update.go +++ /dev/null @@ -1,87 +0,0 @@ -// -// 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/api/controlplane/v1/project.pb.go b/app/controlplane/api/controlplane/v1/project.pb.go index a3e5b9e93..35f326760 100644 --- a/app/controlplane/api/controlplane/v1/project.pb.go +++ b/app/controlplane/api/controlplane/v1/project.pb.go @@ -26,6 +26,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -37,6 +38,59 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// ProjectMemberRole defines the roles a member can have in a project +type ProjectMemberRole int32 + +const ( + // Default role for a project member + ProjectMemberRole_PROJECT_MEMBER_ROLE_UNSPECIFIED ProjectMemberRole = 0 + // Admin role for a project member + ProjectMemberRole_PROJECT_MEMBER_ROLE_ADMIN ProjectMemberRole = 1 + // Viewer role for a project member + ProjectMemberRole_PROJECT_MEMBER_ROLE_VIEWER ProjectMemberRole = 2 +) + +// Enum value maps for ProjectMemberRole. +var ( + ProjectMemberRole_name = map[int32]string{ + 0: "PROJECT_MEMBER_ROLE_UNSPECIFIED", + 1: "PROJECT_MEMBER_ROLE_ADMIN", + 2: "PROJECT_MEMBER_ROLE_VIEWER", + } + ProjectMemberRole_value = map[string]int32{ + "PROJECT_MEMBER_ROLE_UNSPECIFIED": 0, + "PROJECT_MEMBER_ROLE_ADMIN": 1, + "PROJECT_MEMBER_ROLE_VIEWER": 2, + } +) + +func (x ProjectMemberRole) Enum() *ProjectMemberRole { + p := new(ProjectMemberRole) + *p = x + return p +} + +func (x ProjectMemberRole) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProjectMemberRole) Descriptor() protoreflect.EnumDescriptor { + return file_controlplane_v1_project_proto_enumTypes[0].Descriptor() +} + +func (ProjectMemberRole) Type() protoreflect.EnumType { + return &file_controlplane_v1_project_proto_enumTypes[0] +} + +func (x ProjectMemberRole) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProjectMemberRole.Descriptor instead. +func (ProjectMemberRole) EnumDescriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{0} +} + type ProjectServiceAPITokenCreateRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -155,14 +209,16 @@ func (x *ProjectServiceAPITokenCreateResponse) GetResult() *ProjectServiceAPITok return nil } +// ProjectServiceAPITokenRevokeRequest contains the information needed to revoke an API token for a project type ProjectServiceAPITokenRevokeRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // token name - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - ProjectName string `protobuf:"bytes,2,opt,name=project_name,json=projectName,proto3" json:"project_name,omitempty"` + // The name of the API token to revoke + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // IdentityReference is used to specify the project by either its ID or name + ProjectReference *IdentityReference `protobuf:"bytes,2,opt,name=project_reference,json=projectReference,proto3" json:"project_reference,omitempty"` } func (x *ProjectServiceAPITokenRevokeRequest) Reset() { @@ -204,13 +260,14 @@ func (x *ProjectServiceAPITokenRevokeRequest) GetName() string { return "" } -func (x *ProjectServiceAPITokenRevokeRequest) GetProjectName() string { +func (x *ProjectServiceAPITokenRevokeRequest) GetProjectReference() *IdentityReference { if x != nil { - return x.ProjectName + return x.ProjectReference } - return "" + return nil } +// ProjectServiceAPITokenRevokeResponse is returned upon successful revocation of an API token type ProjectServiceAPITokenRevokeResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -249,13 +306,16 @@ func (*ProjectServiceAPITokenRevokeResponse) Descriptor() ([]byte, []int) { return file_controlplane_v1_project_proto_rawDescGZIP(), []int{3} } +// ProjectServiceAPITokenListRequest contains the information needed to list API tokens for a project type ProjectServiceAPITokenListRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ProjectName string `protobuf:"bytes,1,opt,name=project_name,json=projectName,proto3" json:"project_name,omitempty"` - IncludeRevoked bool `protobuf:"varint,2,opt,name=include_revoked,json=includeRevoked,proto3" json:"include_revoked,omitempty"` + // IdentityReference is used to specify the project by either its ID or name + ProjectReference *IdentityReference `protobuf:"bytes,1,opt,name=project_reference,json=projectReference,proto3" json:"project_reference,omitempty"` + // Flag to include revoked tokens in the list + IncludeRevoked bool `protobuf:"varint,2,opt,name=include_revoked,json=includeRevoked,proto3" json:"include_revoked,omitempty"` } func (x *ProjectServiceAPITokenListRequest) Reset() { @@ -290,11 +350,11 @@ func (*ProjectServiceAPITokenListRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_project_proto_rawDescGZIP(), []int{4} } -func (x *ProjectServiceAPITokenListRequest) GetProjectName() string { +func (x *ProjectServiceAPITokenListRequest) GetProjectReference() *IdentityReference { if x != nil { - return x.ProjectName + return x.ProjectReference } - return "" + return nil } func (x *ProjectServiceAPITokenListRequest) GetIncludeRevoked() bool { @@ -351,6 +411,508 @@ func (x *ProjectServiceAPITokenListResponse) GetResult() []*APITokenItem { return nil } +// ProjectServiceListMembersRequest contains the information needed to list members of a project +type ProjectServiceListMembersRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // IdentityReference is used to specify the project by either its ID or name + ProjectReference *IdentityReference `protobuf:"bytes,1,opt,name=project_reference,json=projectReference,proto3" json:"project_reference,omitempty"` + // Pagination parameters to limit and offset results + Pagination *OffsetPaginationRequest `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` +} + +func (x *ProjectServiceListMembersRequest) Reset() { + *x = ProjectServiceListMembersRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_project_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProjectServiceListMembersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectServiceListMembersRequest) ProtoMessage() {} + +func (x *ProjectServiceListMembersRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_project_proto_msgTypes[6] + 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 ProjectServiceListMembersRequest.ProtoReflect.Descriptor instead. +func (*ProjectServiceListMembersRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{6} +} + +func (x *ProjectServiceListMembersRequest) GetProjectReference() *IdentityReference { + if x != nil { + return x.ProjectReference + } + return nil +} + +func (x *ProjectServiceListMembersRequest) GetPagination() *OffsetPaginationRequest { + if x != nil { + return x.Pagination + } + return nil +} + +// ProjectServiceListMembersResponse contains the list of members in a project +type ProjectServiceListMembersResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The list of members in the project + Members []*ProjectMember `protobuf:"bytes,1,rep,name=members,proto3" json:"members,omitempty"` + // Pagination information for the response + Pagination *OffsetPaginationResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` +} + +func (x *ProjectServiceListMembersResponse) Reset() { + *x = ProjectServiceListMembersResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_project_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProjectServiceListMembersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectServiceListMembersResponse) ProtoMessage() {} + +func (x *ProjectServiceListMembersResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_project_proto_msgTypes[7] + 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 ProjectServiceListMembersResponse.ProtoReflect.Descriptor instead. +func (*ProjectServiceListMembersResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{7} +} + +func (x *ProjectServiceListMembersResponse) GetMembers() []*ProjectMember { + if x != nil { + return x.Members + } + return nil +} + +func (x *ProjectServiceListMembersResponse) GetPagination() *OffsetPaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +// ProjectMember represents an user or group who is a member of a project +type ProjectMember struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Subject can be either a user or a group + // + // Types that are assignable to Subject: + // + // *ProjectMember_User + // *ProjectMember_Group + Subject isProjectMember_Subject `protobuf_oneof:"subject"` + // The role of the user in the project + Role ProjectMemberRole `protobuf:"varint,3,opt,name=role,proto3,enum=controlplane.v1.ProjectMemberRole" json:"role,omitempty"` + // Timestamp when the project membership was created + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + // Timestamp when the project membership was last modified + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` +} + +func (x *ProjectMember) Reset() { + *x = ProjectMember{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_project_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProjectMember) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectMember) ProtoMessage() {} + +func (x *ProjectMember) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_project_proto_msgTypes[8] + 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 ProjectMember.ProtoReflect.Descriptor instead. +func (*ProjectMember) Descriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{8} +} + +func (m *ProjectMember) GetSubject() isProjectMember_Subject { + if m != nil { + return m.Subject + } + return nil +} + +func (x *ProjectMember) GetUser() *User { + if x, ok := x.GetSubject().(*ProjectMember_User); ok { + return x.User + } + return nil +} + +func (x *ProjectMember) GetGroup() *Group { + if x, ok := x.GetSubject().(*ProjectMember_Group); ok { + return x.Group + } + return nil +} + +func (x *ProjectMember) GetRole() ProjectMemberRole { + if x != nil { + return x.Role + } + return ProjectMemberRole_PROJECT_MEMBER_ROLE_UNSPECIFIED +} + +func (x *ProjectMember) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ProjectMember) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type isProjectMember_Subject interface { + isProjectMember_Subject() +} + +type ProjectMember_User struct { + // The user who is a member of the project + User *User `protobuf:"bytes,1,opt,name=user,proto3,oneof"` +} + +type ProjectMember_Group struct { + // The group who is a member of the project + Group *Group `protobuf:"bytes,2,opt,name=group,proto3,oneof"` +} + +func (*ProjectMember_User) isProjectMember_Subject() {} + +func (*ProjectMember_Group) isProjectMember_Subject() {} + +// ProjectServiceAddMemberRequest contains the information needed to add a user to a project +type ProjectServiceAddMemberRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // IdentityReference is used to specify the project by either its ID or name + ProjectReference *IdentityReference `protobuf:"bytes,1,opt,name=project_reference,json=projectReference,proto3" json:"project_reference,omitempty"` + // The membership reference can be a user email or groups references in the future + MemberReference *ProjectMembershipReference `protobuf:"bytes,2,opt,name=member_reference,json=memberReference,proto3" json:"member_reference,omitempty"` + // Indicates if the user should be added as an admin + Role ProjectMemberRole `protobuf:"varint,3,opt,name=role,proto3,enum=controlplane.v1.ProjectMemberRole" json:"role,omitempty"` +} + +func (x *ProjectServiceAddMemberRequest) Reset() { + *x = ProjectServiceAddMemberRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_project_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProjectServiceAddMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectServiceAddMemberRequest) ProtoMessage() {} + +func (x *ProjectServiceAddMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_project_proto_msgTypes[9] + 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 ProjectServiceAddMemberRequest.ProtoReflect.Descriptor instead. +func (*ProjectServiceAddMemberRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{9} +} + +func (x *ProjectServiceAddMemberRequest) GetProjectReference() *IdentityReference { + if x != nil { + return x.ProjectReference + } + return nil +} + +func (x *ProjectServiceAddMemberRequest) GetMemberReference() *ProjectMembershipReference { + if x != nil { + return x.MemberReference + } + return nil +} + +func (x *ProjectServiceAddMemberRequest) GetRole() ProjectMemberRole { + if x != nil { + return x.Role + } + return ProjectMemberRole_PROJECT_MEMBER_ROLE_UNSPECIFIED +} + +// ProjectServiceAddMemberResponse contains the result of adding a user to a project +type ProjectServiceAddMemberResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ProjectServiceAddMemberResponse) Reset() { + *x = ProjectServiceAddMemberResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_project_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProjectServiceAddMemberResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectServiceAddMemberResponse) ProtoMessage() {} + +func (x *ProjectServiceAddMemberResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_project_proto_msgTypes[10] + 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 ProjectServiceAddMemberResponse.ProtoReflect.Descriptor instead. +func (*ProjectServiceAddMemberResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{10} +} + +type ProjectServiceRemoveMemberRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // IdentityReference is used to specify the project by either its ID or name + ProjectReference *IdentityReference `protobuf:"bytes,1,opt,name=project_reference,json=projectReference,proto3" json:"project_reference,omitempty"` + // The membership reference can be a user email or groups references in the future + MemberReference *ProjectMembershipReference `protobuf:"bytes,2,opt,name=member_reference,json=memberReference,proto3" json:"member_reference,omitempty"` +} + +func (x *ProjectServiceRemoveMemberRequest) Reset() { + *x = ProjectServiceRemoveMemberRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_project_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProjectServiceRemoveMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectServiceRemoveMemberRequest) ProtoMessage() {} + +func (x *ProjectServiceRemoveMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_project_proto_msgTypes[11] + 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 ProjectServiceRemoveMemberRequest.ProtoReflect.Descriptor instead. +func (*ProjectServiceRemoveMemberRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{11} +} + +func (x *ProjectServiceRemoveMemberRequest) GetProjectReference() *IdentityReference { + if x != nil { + return x.ProjectReference + } + return nil +} + +func (x *ProjectServiceRemoveMemberRequest) GetMemberReference() *ProjectMembershipReference { + if x != nil { + return x.MemberReference + } + return nil +} + +// ProjectServiceRemoveMemberResponse is returned upon successful removal of a user from a project +type ProjectServiceRemoveMemberResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ProjectServiceRemoveMemberResponse) Reset() { + *x = ProjectServiceRemoveMemberResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_project_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProjectServiceRemoveMemberResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectServiceRemoveMemberResponse) ProtoMessage() {} + +func (x *ProjectServiceRemoveMemberResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_project_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 ProjectServiceRemoveMemberResponse.ProtoReflect.Descriptor instead. +func (*ProjectServiceRemoveMemberResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{12} +} + +// ProjectMembershipReference is used to reference a user or group in the context of project membership +type ProjectMembershipReference struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The membership reference can be a user email or groups references in the future + // + // Types that are assignable to MembershipReference: + // + // *ProjectMembershipReference_UserEmail + MembershipReference isProjectMembershipReference_MembershipReference `protobuf_oneof:"membership_reference"` +} + +func (x *ProjectMembershipReference) Reset() { + *x = ProjectMembershipReference{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_project_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProjectMembershipReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectMembershipReference) ProtoMessage() {} + +func (x *ProjectMembershipReference) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_project_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 ProjectMembershipReference.ProtoReflect.Descriptor instead. +func (*ProjectMembershipReference) Descriptor() ([]byte, []int) { + return file_controlplane_v1_project_proto_rawDescGZIP(), []int{13} +} + +func (m *ProjectMembershipReference) GetMembershipReference() isProjectMembershipReference_MembershipReference { + if m != nil { + return m.MembershipReference + } + return nil +} + +func (x *ProjectMembershipReference) GetUserEmail() string { + if x, ok := x.GetMembershipReference().(*ProjectMembershipReference_UserEmail); ok { + return x.UserEmail + } + return "" +} + +type isProjectMembershipReference_MembershipReference interface { + isProjectMembershipReference_MembershipReference() +} + +type ProjectMembershipReference_UserEmail struct { + // The user to add to the project + UserEmail string `protobuf:"bytes,3,opt,name=user_email,json=userEmail,proto3,oneof"` +} + +func (*ProjectMembershipReference_UserEmail) isProjectMembershipReference_MembershipReference() {} + type ProjectServiceAPITokenCreateResponse_APITokenFull struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -363,7 +925,7 @@ type ProjectServiceAPITokenCreateResponse_APITokenFull struct { func (x *ProjectServiceAPITokenCreateResponse_APITokenFull) Reset() { *x = ProjectServiceAPITokenCreateResponse_APITokenFull{} if protoimpl.UnsafeEnabled { - mi := &file_controlplane_v1_project_proto_msgTypes[6] + mi := &file_controlplane_v1_project_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -376,7 +938,7 @@ func (x *ProjectServiceAPITokenCreateResponse_APITokenFull) String() string { func (*ProjectServiceAPITokenCreateResponse_APITokenFull) ProtoMessage() {} func (x *ProjectServiceAPITokenCreateResponse_APITokenFull) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_project_proto_msgTypes[6] + mi := &file_controlplane_v1_project_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -413,94 +975,222 @@ var file_controlplane_v1_project_proto_rawDesc = []byte{ 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 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, 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, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf3, 0x01, 0x0a, 0x23, 0x50, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 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, 0x2a, 0x0a, 0x0c, 0x70, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x3d, - 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x01, 0x52, - 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x49, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, - 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0d, 0x0a, - 0x0b, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x69, 0x6e, 0x22, 0xd7, 0x01, 0x0a, - 0x24, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, - 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, - 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x50, 0x49, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x46, 0x75, 0x6c, 0x6c, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x1a, 0x53, 0x0a, 0x0c, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x46, 0x75, 0x6c, - 0x6c, 0x12, 0x31, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x77, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6a, 0x77, 0x74, 0x22, 0x6e, 0x0a, 0x23, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x52, 0x65, 0x76, 0x6f, 0x6b, 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, 0x2a, 0x0a, 0x0c, 0x70, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x78, - 0x0a, 0x21, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, - 0x10, 0x01, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x65, 0x76, 0x6f, 0x6b, - 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, - 0x65, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x22, 0x5b, 0x0a, 0x22, 0x50, 0x72, 0x6f, 0x6a, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x27, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x1e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0xf3, 0x01, 0x0a, + 0x23, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, + 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 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, 0x2a, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, + 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x88, 0x01, 0x01, 0x12, 0x3d, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, + 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x48, 0x01, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x49, 0x6e, + 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, + 0x69, 0x6e, 0x22, 0xd7, 0x01, 0x0a, 0x24, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x06, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x46, 0x75, 0x6c, 0x6c, 0x52, + 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x1a, 0x53, 0x0a, 0x0c, 0x41, 0x50, 0x49, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x46, 0x75, 0x6c, 0x6c, 0x12, 0x31, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x77, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6a, 0x77, 0x74, 0x22, 0x9b, 0x01, 0x0a, + 0x23, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, + 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 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, 0x57, 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 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, 0x10, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x21, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x57, 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 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, + 0x10, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, + 0x75, 0x64, 0x65, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x22, 0x5b, 0x0a, 0x22, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x35, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x52, + 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0xc5, 0x01, 0x0a, 0x20, 0x50, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 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, 0x57, 0x0a, 0x11, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 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, 0x10, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 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, 0x22, + 0xa8, 0x01, 0x0a, 0x21, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 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, 0x38, 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 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, 0xa5, 0x02, 0x0a, 0x0d, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2b, 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, 0x48, 0x00, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x2e, 0x0a, 0x05, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x18, 0x02, 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, + 0x48, 0x00, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x36, 0x0a, 0x04, 0x72, 0x6f, 0x6c, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x04, 0x72, 0x6f, 0x6c, + 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, + 0x75, 0x70, 0x64, 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, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x22, 0xa1, 0x02, 0x0a, 0x1e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x57, 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 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, 0x10, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x5e, + 0x0a, 0x10, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0f, 0x6d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x46, + 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, + 0x42, 0x0e, 0xba, 0x48, 0x0b, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x05, 0x10, 0x01, 0x22, 0x01, 0x00, + 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x21, 0x0a, 0x1f, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 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, 0xdc, 0x01, 0x0a, 0x21, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 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, + 0x57, 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 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, 0x10, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x5e, 0x0a, 0x10, 0x6d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x24, 0x0a, 0x22, 0x50, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 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, 0x5e, + 0x0a, 0x1a, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x68, 0x69, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x28, 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, 0x48, 0x00, 0x52, 0x09, 0x75, 0x73, 0x65, + 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x42, 0x16, 0x0a, 0x14, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x73, 0x68, 0x69, 0x70, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x2a, 0x77, + 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, + 0x6f, 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x4d, + 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x52, 0x4f, 0x4a, + 0x45, 0x43, 0x54, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, + 0x41, 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x50, 0x52, 0x4f, 0x4a, 0x45, + 0x43, 0x54, 0x5f, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x56, + 0x49, 0x45, 0x57, 0x45, 0x52, 0x10, 0x02, 0x32, 0xe6, 0x05, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7d, 0x0a, 0x0e, 0x41, 0x50, + 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x34, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x0c, 0x41, 0x50, 0x49, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, - 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, - 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x06, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0x87, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7d, 0x0a, 0x0e, 0x41, 0x50, 0x49, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, + 0x65, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, + 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x7d, 0x0a, 0x0e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x74, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x12, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x0c, 0x41, 0x50, 0x49, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, + 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 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, 0x6e, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x4d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 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, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, + 0x74, 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, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x7d, 0x0a, 0x0e, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x76, 0x6f, - 0x6b, 0x65, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x76, 0x6f, 0x6b, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, - 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 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, + 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 ( @@ -515,34 +1205,72 @@ func file_controlplane_v1_project_proto_rawDescGZIP() []byte { return file_controlplane_v1_project_proto_rawDescData } -var file_controlplane_v1_project_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_controlplane_v1_project_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_controlplane_v1_project_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_controlplane_v1_project_proto_goTypes = []interface{}{ - (*ProjectServiceAPITokenCreateRequest)(nil), // 0: controlplane.v1.ProjectServiceAPITokenCreateRequest - (*ProjectServiceAPITokenCreateResponse)(nil), // 1: controlplane.v1.ProjectServiceAPITokenCreateResponse - (*ProjectServiceAPITokenRevokeRequest)(nil), // 2: controlplane.v1.ProjectServiceAPITokenRevokeRequest - (*ProjectServiceAPITokenRevokeResponse)(nil), // 3: controlplane.v1.ProjectServiceAPITokenRevokeResponse - (*ProjectServiceAPITokenListRequest)(nil), // 4: controlplane.v1.ProjectServiceAPITokenListRequest - (*ProjectServiceAPITokenListResponse)(nil), // 5: controlplane.v1.ProjectServiceAPITokenListResponse - (*ProjectServiceAPITokenCreateResponse_APITokenFull)(nil), // 6: controlplane.v1.ProjectServiceAPITokenCreateResponse.APITokenFull - (*durationpb.Duration)(nil), // 7: google.protobuf.Duration - (*APITokenItem)(nil), // 8: controlplane.v1.APITokenItem + (ProjectMemberRole)(0), // 0: controlplane.v1.ProjectMemberRole + (*ProjectServiceAPITokenCreateRequest)(nil), // 1: controlplane.v1.ProjectServiceAPITokenCreateRequest + (*ProjectServiceAPITokenCreateResponse)(nil), // 2: controlplane.v1.ProjectServiceAPITokenCreateResponse + (*ProjectServiceAPITokenRevokeRequest)(nil), // 3: controlplane.v1.ProjectServiceAPITokenRevokeRequest + (*ProjectServiceAPITokenRevokeResponse)(nil), // 4: controlplane.v1.ProjectServiceAPITokenRevokeResponse + (*ProjectServiceAPITokenListRequest)(nil), // 5: controlplane.v1.ProjectServiceAPITokenListRequest + (*ProjectServiceAPITokenListResponse)(nil), // 6: controlplane.v1.ProjectServiceAPITokenListResponse + (*ProjectServiceListMembersRequest)(nil), // 7: controlplane.v1.ProjectServiceListMembersRequest + (*ProjectServiceListMembersResponse)(nil), // 8: controlplane.v1.ProjectServiceListMembersResponse + (*ProjectMember)(nil), // 9: controlplane.v1.ProjectMember + (*ProjectServiceAddMemberRequest)(nil), // 10: controlplane.v1.ProjectServiceAddMemberRequest + (*ProjectServiceAddMemberResponse)(nil), // 11: controlplane.v1.ProjectServiceAddMemberResponse + (*ProjectServiceRemoveMemberRequest)(nil), // 12: controlplane.v1.ProjectServiceRemoveMemberRequest + (*ProjectServiceRemoveMemberResponse)(nil), // 13: controlplane.v1.ProjectServiceRemoveMemberResponse + (*ProjectMembershipReference)(nil), // 14: controlplane.v1.ProjectMembershipReference + (*ProjectServiceAPITokenCreateResponse_APITokenFull)(nil), // 15: controlplane.v1.ProjectServiceAPITokenCreateResponse.APITokenFull + (*durationpb.Duration)(nil), // 16: google.protobuf.Duration + (*IdentityReference)(nil), // 17: controlplane.v1.IdentityReference + (*APITokenItem)(nil), // 18: controlplane.v1.APITokenItem + (*OffsetPaginationRequest)(nil), // 19: controlplane.v1.OffsetPaginationRequest + (*OffsetPaginationResponse)(nil), // 20: controlplane.v1.OffsetPaginationResponse + (*User)(nil), // 21: controlplane.v1.User + (*Group)(nil), // 22: controlplane.v1.Group + (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp } var file_controlplane_v1_project_proto_depIdxs = []int32{ - 7, // 0: controlplane.v1.ProjectServiceAPITokenCreateRequest.expires_in:type_name -> google.protobuf.Duration - 6, // 1: controlplane.v1.ProjectServiceAPITokenCreateResponse.result:type_name -> controlplane.v1.ProjectServiceAPITokenCreateResponse.APITokenFull - 8, // 2: controlplane.v1.ProjectServiceAPITokenListResponse.result:type_name -> controlplane.v1.APITokenItem - 8, // 3: controlplane.v1.ProjectServiceAPITokenCreateResponse.APITokenFull.item:type_name -> controlplane.v1.APITokenItem - 0, // 4: controlplane.v1.ProjectService.APITokenCreate:input_type -> controlplane.v1.ProjectServiceAPITokenCreateRequest - 4, // 5: controlplane.v1.ProjectService.APITokenList:input_type -> controlplane.v1.ProjectServiceAPITokenListRequest - 2, // 6: controlplane.v1.ProjectService.APITokenRevoke:input_type -> controlplane.v1.ProjectServiceAPITokenRevokeRequest - 1, // 7: controlplane.v1.ProjectService.APITokenCreate:output_type -> controlplane.v1.ProjectServiceAPITokenCreateResponse - 5, // 8: controlplane.v1.ProjectService.APITokenList:output_type -> controlplane.v1.ProjectServiceAPITokenListResponse - 3, // 9: controlplane.v1.ProjectService.APITokenRevoke:output_type -> controlplane.v1.ProjectServiceAPITokenRevokeResponse - 7, // [7:10] is the sub-list for method output_type - 4, // [4:7] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 16, // 0: controlplane.v1.ProjectServiceAPITokenCreateRequest.expires_in:type_name -> google.protobuf.Duration + 15, // 1: controlplane.v1.ProjectServiceAPITokenCreateResponse.result:type_name -> controlplane.v1.ProjectServiceAPITokenCreateResponse.APITokenFull + 17, // 2: controlplane.v1.ProjectServiceAPITokenRevokeRequest.project_reference:type_name -> controlplane.v1.IdentityReference + 17, // 3: controlplane.v1.ProjectServiceAPITokenListRequest.project_reference:type_name -> controlplane.v1.IdentityReference + 18, // 4: controlplane.v1.ProjectServiceAPITokenListResponse.result:type_name -> controlplane.v1.APITokenItem + 17, // 5: controlplane.v1.ProjectServiceListMembersRequest.project_reference:type_name -> controlplane.v1.IdentityReference + 19, // 6: controlplane.v1.ProjectServiceListMembersRequest.pagination:type_name -> controlplane.v1.OffsetPaginationRequest + 9, // 7: controlplane.v1.ProjectServiceListMembersResponse.members:type_name -> controlplane.v1.ProjectMember + 20, // 8: controlplane.v1.ProjectServiceListMembersResponse.pagination:type_name -> controlplane.v1.OffsetPaginationResponse + 21, // 9: controlplane.v1.ProjectMember.user:type_name -> controlplane.v1.User + 22, // 10: controlplane.v1.ProjectMember.group:type_name -> controlplane.v1.Group + 0, // 11: controlplane.v1.ProjectMember.role:type_name -> controlplane.v1.ProjectMemberRole + 23, // 12: controlplane.v1.ProjectMember.created_at:type_name -> google.protobuf.Timestamp + 23, // 13: controlplane.v1.ProjectMember.updated_at:type_name -> google.protobuf.Timestamp + 17, // 14: controlplane.v1.ProjectServiceAddMemberRequest.project_reference:type_name -> controlplane.v1.IdentityReference + 14, // 15: controlplane.v1.ProjectServiceAddMemberRequest.member_reference:type_name -> controlplane.v1.ProjectMembershipReference + 0, // 16: controlplane.v1.ProjectServiceAddMemberRequest.role:type_name -> controlplane.v1.ProjectMemberRole + 17, // 17: controlplane.v1.ProjectServiceRemoveMemberRequest.project_reference:type_name -> controlplane.v1.IdentityReference + 14, // 18: controlplane.v1.ProjectServiceRemoveMemberRequest.member_reference:type_name -> controlplane.v1.ProjectMembershipReference + 18, // 19: controlplane.v1.ProjectServiceAPITokenCreateResponse.APITokenFull.item:type_name -> controlplane.v1.APITokenItem + 1, // 20: controlplane.v1.ProjectService.APITokenCreate:input_type -> controlplane.v1.ProjectServiceAPITokenCreateRequest + 5, // 21: controlplane.v1.ProjectService.APITokenList:input_type -> controlplane.v1.ProjectServiceAPITokenListRequest + 3, // 22: controlplane.v1.ProjectService.APITokenRevoke:input_type -> controlplane.v1.ProjectServiceAPITokenRevokeRequest + 7, // 23: controlplane.v1.ProjectService.ListMembers:input_type -> controlplane.v1.ProjectServiceListMembersRequest + 10, // 24: controlplane.v1.ProjectService.AddMember:input_type -> controlplane.v1.ProjectServiceAddMemberRequest + 12, // 25: controlplane.v1.ProjectService.RemoveMember:input_type -> controlplane.v1.ProjectServiceRemoveMemberRequest + 2, // 26: controlplane.v1.ProjectService.APITokenCreate:output_type -> controlplane.v1.ProjectServiceAPITokenCreateResponse + 6, // 27: controlplane.v1.ProjectService.APITokenList:output_type -> controlplane.v1.ProjectServiceAPITokenListResponse + 4, // 28: controlplane.v1.ProjectService.APITokenRevoke:output_type -> controlplane.v1.ProjectServiceAPITokenRevokeResponse + 8, // 29: controlplane.v1.ProjectService.ListMembers:output_type -> controlplane.v1.ProjectServiceListMembersResponse + 11, // 30: controlplane.v1.ProjectService.AddMember:output_type -> controlplane.v1.ProjectServiceAddMemberResponse + 13, // 31: controlplane.v1.ProjectService.RemoveMember:output_type -> controlplane.v1.ProjectServiceRemoveMemberResponse + 26, // [26:32] is the sub-list for method output_type + 20, // [20:26] 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_project_proto_init() } @@ -550,7 +1278,10 @@ func file_controlplane_v1_project_proto_init() { if File_controlplane_v1_project_proto != nil { return } + 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_project_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ProjectServiceAPITokenCreateRequest); i { @@ -625,6 +1356,102 @@ func file_controlplane_v1_project_proto_init() { } } file_controlplane_v1_project_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProjectServiceListMembersRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_project_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProjectServiceListMembersResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_project_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProjectMember); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_project_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProjectServiceAddMemberRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_project_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProjectServiceAddMemberResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_project_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProjectServiceRemoveMemberRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_project_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProjectServiceRemoveMemberResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_project_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProjectMembershipReference); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_project_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ProjectServiceAPITokenCreateResponse_APITokenFull); i { case 0: return &v.state @@ -638,18 +1465,26 @@ func file_controlplane_v1_project_proto_init() { } } file_controlplane_v1_project_proto_msgTypes[0].OneofWrappers = []interface{}{} + file_controlplane_v1_project_proto_msgTypes[8].OneofWrappers = []interface{}{ + (*ProjectMember_User)(nil), + (*ProjectMember_Group)(nil), + } + file_controlplane_v1_project_proto_msgTypes[13].OneofWrappers = []interface{}{ + (*ProjectMembershipReference_UserEmail)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controlplane_v1_project_proto_rawDesc, - NumEnums: 0, - NumMessages: 7, + NumEnums: 1, + NumMessages: 15, NumExtensions: 0, NumServices: 1, }, GoTypes: file_controlplane_v1_project_proto_goTypes, DependencyIndexes: file_controlplane_v1_project_proto_depIdxs, + EnumInfos: file_controlplane_v1_project_proto_enumTypes, MessageInfos: file_controlplane_v1_project_proto_msgTypes, }.Build() File_controlplane_v1_project_proto = out.File diff --git a/app/controlplane/api/controlplane/v1/project.proto b/app/controlplane/api/controlplane/v1/project.proto index 9d90d0f3e..5e386fcae 100644 --- a/app/controlplane/api/controlplane/v1/project.proto +++ b/app/controlplane/api/controlplane/v1/project.proto @@ -18,8 +18,12 @@ syntax = "proto3"; package controlplane.v1; import "buf/validate/validate.proto"; +import "controlplane/v1/group.proto"; +import "controlplane/v1/pagination.proto"; import "controlplane/v1/response_messages.proto"; +import "controlplane/v1/shared_message.proto"; import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1"; @@ -28,6 +32,10 @@ service ProjectService { rpc APITokenCreate(ProjectServiceAPITokenCreateRequest) returns (ProjectServiceAPITokenCreateResponse); rpc APITokenList(ProjectServiceAPITokenListRequest) returns (ProjectServiceAPITokenListResponse); rpc APITokenRevoke(ProjectServiceAPITokenRevokeRequest) returns (ProjectServiceAPITokenRevokeResponse); + // Project membership management + rpc ListMembers(ProjectServiceListMembersRequest) returns (ProjectServiceListMembersResponse); + rpc AddMember(ProjectServiceAddMemberRequest) returns (ProjectServiceAddMemberResponse); + rpc RemoveMember(ProjectServiceRemoveMemberRequest) returns (ProjectServiceRemoveMemberResponse); } message ProjectServiceAPITokenCreateRequest { @@ -46,19 +54,106 @@ message ProjectServiceAPITokenCreateResponse { } } +// ProjectServiceAPITokenRevokeRequest contains the information needed to revoke an API token for a project message ProjectServiceAPITokenRevokeRequest { - // token name + // The name of the API token to revoke string name = 1 [(buf.validate.field).string.min_len = 1]; - string project_name = 2 [(buf.validate.field).string.min_len = 1]; + // IdentityReference is used to specify the project by either its ID or name + IdentityReference project_reference = 2 [(buf.validate.field).required = true]; } +// ProjectServiceAPITokenRevokeResponse is returned upon successful revocation of an API token message ProjectServiceAPITokenRevokeResponse {} +// ProjectServiceAPITokenListRequest contains the information needed to list API tokens for a project message ProjectServiceAPITokenListRequest { - string project_name = 1 [(buf.validate.field).string.min_len = 1]; + // IdentityReference is used to specify the project by either its ID or name + IdentityReference project_reference = 1 [(buf.validate.field).required = true]; + // Flag to include revoked tokens in the list bool include_revoked = 2; } message ProjectServiceAPITokenListResponse { repeated APITokenItem result = 1; } + +// ProjectServiceListMembersRequest contains the information needed to list members of a project +message ProjectServiceListMembersRequest { + // IdentityReference is used to specify the project by either its ID or name + IdentityReference project_reference = 1 [(buf.validate.field).required = true]; + // Pagination parameters to limit and offset results + OffsetPaginationRequest pagination = 2; +} + +// ProjectServiceListMembersResponse contains the list of members in a project +message ProjectServiceListMembersResponse { + // The list of members in the project + repeated ProjectMember members = 1; + // Pagination information for the response + OffsetPaginationResponse pagination = 2; +} + +// ProjectMember represents an user or group who is a member of a project +message ProjectMember { + // Subject can be either a user or a group + oneof subject { + // The user who is a member of the project + User user = 1; + // The group who is a member of the project + Group group = 2; + } + // The role of the user in the project + ProjectMemberRole role = 3; + // Timestamp when the project membership was created + google.protobuf.Timestamp created_at = 4; + // Timestamp when the project membership was last modified + google.protobuf.Timestamp updated_at = 5; +} + +// ProjectServiceAddMemberRequest contains the information needed to add a user to a project +message ProjectServiceAddMemberRequest { + // IdentityReference is used to specify the project by either its ID or name + IdentityReference project_reference = 1 [(buf.validate.field).required = true]; + // The membership reference can be a user email or groups references in the future + ProjectMembershipReference member_reference = 2 [(buf.validate.field).required = true]; + // Indicates if the user should be added as an admin + ProjectMemberRole role = 3 [ + (buf.validate.field).required = true, + (buf.validate.field).enum = { + defined_only: true + not_in: [0] + } + ]; +} + +// ProjectServiceAddMemberResponse contains the result of adding a user to a project +message ProjectServiceAddMemberResponse {} + +message ProjectServiceRemoveMemberRequest { + // IdentityReference is used to specify the project by either its ID or name + IdentityReference project_reference = 1 [(buf.validate.field).required = true]; + // The membership reference can be a user email or groups references in the future + ProjectMembershipReference member_reference = 2 [(buf.validate.field).required = true]; +} + +// ProjectServiceRemoveMemberResponse is returned upon successful removal of a user from a project +message ProjectServiceRemoveMemberResponse {} + +// ProjectMembershipReference is used to reference a user or group in the context of project membership +message ProjectMembershipReference { + // The membership reference can be a user email or groups references in the future + oneof membership_reference { + // The user to add to the project + string user_email = 3 [(buf.validate.field).string.email = true]; + } +} + +// ProjectMemberRole defines the roles a member can have in a project +enum ProjectMemberRole { + // Default role for a project member + PROJECT_MEMBER_ROLE_UNSPECIFIED = 0; + // Admin role for a project member + PROJECT_MEMBER_ROLE_ADMIN = 1; + // Viewer role for a project member + PROJECT_MEMBER_ROLE_VIEWER = 2; +} diff --git a/app/controlplane/api/controlplane/v1/project_grpc.pb.go b/app/controlplane/api/controlplane/v1/project_grpc.pb.go index 98aff31ce..1f68affd8 100644 --- a/app/controlplane/api/controlplane/v1/project_grpc.pb.go +++ b/app/controlplane/api/controlplane/v1/project_grpc.pb.go @@ -37,6 +37,9 @@ const ( ProjectService_APITokenCreate_FullMethodName = "/controlplane.v1.ProjectService/APITokenCreate" ProjectService_APITokenList_FullMethodName = "/controlplane.v1.ProjectService/APITokenList" ProjectService_APITokenRevoke_FullMethodName = "/controlplane.v1.ProjectService/APITokenRevoke" + ProjectService_ListMembers_FullMethodName = "/controlplane.v1.ProjectService/ListMembers" + ProjectService_AddMember_FullMethodName = "/controlplane.v1.ProjectService/AddMember" + ProjectService_RemoveMember_FullMethodName = "/controlplane.v1.ProjectService/RemoveMember" ) // ProjectServiceClient is the client API for ProjectService service. @@ -47,6 +50,10 @@ type ProjectServiceClient interface { APITokenCreate(ctx context.Context, in *ProjectServiceAPITokenCreateRequest, opts ...grpc.CallOption) (*ProjectServiceAPITokenCreateResponse, error) APITokenList(ctx context.Context, in *ProjectServiceAPITokenListRequest, opts ...grpc.CallOption) (*ProjectServiceAPITokenListResponse, error) APITokenRevoke(ctx context.Context, in *ProjectServiceAPITokenRevokeRequest, opts ...grpc.CallOption) (*ProjectServiceAPITokenRevokeResponse, error) + // Project membership management + ListMembers(ctx context.Context, in *ProjectServiceListMembersRequest, opts ...grpc.CallOption) (*ProjectServiceListMembersResponse, error) + AddMember(ctx context.Context, in *ProjectServiceAddMemberRequest, opts ...grpc.CallOption) (*ProjectServiceAddMemberResponse, error) + RemoveMember(ctx context.Context, in *ProjectServiceRemoveMemberRequest, opts ...grpc.CallOption) (*ProjectServiceRemoveMemberResponse, error) } type projectServiceClient struct { @@ -84,6 +91,33 @@ func (c *projectServiceClient) APITokenRevoke(ctx context.Context, in *ProjectSe return out, nil } +func (c *projectServiceClient) ListMembers(ctx context.Context, in *ProjectServiceListMembersRequest, opts ...grpc.CallOption) (*ProjectServiceListMembersResponse, error) { + out := new(ProjectServiceListMembersResponse) + err := c.cc.Invoke(ctx, ProjectService_ListMembers_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) AddMember(ctx context.Context, in *ProjectServiceAddMemberRequest, opts ...grpc.CallOption) (*ProjectServiceAddMemberResponse, error) { + out := new(ProjectServiceAddMemberResponse) + err := c.cc.Invoke(ctx, ProjectService_AddMember_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) RemoveMember(ctx context.Context, in *ProjectServiceRemoveMemberRequest, opts ...grpc.CallOption) (*ProjectServiceRemoveMemberResponse, error) { + out := new(ProjectServiceRemoveMemberResponse) + err := c.cc.Invoke(ctx, ProjectService_RemoveMember_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ProjectServiceServer is the server API for ProjectService service. // All implementations must embed UnimplementedProjectServiceServer // for forward compatibility @@ -92,6 +126,10 @@ type ProjectServiceServer interface { APITokenCreate(context.Context, *ProjectServiceAPITokenCreateRequest) (*ProjectServiceAPITokenCreateResponse, error) APITokenList(context.Context, *ProjectServiceAPITokenListRequest) (*ProjectServiceAPITokenListResponse, error) APITokenRevoke(context.Context, *ProjectServiceAPITokenRevokeRequest) (*ProjectServiceAPITokenRevokeResponse, error) + // Project membership management + ListMembers(context.Context, *ProjectServiceListMembersRequest) (*ProjectServiceListMembersResponse, error) + AddMember(context.Context, *ProjectServiceAddMemberRequest) (*ProjectServiceAddMemberResponse, error) + RemoveMember(context.Context, *ProjectServiceRemoveMemberRequest) (*ProjectServiceRemoveMemberResponse, error) mustEmbedUnimplementedProjectServiceServer() } @@ -108,6 +146,15 @@ func (UnimplementedProjectServiceServer) APITokenList(context.Context, *ProjectS func (UnimplementedProjectServiceServer) APITokenRevoke(context.Context, *ProjectServiceAPITokenRevokeRequest) (*ProjectServiceAPITokenRevokeResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method APITokenRevoke not implemented") } +func (UnimplementedProjectServiceServer) ListMembers(context.Context, *ProjectServiceListMembersRequest) (*ProjectServiceListMembersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListMembers not implemented") +} +func (UnimplementedProjectServiceServer) AddMember(context.Context, *ProjectServiceAddMemberRequest) (*ProjectServiceAddMemberResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddMember not implemented") +} +func (UnimplementedProjectServiceServer) RemoveMember(context.Context, *ProjectServiceRemoveMemberRequest) (*ProjectServiceRemoveMemberResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveMember not implemented") +} func (UnimplementedProjectServiceServer) mustEmbedUnimplementedProjectServiceServer() {} // UnsafeProjectServiceServer may be embedded to opt out of forward compatibility for this service. @@ -175,6 +222,60 @@ func _ProjectService_APITokenRevoke_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _ProjectService_ListMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProjectServiceListMembersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).ListMembers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_ListMembers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).ListMembers(ctx, req.(*ProjectServiceListMembersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_AddMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProjectServiceAddMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).AddMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_AddMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).AddMember(ctx, req.(*ProjectServiceAddMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_RemoveMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProjectServiceRemoveMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).RemoveMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_RemoveMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).RemoveMember(ctx, req.(*ProjectServiceRemoveMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ProjectService_ServiceDesc is the grpc.ServiceDesc for ProjectService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -194,6 +295,18 @@ var ProjectService_ServiceDesc = grpc.ServiceDesc{ MethodName: "APITokenRevoke", Handler: _ProjectService_APITokenRevoke_Handler, }, + { + MethodName: "ListMembers", + Handler: _ProjectService_ListMembers_Handler, + }, + { + MethodName: "AddMember", + Handler: _ProjectService_AddMember_Handler, + }, + { + MethodName: "RemoveMember", + Handler: _ProjectService_RemoveMember_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "controlplane/v1/project.proto", diff --git a/app/cli/cmd/group.go b/app/controlplane/api/controlplane/v1/shared_message.go similarity index 50% rename from app/cli/cmd/group.go rename to app/controlplane/api/controlplane/v1/shared_message.go index 78aec221f..8fd0e8847 100644 --- a/app/cli/cmd/group.go +++ b/app/controlplane/api/controlplane/v1/shared_message.go @@ -13,21 +13,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cmd +package v1 import ( - "github.com/spf13/cobra" + "fmt" + + "github.com/google/uuid" ) -func newGroupCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "group", - Aliases: []string{"groups", "grp", "g"}, - Short: "Group management", - Hidden: true, +// Parse is a helper method to parse an IdentityReference from the protobuf message. +func (i *IdentityReference) Parse() (*uuid.UUID, *string, error) { + if i.GetId() != "" && i.GetName() != "" { + return nil, nil, fmt.Errorf("cannot provide both ID and name") } - cmd.AddCommand(newGroupCreateCmd(), newGroupDescribeCmd(), newGroupUpdateCmd(), newGroupListCmd(), newGroupDeleteCmd(), newGroupMembersCmd()) + if i.GetId() != "" { + identityUUID, err := uuid.Parse(i.GetId()) + if err != nil { + return nil, nil, fmt.Errorf("invalid identity ID") + } + return &identityUUID, nil, nil + } else if i.GetName() != "" { + identityName := i.GetName() + return nil, &identityName, nil + } - return cmd + return nil, nil, nil } diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/project.ts b/app/controlplane/api/gen/frontend/controlplane/v1/project.ts index 4371e408e..4eaac0234 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/project.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/project.ts @@ -3,10 +3,57 @@ import { grpc } from "@improbable-eng/grpc-web"; import { BrowserHeaders } from "browser-headers"; import _m0 from "protobufjs/minimal"; import { Duration } from "../../google/protobuf/duration"; -import { APITokenItem } from "./response_messages"; +import { Timestamp } from "../../google/protobuf/timestamp"; +import { Group } from "./group"; +import { OffsetPaginationRequest, OffsetPaginationResponse } from "./pagination"; +import { APITokenItem, User } from "./response_messages"; +import { IdentityReference } from "./shared_message"; export const protobufPackage = "controlplane.v1"; +/** ProjectMemberRole defines the roles a member can have in a project */ +export enum ProjectMemberRole { + /** PROJECT_MEMBER_ROLE_UNSPECIFIED - Default role for a project member */ + PROJECT_MEMBER_ROLE_UNSPECIFIED = 0, + /** PROJECT_MEMBER_ROLE_ADMIN - Admin role for a project member */ + PROJECT_MEMBER_ROLE_ADMIN = 1, + /** PROJECT_MEMBER_ROLE_VIEWER - Viewer role for a project member */ + PROJECT_MEMBER_ROLE_VIEWER = 2, + UNRECOGNIZED = -1, +} + +export function projectMemberRoleFromJSON(object: any): ProjectMemberRole { + switch (object) { + case 0: + case "PROJECT_MEMBER_ROLE_UNSPECIFIED": + return ProjectMemberRole.PROJECT_MEMBER_ROLE_UNSPECIFIED; + case 1: + case "PROJECT_MEMBER_ROLE_ADMIN": + return ProjectMemberRole.PROJECT_MEMBER_ROLE_ADMIN; + case 2: + case "PROJECT_MEMBER_ROLE_VIEWER": + return ProjectMemberRole.PROJECT_MEMBER_ROLE_VIEWER; + case -1: + case "UNRECOGNIZED": + default: + return ProjectMemberRole.UNRECOGNIZED; + } +} + +export function projectMemberRoleToJSON(object: ProjectMemberRole): string { + switch (object) { + case ProjectMemberRole.PROJECT_MEMBER_ROLE_UNSPECIFIED: + return "PROJECT_MEMBER_ROLE_UNSPECIFIED"; + case ProjectMemberRole.PROJECT_MEMBER_ROLE_ADMIN: + return "PROJECT_MEMBER_ROLE_ADMIN"; + case ProjectMemberRole.PROJECT_MEMBER_ROLE_VIEWER: + return "PROJECT_MEMBER_ROLE_VIEWER"; + case ProjectMemberRole.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + export interface ProjectServiceAPITokenCreateRequest { name: string; projectName: string; @@ -23,17 +70,23 @@ export interface ProjectServiceAPITokenCreateResponse_APITokenFull { jwt: string; } +/** ProjectServiceAPITokenRevokeRequest contains the information needed to revoke an API token for a project */ export interface ProjectServiceAPITokenRevokeRequest { - /** token name */ + /** The name of the API token to revoke */ name: string; - projectName: string; + /** IdentityReference is used to specify the project by either its ID or name */ + projectReference?: IdentityReference; } +/** ProjectServiceAPITokenRevokeResponse is returned upon successful revocation of an API token */ export interface ProjectServiceAPITokenRevokeResponse { } +/** ProjectServiceAPITokenListRequest contains the information needed to list API tokens for a project */ export interface ProjectServiceAPITokenListRequest { - projectName: string; + /** IdentityReference is used to specify the project by either its ID or name */ + projectReference?: IdentityReference; + /** Flag to include revoked tokens in the list */ includeRevoked: boolean; } @@ -41,6 +94,71 @@ export interface ProjectServiceAPITokenListResponse { result: APITokenItem[]; } +/** ProjectServiceListMembersRequest contains the information needed to list members of a project */ +export interface ProjectServiceListMembersRequest { + /** IdentityReference is used to specify the project by either its ID or name */ + projectReference?: IdentityReference; + /** Pagination parameters to limit and offset results */ + pagination?: OffsetPaginationRequest; +} + +/** ProjectServiceListMembersResponse contains the list of members in a project */ +export interface ProjectServiceListMembersResponse { + /** The list of members in the project */ + members: ProjectMember[]; + /** Pagination information for the response */ + pagination?: OffsetPaginationResponse; +} + +/** ProjectMember represents an user or group who is a member of a project */ +export interface ProjectMember { + /** The user who is a member of the project */ + user?: + | User + | undefined; + /** The group who is a member of the project */ + group?: + | Group + | undefined; + /** The role of the user in the project */ + role: ProjectMemberRole; + /** Timestamp when the project membership was created */ + createdAt?: Date; + /** Timestamp when the project membership was last modified */ + updatedAt?: Date; +} + +/** ProjectServiceAddMemberRequest contains the information needed to add a user to a project */ +export interface ProjectServiceAddMemberRequest { + /** IdentityReference is used to specify the project by either its ID or name */ + projectReference?: IdentityReference; + /** The membership reference can be a user email or groups references in the future */ + memberReference?: ProjectMembershipReference; + /** Indicates if the user should be added as an admin */ + role: ProjectMemberRole; +} + +/** ProjectServiceAddMemberResponse contains the result of adding a user to a project */ +export interface ProjectServiceAddMemberResponse { +} + +export interface ProjectServiceRemoveMemberRequest { + /** IdentityReference is used to specify the project by either its ID or name */ + projectReference?: IdentityReference; + /** The membership reference can be a user email or groups references in the future */ + memberReference?: ProjectMembershipReference; +} + +/** ProjectServiceRemoveMemberResponse is returned upon successful removal of a user from a project */ +export interface ProjectServiceRemoveMemberResponse { +} + +/** ProjectMembershipReference is used to reference a user or group in the context of project membership */ +export interface ProjectMembershipReference { + /** The user to add to the project */ + userEmail?: string | undefined; +} + function createBaseProjectServiceAPITokenCreateRequest(): ProjectServiceAPITokenCreateRequest { return { name: "", projectName: "", description: undefined, expiresIn: undefined }; } @@ -294,7 +412,7 @@ export const ProjectServiceAPITokenCreateResponse_APITokenFull = { }; function createBaseProjectServiceAPITokenRevokeRequest(): ProjectServiceAPITokenRevokeRequest { - return { name: "", projectName: "" }; + return { name: "", projectReference: undefined }; } export const ProjectServiceAPITokenRevokeRequest = { @@ -302,8 +420,8 @@ export const ProjectServiceAPITokenRevokeRequest = { if (message.name !== "") { writer.uint32(10).string(message.name); } - if (message.projectName !== "") { - writer.uint32(18).string(message.projectName); + if (message.projectReference !== undefined) { + IdentityReference.encode(message.projectReference, writer.uint32(18).fork()).ldelim(); } return writer; }, @@ -327,7 +445,7 @@ export const ProjectServiceAPITokenRevokeRequest = { break; } - message.projectName = reader.string(); + message.projectReference = IdentityReference.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { @@ -341,14 +459,19 @@ export const ProjectServiceAPITokenRevokeRequest = { fromJSON(object: any): ProjectServiceAPITokenRevokeRequest { return { name: isSet(object.name) ? String(object.name) : "", - projectName: isSet(object.projectName) ? String(object.projectName) : "", + projectReference: isSet(object.projectReference) + ? IdentityReference.fromJSON(object.projectReference) + : undefined, }; }, toJSON(message: ProjectServiceAPITokenRevokeRequest): unknown { const obj: any = {}; message.name !== undefined && (obj.name = message.name); - message.projectName !== undefined && (obj.projectName = message.projectName); + message.projectReference !== undefined && + (obj.projectReference = message.projectReference + ? IdentityReference.toJSON(message.projectReference) + : undefined); return obj; }, @@ -363,7 +486,9 @@ export const ProjectServiceAPITokenRevokeRequest = { ): ProjectServiceAPITokenRevokeRequest { const message = createBaseProjectServiceAPITokenRevokeRequest(); message.name = object.name ?? ""; - message.projectName = object.projectName ?? ""; + message.projectReference = (object.projectReference !== undefined && object.projectReference !== null) + ? IdentityReference.fromPartial(object.projectReference) + : undefined; return message; }, }; @@ -417,13 +542,13 @@ export const ProjectServiceAPITokenRevokeResponse = { }; function createBaseProjectServiceAPITokenListRequest(): ProjectServiceAPITokenListRequest { - return { projectName: "", includeRevoked: false }; + return { projectReference: undefined, includeRevoked: false }; } export const ProjectServiceAPITokenListRequest = { encode(message: ProjectServiceAPITokenListRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.projectName !== "") { - writer.uint32(10).string(message.projectName); + if (message.projectReference !== undefined) { + IdentityReference.encode(message.projectReference, writer.uint32(10).fork()).ldelim(); } if (message.includeRevoked === true) { writer.uint32(16).bool(message.includeRevoked); @@ -443,7 +568,7 @@ export const ProjectServiceAPITokenListRequest = { break; } - message.projectName = reader.string(); + message.projectReference = IdentityReference.decode(reader, reader.uint32()); continue; case 2: if (tag !== 16) { @@ -463,14 +588,19 @@ export const ProjectServiceAPITokenListRequest = { fromJSON(object: any): ProjectServiceAPITokenListRequest { return { - projectName: isSet(object.projectName) ? String(object.projectName) : "", + projectReference: isSet(object.projectReference) + ? IdentityReference.fromJSON(object.projectReference) + : undefined, includeRevoked: isSet(object.includeRevoked) ? Boolean(object.includeRevoked) : false, }; }, toJSON(message: ProjectServiceAPITokenListRequest): unknown { const obj: any = {}; - message.projectName !== undefined && (obj.projectName = message.projectName); + message.projectReference !== undefined && + (obj.projectReference = message.projectReference + ? IdentityReference.toJSON(message.projectReference) + : undefined); message.includeRevoked !== undefined && (obj.includeRevoked = message.includeRevoked); return obj; }, @@ -485,7 +615,9 @@ export const ProjectServiceAPITokenListRequest = { object: I, ): ProjectServiceAPITokenListRequest { const message = createBaseProjectServiceAPITokenListRequest(); - message.projectName = object.projectName ?? ""; + message.projectReference = (object.projectReference !== undefined && object.projectReference !== null) + ? IdentityReference.fromPartial(object.projectReference) + : undefined; message.includeRevoked = object.includeRevoked ?? false; return message; }, @@ -555,6 +687,618 @@ export const ProjectServiceAPITokenListResponse = { }, }; +function createBaseProjectServiceListMembersRequest(): ProjectServiceListMembersRequest { + return { projectReference: undefined, pagination: undefined }; +} + +export const ProjectServiceListMembersRequest = { + encode(message: ProjectServiceListMembersRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.projectReference !== undefined) { + IdentityReference.encode(message.projectReference, writer.uint32(10).fork()).ldelim(); + } + if (message.pagination !== undefined) { + OffsetPaginationRequest.encode(message.pagination, writer.uint32(18).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProjectServiceListMembersRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProjectServiceListMembersRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.projectReference = IdentityReference.decode(reader, reader.uint32()); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.pagination = OffsetPaginationRequest.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): ProjectServiceListMembersRequest { + return { + projectReference: isSet(object.projectReference) + ? IdentityReference.fromJSON(object.projectReference) + : undefined, + pagination: isSet(object.pagination) ? OffsetPaginationRequest.fromJSON(object.pagination) : undefined, + }; + }, + + toJSON(message: ProjectServiceListMembersRequest): unknown { + const obj: any = {}; + message.projectReference !== undefined && + (obj.projectReference = message.projectReference + ? IdentityReference.toJSON(message.projectReference) + : undefined); + message.pagination !== undefined && + (obj.pagination = message.pagination ? OffsetPaginationRequest.toJSON(message.pagination) : undefined); + return obj; + }, + + create, I>>( + base?: I, + ): ProjectServiceListMembersRequest { + return ProjectServiceListMembersRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): ProjectServiceListMembersRequest { + const message = createBaseProjectServiceListMembersRequest(); + message.projectReference = (object.projectReference !== undefined && object.projectReference !== null) + ? IdentityReference.fromPartial(object.projectReference) + : undefined; + message.pagination = (object.pagination !== undefined && object.pagination !== null) + ? OffsetPaginationRequest.fromPartial(object.pagination) + : undefined; + return message; + }, +}; + +function createBaseProjectServiceListMembersResponse(): ProjectServiceListMembersResponse { + return { members: [], pagination: undefined }; +} + +export const ProjectServiceListMembersResponse = { + encode(message: ProjectServiceListMembersResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + for (const v of message.members) { + ProjectMember.encode(v!, writer.uint32(10).fork()).ldelim(); + } + if (message.pagination !== undefined) { + OffsetPaginationResponse.encode(message.pagination, writer.uint32(18).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProjectServiceListMembersResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProjectServiceListMembersResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.members.push(ProjectMember.decode(reader, reader.uint32())); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.pagination = OffsetPaginationResponse.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): ProjectServiceListMembersResponse { + return { + members: Array.isArray(object?.members) ? object.members.map((e: any) => ProjectMember.fromJSON(e)) : [], + pagination: isSet(object.pagination) ? OffsetPaginationResponse.fromJSON(object.pagination) : undefined, + }; + }, + + toJSON(message: ProjectServiceListMembersResponse): unknown { + const obj: any = {}; + if (message.members) { + obj.members = message.members.map((e) => e ? ProjectMember.toJSON(e) : undefined); + } else { + obj.members = []; + } + message.pagination !== undefined && + (obj.pagination = message.pagination ? OffsetPaginationResponse.toJSON(message.pagination) : undefined); + return obj; + }, + + create, I>>( + base?: I, + ): ProjectServiceListMembersResponse { + return ProjectServiceListMembersResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): ProjectServiceListMembersResponse { + const message = createBaseProjectServiceListMembersResponse(); + message.members = object.members?.map((e) => ProjectMember.fromPartial(e)) || []; + message.pagination = (object.pagination !== undefined && object.pagination !== null) + ? OffsetPaginationResponse.fromPartial(object.pagination) + : undefined; + return message; + }, +}; + +function createBaseProjectMember(): ProjectMember { + return { user: undefined, group: undefined, role: 0, createdAt: undefined, updatedAt: undefined }; +} + +export const ProjectMember = { + encode(message: ProjectMember, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.user !== undefined) { + User.encode(message.user, writer.uint32(10).fork()).ldelim(); + } + if (message.group !== undefined) { + Group.encode(message.group, writer.uint32(18).fork()).ldelim(); + } + if (message.role !== 0) { + writer.uint32(24).int32(message.role); + } + if (message.createdAt !== undefined) { + Timestamp.encode(toTimestamp(message.createdAt), writer.uint32(34).fork()).ldelim(); + } + if (message.updatedAt !== undefined) { + Timestamp.encode(toTimestamp(message.updatedAt), writer.uint32(42).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProjectMember { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProjectMember(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.user = User.decode(reader, reader.uint32()); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.group = Group.decode(reader, reader.uint32()); + continue; + case 3: + if (tag !== 24) { + break; + } + + message.role = reader.int32() as any; + continue; + case 4: + if (tag !== 34) { + break; + } + + message.createdAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.updatedAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): ProjectMember { + return { + user: isSet(object.user) ? User.fromJSON(object.user) : undefined, + group: isSet(object.group) ? Group.fromJSON(object.group) : undefined, + role: isSet(object.role) ? projectMemberRoleFromJSON(object.role) : 0, + createdAt: isSet(object.createdAt) ? fromJsonTimestamp(object.createdAt) : undefined, + updatedAt: isSet(object.updatedAt) ? fromJsonTimestamp(object.updatedAt) : undefined, + }; + }, + + toJSON(message: ProjectMember): unknown { + const obj: any = {}; + message.user !== undefined && (obj.user = message.user ? User.toJSON(message.user) : undefined); + message.group !== undefined && (obj.group = message.group ? Group.toJSON(message.group) : undefined); + message.role !== undefined && (obj.role = projectMemberRoleToJSON(message.role)); + message.createdAt !== undefined && (obj.createdAt = message.createdAt.toISOString()); + message.updatedAt !== undefined && (obj.updatedAt = message.updatedAt.toISOString()); + return obj; + }, + + create, I>>(base?: I): ProjectMember { + return ProjectMember.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): ProjectMember { + const message = createBaseProjectMember(); + message.user = (object.user !== undefined && object.user !== null) ? User.fromPartial(object.user) : undefined; + message.group = (object.group !== undefined && object.group !== null) ? Group.fromPartial(object.group) : undefined; + message.role = object.role ?? 0; + message.createdAt = object.createdAt ?? undefined; + message.updatedAt = object.updatedAt ?? undefined; + return message; + }, +}; + +function createBaseProjectServiceAddMemberRequest(): ProjectServiceAddMemberRequest { + return { projectReference: undefined, memberReference: undefined, role: 0 }; +} + +export const ProjectServiceAddMemberRequest = { + encode(message: ProjectServiceAddMemberRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.projectReference !== undefined) { + IdentityReference.encode(message.projectReference, writer.uint32(10).fork()).ldelim(); + } + if (message.memberReference !== undefined) { + ProjectMembershipReference.encode(message.memberReference, writer.uint32(18).fork()).ldelim(); + } + if (message.role !== 0) { + writer.uint32(24).int32(message.role); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProjectServiceAddMemberRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProjectServiceAddMemberRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.projectReference = IdentityReference.decode(reader, reader.uint32()); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.memberReference = ProjectMembershipReference.decode(reader, reader.uint32()); + continue; + case 3: + if (tag !== 24) { + break; + } + + message.role = reader.int32() as any; + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): ProjectServiceAddMemberRequest { + return { + projectReference: isSet(object.projectReference) + ? IdentityReference.fromJSON(object.projectReference) + : undefined, + memberReference: isSet(object.memberReference) + ? ProjectMembershipReference.fromJSON(object.memberReference) + : undefined, + role: isSet(object.role) ? projectMemberRoleFromJSON(object.role) : 0, + }; + }, + + toJSON(message: ProjectServiceAddMemberRequest): unknown { + const obj: any = {}; + message.projectReference !== undefined && + (obj.projectReference = message.projectReference + ? IdentityReference.toJSON(message.projectReference) + : undefined); + message.memberReference !== undefined && (obj.memberReference = message.memberReference + ? ProjectMembershipReference.toJSON(message.memberReference) + : undefined); + message.role !== undefined && (obj.role = projectMemberRoleToJSON(message.role)); + return obj; + }, + + create, I>>(base?: I): ProjectServiceAddMemberRequest { + return ProjectServiceAddMemberRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): ProjectServiceAddMemberRequest { + const message = createBaseProjectServiceAddMemberRequest(); + message.projectReference = (object.projectReference !== undefined && object.projectReference !== null) + ? IdentityReference.fromPartial(object.projectReference) + : undefined; + message.memberReference = (object.memberReference !== undefined && object.memberReference !== null) + ? ProjectMembershipReference.fromPartial(object.memberReference) + : undefined; + message.role = object.role ?? 0; + return message; + }, +}; + +function createBaseProjectServiceAddMemberResponse(): ProjectServiceAddMemberResponse { + return {}; +} + +export const ProjectServiceAddMemberResponse = { + encode(_: ProjectServiceAddMemberResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProjectServiceAddMemberResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProjectServiceAddMemberResponse(); + 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): ProjectServiceAddMemberResponse { + return {}; + }, + + toJSON(_: ProjectServiceAddMemberResponse): unknown { + const obj: any = {}; + return obj; + }, + + create, I>>(base?: I): ProjectServiceAddMemberResponse { + return ProjectServiceAddMemberResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>(_: I): ProjectServiceAddMemberResponse { + const message = createBaseProjectServiceAddMemberResponse(); + return message; + }, +}; + +function createBaseProjectServiceRemoveMemberRequest(): ProjectServiceRemoveMemberRequest { + return { projectReference: undefined, memberReference: undefined }; +} + +export const ProjectServiceRemoveMemberRequest = { + encode(message: ProjectServiceRemoveMemberRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.projectReference !== undefined) { + IdentityReference.encode(message.projectReference, writer.uint32(10).fork()).ldelim(); + } + if (message.memberReference !== undefined) { + ProjectMembershipReference.encode(message.memberReference, writer.uint32(18).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProjectServiceRemoveMemberRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProjectServiceRemoveMemberRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.projectReference = IdentityReference.decode(reader, reader.uint32()); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.memberReference = ProjectMembershipReference.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): ProjectServiceRemoveMemberRequest { + return { + projectReference: isSet(object.projectReference) + ? IdentityReference.fromJSON(object.projectReference) + : undefined, + memberReference: isSet(object.memberReference) + ? ProjectMembershipReference.fromJSON(object.memberReference) + : undefined, + }; + }, + + toJSON(message: ProjectServiceRemoveMemberRequest): unknown { + const obj: any = {}; + message.projectReference !== undefined && + (obj.projectReference = message.projectReference + ? IdentityReference.toJSON(message.projectReference) + : undefined); + message.memberReference !== undefined && (obj.memberReference = message.memberReference + ? ProjectMembershipReference.toJSON(message.memberReference) + : undefined); + return obj; + }, + + create, I>>( + base?: I, + ): ProjectServiceRemoveMemberRequest { + return ProjectServiceRemoveMemberRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): ProjectServiceRemoveMemberRequest { + const message = createBaseProjectServiceRemoveMemberRequest(); + message.projectReference = (object.projectReference !== undefined && object.projectReference !== null) + ? IdentityReference.fromPartial(object.projectReference) + : undefined; + message.memberReference = (object.memberReference !== undefined && object.memberReference !== null) + ? ProjectMembershipReference.fromPartial(object.memberReference) + : undefined; + return message; + }, +}; + +function createBaseProjectServiceRemoveMemberResponse(): ProjectServiceRemoveMemberResponse { + return {}; +} + +export const ProjectServiceRemoveMemberResponse = { + encode(_: ProjectServiceRemoveMemberResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProjectServiceRemoveMemberResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProjectServiceRemoveMemberResponse(); + 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): ProjectServiceRemoveMemberResponse { + return {}; + }, + + toJSON(_: ProjectServiceRemoveMemberResponse): unknown { + const obj: any = {}; + return obj; + }, + + create, I>>( + base?: I, + ): ProjectServiceRemoveMemberResponse { + return ProjectServiceRemoveMemberResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + _: I, + ): ProjectServiceRemoveMemberResponse { + const message = createBaseProjectServiceRemoveMemberResponse(); + return message; + }, +}; + +function createBaseProjectMembershipReference(): ProjectMembershipReference { + return { userEmail: undefined }; +} + +export const ProjectMembershipReference = { + encode(message: ProjectMembershipReference, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.userEmail !== undefined) { + writer.uint32(26).string(message.userEmail); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): ProjectMembershipReference { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseProjectMembershipReference(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + 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): ProjectMembershipReference { + return { userEmail: isSet(object.userEmail) ? String(object.userEmail) : undefined }; + }, + + toJSON(message: ProjectMembershipReference): unknown { + const obj: any = {}; + message.userEmail !== undefined && (obj.userEmail = message.userEmail); + return obj; + }, + + create, I>>(base?: I): ProjectMembershipReference { + return ProjectMembershipReference.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): ProjectMembershipReference { + const message = createBaseProjectMembershipReference(); + message.userEmail = object.userEmail ?? undefined; + return message; + }, +}; + export interface ProjectService { /** Project level API tokens */ APITokenCreate( @@ -569,6 +1313,19 @@ export interface ProjectService { request: DeepPartial, metadata?: grpc.Metadata, ): Promise; + /** Project membership management */ + ListMembers( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + AddMember( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; + RemoveMember( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; } export class ProjectServiceClientImpl implements ProjectService { @@ -579,6 +1336,9 @@ export class ProjectServiceClientImpl implements ProjectService { this.APITokenCreate = this.APITokenCreate.bind(this); this.APITokenList = this.APITokenList.bind(this); this.APITokenRevoke = this.APITokenRevoke.bind(this); + this.ListMembers = this.ListMembers.bind(this); + this.AddMember = this.AddMember.bind(this); + this.RemoveMember = this.RemoveMember.bind(this); } APITokenCreate( @@ -613,6 +1373,35 @@ export class ProjectServiceClientImpl implements ProjectService { metadata, ); } + + ListMembers( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + ProjectServiceListMembersDesc, + ProjectServiceListMembersRequest.fromPartial(request), + metadata, + ); + } + + AddMember( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary(ProjectServiceAddMemberDesc, ProjectServiceAddMemberRequest.fromPartial(request), metadata); + } + + RemoveMember( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary( + ProjectServiceRemoveMemberDesc, + ProjectServiceRemoveMemberRequest.fromPartial(request), + metadata, + ); + } } export const ProjectServiceDesc = { serviceName: "controlplane.v1.ProjectService" }; @@ -686,6 +1475,75 @@ export const ProjectServiceAPITokenRevokeDesc: UnaryMethodDefinitionish = { } as any, }; +export const ProjectServiceListMembersDesc: UnaryMethodDefinitionish = { + methodName: "ListMembers", + service: ProjectServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return ProjectServiceListMembersRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = ProjectServiceListMembersResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const ProjectServiceAddMemberDesc: UnaryMethodDefinitionish = { + methodName: "AddMember", + service: ProjectServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return ProjectServiceAddMemberRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = ProjectServiceAddMemberResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + +export const ProjectServiceRemoveMemberDesc: UnaryMethodDefinitionish = { + methodName: "RemoveMember", + service: ProjectServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return ProjectServiceRemoveMemberRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = ProjectServiceRemoveMemberResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + interface UnaryMethodDefinitionishR extends grpc.UnaryMethodDefinition { requestStream: any; responseStream: any; @@ -784,6 +1642,28 @@ 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 toTimestamp(date: Date): Timestamp { + const seconds = date.getTime() / 1_000; + const nanos = (date.getTime() % 1_000) * 1_000_000; + return { seconds, nanos }; +} + +function fromTimestamp(t: Timestamp): Date { + let millis = (t.seconds || 0) * 1_000; + millis += (t.nanos || 0) / 1_000_000; + return new Date(millis); +} + +function fromJsonTimestamp(o: any): Date { + if (o instanceof Date) { + return o; + } else if (typeof o === "string") { + return new Date(o); + } else { + return fromTimestamp(Timestamp.fromJSON(o)); + } +} + function isSet(value: any): boolean { return value !== null && value !== undefined; } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMember.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMember.jsonschema.json new file mode 100644 index 000000000..4e044ac1e --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMember.jsonschema.json @@ -0,0 +1,55 @@ +{ + "$id": "controlplane.v1.ProjectMember.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectMember represents an user or group who is a member of a project", + "patternProperties": { + "^(created_at)$": { + "$ref": "google.protobuf.Timestamp.jsonschema.json", + "description": "Timestamp when the project membership was created" + }, + "^(updated_at)$": { + "$ref": "google.protobuf.Timestamp.jsonschema.json", + "description": "Timestamp when the project membership was last modified" + } + }, + "properties": { + "createdAt": { + "$ref": "google.protobuf.Timestamp.jsonschema.json", + "description": "Timestamp when the project membership was created" + }, + "group": { + "$ref": "controlplane.v1.Group.jsonschema.json", + "description": "The group who is a member of the project" + }, + "role": { + "anyOf": [ + { + "enum": [ + "PROJECT_MEMBER_ROLE_UNSPECIFIED", + "PROJECT_MEMBER_ROLE_ADMIN", + "PROJECT_MEMBER_ROLE_VIEWER" + ], + "title": "Project Member Role", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "The role of the user in the project" + }, + "updatedAt": { + "$ref": "google.protobuf.Timestamp.jsonschema.json", + "description": "Timestamp when the project membership was last modified" + }, + "user": { + "$ref": "controlplane.v1.User.jsonschema.json", + "description": "The user who is a member of the project" + } + }, + "title": "Project Member", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMember.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMember.schema.json new file mode 100644 index 000000000..b3965d425 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMember.schema.json @@ -0,0 +1,55 @@ +{ + "$id": "controlplane.v1.ProjectMember.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectMember represents an user or group who is a member of a project", + "patternProperties": { + "^(createdAt)$": { + "$ref": "google.protobuf.Timestamp.schema.json", + "description": "Timestamp when the project membership was created" + }, + "^(updatedAt)$": { + "$ref": "google.protobuf.Timestamp.schema.json", + "description": "Timestamp when the project membership was last modified" + } + }, + "properties": { + "created_at": { + "$ref": "google.protobuf.Timestamp.schema.json", + "description": "Timestamp when the project membership was created" + }, + "group": { + "$ref": "controlplane.v1.Group.schema.json", + "description": "The group who is a member of the project" + }, + "role": { + "anyOf": [ + { + "enum": [ + "PROJECT_MEMBER_ROLE_UNSPECIFIED", + "PROJECT_MEMBER_ROLE_ADMIN", + "PROJECT_MEMBER_ROLE_VIEWER" + ], + "title": "Project Member Role", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "The role of the user in the project" + }, + "updated_at": { + "$ref": "google.protobuf.Timestamp.schema.json", + "description": "Timestamp when the project membership was last modified" + }, + "user": { + "$ref": "controlplane.v1.User.schema.json", + "description": "The user who is a member of the project" + } + }, + "title": "Project Member", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMembershipReference.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMembershipReference.jsonschema.json new file mode 100644 index 000000000..da370b942 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMembershipReference.jsonschema.json @@ -0,0 +1,22 @@ +{ + "$id": "controlplane.v1.ProjectMembershipReference.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectMembershipReference is used to reference a user or group in the context of project membership", + "patternProperties": { + "^(user_email)$": { + "description": "The user to add to the project", + "format": "email", + "type": "string" + } + }, + "properties": { + "userEmail": { + "description": "The user to add to the project", + "format": "email", + "type": "string" + } + }, + "title": "Project Membership Reference", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMembershipReference.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMembershipReference.schema.json new file mode 100644 index 000000000..809e35e42 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectMembershipReference.schema.json @@ -0,0 +1,22 @@ +{ + "$id": "controlplane.v1.ProjectMembershipReference.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectMembershipReference is used to reference a user or group in the context of project membership", + "patternProperties": { + "^(userEmail)$": { + "description": "The user to add to the project", + "format": "email", + "type": "string" + } + }, + "properties": { + "user_email": { + "description": "The user to add to the project", + "format": "email", + "type": "string" + } + }, + "title": "Project Membership Reference", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenListRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenListRequest.jsonschema.json index 0f6261e85..090f0dd82 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenListRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenListRequest.jsonschema.json @@ -2,24 +2,30 @@ "$id": "controlplane.v1.ProjectServiceAPITokenListRequest.jsonschema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "description": "ProjectServiceAPITokenListRequest contains the information needed to list API tokens for a project", "patternProperties": { "^(include_revoked)$": { + "description": "Flag to include revoked tokens in the list", "type": "boolean" }, - "^(project_name)$": { - "minLength": 1, - "type": "string" + "^(project_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" } }, "properties": { "includeRevoked": { + "description": "Flag to include revoked tokens in the list", "type": "boolean" }, - "projectName": { - "minLength": 1, - "type": "string" + "projectReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" } }, + "required": [ + "project_reference" + ], "title": "Project ServiceAPI Token List Request", "type": "object" } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenListRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenListRequest.schema.json index d7c094871..d8a87e383 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenListRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenListRequest.schema.json @@ -2,24 +2,30 @@ "$id": "controlplane.v1.ProjectServiceAPITokenListRequest.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "description": "ProjectServiceAPITokenListRequest contains the information needed to list API tokens for a project", "patternProperties": { "^(includeRevoked)$": { + "description": "Flag to include revoked tokens in the list", "type": "boolean" }, - "^(projectName)$": { - "minLength": 1, - "type": "string" + "^(projectReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" } }, "properties": { "include_revoked": { + "description": "Flag to include revoked tokens in the list", "type": "boolean" }, - "project_name": { - "minLength": 1, - "type": "string" + "project_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" } }, + "required": [ + "project_reference" + ], "title": "Project ServiceAPI Token List Request", "type": "object" } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeRequest.jsonschema.json index bc6e26064..ddc885dd8 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeRequest.jsonschema.json @@ -2,23 +2,27 @@ "$id": "controlplane.v1.ProjectServiceAPITokenRevokeRequest.jsonschema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "description": "ProjectServiceAPITokenRevokeRequest contains the information needed to revoke an API token for a project", "patternProperties": { - "^(project_name)$": { - "minLength": 1, - "type": "string" + "^(project_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" } }, "properties": { "name": { - "description": "token name", + "description": "The name of the API token to revoke", "minLength": 1, "type": "string" }, - "projectName": { - "minLength": 1, - "type": "string" + "projectReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" } }, + "required": [ + "project_reference" + ], "title": "Project ServiceAPI Token Revoke Request", "type": "object" } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeRequest.schema.json index 4be4ad48c..996876a58 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeRequest.schema.json @@ -2,23 +2,27 @@ "$id": "controlplane.v1.ProjectServiceAPITokenRevokeRequest.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "description": "ProjectServiceAPITokenRevokeRequest contains the information needed to revoke an API token for a project", "patternProperties": { - "^(projectName)$": { - "minLength": 1, - "type": "string" + "^(projectReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" } }, "properties": { "name": { - "description": "token name", + "description": "The name of the API token to revoke", "minLength": 1, "type": "string" }, - "project_name": { - "minLength": 1, - "type": "string" + "project_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" } }, + "required": [ + "project_reference" + ], "title": "Project ServiceAPI Token Revoke Request", "type": "object" } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeResponse.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeResponse.jsonschema.json index 143896acd..47b2bb95a 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeResponse.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeResponse.jsonschema.json @@ -2,6 +2,7 @@ "$id": "controlplane.v1.ProjectServiceAPITokenRevokeResponse.jsonschema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "description": "ProjectServiceAPITokenRevokeResponse is returned upon successful revocation of an API token", "properties": {}, "title": "Project ServiceAPI Token Revoke Response", "type": "object" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeResponse.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeResponse.schema.json index 03d8f410f..e035ec41c 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeResponse.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAPITokenRevokeResponse.schema.json @@ -2,6 +2,7 @@ "$id": "controlplane.v1.ProjectServiceAPITokenRevokeResponse.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "description": "ProjectServiceAPITokenRevokeResponse is returned upon successful revocation of an API token", "properties": {}, "title": "Project ServiceAPI Token Revoke Response", "type": "object" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberRequest.jsonschema.json new file mode 100644 index 000000000..f4a875052 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberRequest.jsonschema.json @@ -0,0 +1,52 @@ +{ + "$id": "controlplane.v1.ProjectServiceAddMemberRequest.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceAddMemberRequest contains the information needed to add a user to a project", + "patternProperties": { + "^(member_reference)$": { + "$ref": "controlplane.v1.ProjectMembershipReference.jsonschema.json", + "description": "The membership reference can be a user email or groups references in the future" + }, + "^(project_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "properties": { + "memberReference": { + "$ref": "controlplane.v1.ProjectMembershipReference.jsonschema.json", + "description": "The membership reference can be a user email or groups references in the future" + }, + "projectReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + }, + "role": { + "anyOf": [ + { + "enum": [ + "PROJECT_MEMBER_ROLE_UNSPECIFIED", + "PROJECT_MEMBER_ROLE_ADMIN", + "PROJECT_MEMBER_ROLE_VIEWER" + ], + "title": "Project Member Role", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Indicates if the user should be added as an admin" + } + }, + "required": [ + "project_reference", + "member_reference", + "role" + ], + "title": "Project Service Add Member Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberRequest.schema.json new file mode 100644 index 000000000..6a98b9044 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberRequest.schema.json @@ -0,0 +1,52 @@ +{ + "$id": "controlplane.v1.ProjectServiceAddMemberRequest.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceAddMemberRequest contains the information needed to add a user to a project", + "patternProperties": { + "^(memberReference)$": { + "$ref": "controlplane.v1.ProjectMembershipReference.schema.json", + "description": "The membership reference can be a user email or groups references in the future" + }, + "^(projectReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "properties": { + "member_reference": { + "$ref": "controlplane.v1.ProjectMembershipReference.schema.json", + "description": "The membership reference can be a user email or groups references in the future" + }, + "project_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + }, + "role": { + "anyOf": [ + { + "enum": [ + "PROJECT_MEMBER_ROLE_UNSPECIFIED", + "PROJECT_MEMBER_ROLE_ADMIN", + "PROJECT_MEMBER_ROLE_VIEWER" + ], + "title": "Project Member Role", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Indicates if the user should be added as an admin" + } + }, + "required": [ + "project_reference", + "member_reference", + "role" + ], + "title": "Project Service Add Member Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberResponse.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberResponse.jsonschema.json new file mode 100644 index 000000000..0b458c46a --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberResponse.jsonschema.json @@ -0,0 +1,9 @@ +{ + "$id": "controlplane.v1.ProjectServiceAddMemberResponse.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceAddMemberResponse contains the result of adding a user to a project", + "properties": {}, + "title": "Project Service Add Member Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberResponse.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberResponse.schema.json new file mode 100644 index 000000000..d3cf61791 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceAddMemberResponse.schema.json @@ -0,0 +1,9 @@ +{ + "$id": "controlplane.v1.ProjectServiceAddMemberResponse.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceAddMemberResponse contains the result of adding a user to a project", + "properties": {}, + "title": "Project Service Add Member Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersRequest.jsonschema.json new file mode 100644 index 000000000..4eb771bb9 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersRequest.jsonschema.json @@ -0,0 +1,27 @@ +{ + "$id": "controlplane.v1.ProjectServiceListMembersRequest.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceListMembersRequest contains the information needed to list members of a project", + "patternProperties": { + "^(project_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "properties": { + "pagination": { + "$ref": "controlplane.v1.OffsetPaginationRequest.jsonschema.json", + "description": "Pagination parameters to limit and offset results" + }, + "projectReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "required": [ + "project_reference" + ], + "title": "Project Service List Members Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersRequest.schema.json new file mode 100644 index 000000000..82bc105a1 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersRequest.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "controlplane.v1.ProjectServiceListMembersRequest.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceListMembersRequest contains the information needed to list members of a project", + "patternProperties": { + "^(projectReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "properties": { + "pagination": { + "$ref": "controlplane.v1.OffsetPaginationRequest.schema.json", + "description": "Pagination parameters to limit and offset results" + }, + "project_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "required": [ + "project_reference" + ], + "title": "Project Service List Members Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersResponse.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersResponse.jsonschema.json new file mode 100644 index 000000000..6bde1e02f --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersResponse.jsonschema.json @@ -0,0 +1,21 @@ +{ + "$id": "controlplane.v1.ProjectServiceListMembersResponse.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceListMembersResponse contains the list of members in a project", + "properties": { + "members": { + "description": "The list of members in the project", + "items": { + "$ref": "controlplane.v1.ProjectMember.jsonschema.json" + }, + "type": "array" + }, + "pagination": { + "$ref": "controlplane.v1.OffsetPaginationResponse.jsonschema.json", + "description": "Pagination information for the response" + } + }, + "title": "Project Service List Members Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersResponse.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersResponse.schema.json new file mode 100644 index 000000000..b24e19393 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceListMembersResponse.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "controlplane.v1.ProjectServiceListMembersResponse.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceListMembersResponse contains the list of members in a project", + "properties": { + "members": { + "description": "The list of members in the project", + "items": { + "$ref": "controlplane.v1.ProjectMember.schema.json" + }, + "type": "array" + }, + "pagination": { + "$ref": "controlplane.v1.OffsetPaginationResponse.schema.json", + "description": "Pagination information for the response" + } + }, + "title": "Project Service List Members Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberRequest.jsonschema.json new file mode 100644 index 000000000..c99e8d5d8 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberRequest.jsonschema.json @@ -0,0 +1,31 @@ +{ + "$id": "controlplane.v1.ProjectServiceRemoveMemberRequest.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(member_reference)$": { + "$ref": "controlplane.v1.ProjectMembershipReference.jsonschema.json", + "description": "The membership reference can be a user email or groups references in the future" + }, + "^(project_reference)$": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "properties": { + "memberReference": { + "$ref": "controlplane.v1.ProjectMembershipReference.jsonschema.json", + "description": "The membership reference can be a user email or groups references in the future" + }, + "projectReference": { + "$ref": "controlplane.v1.IdentityReference.jsonschema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "required": [ + "project_reference", + "member_reference" + ], + "title": "Project Service Remove Member Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberRequest.schema.json new file mode 100644 index 000000000..fd1fe262e --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberRequest.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "controlplane.v1.ProjectServiceRemoveMemberRequest.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "patternProperties": { + "^(memberReference)$": { + "$ref": "controlplane.v1.ProjectMembershipReference.schema.json", + "description": "The membership reference can be a user email or groups references in the future" + }, + "^(projectReference)$": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "properties": { + "member_reference": { + "$ref": "controlplane.v1.ProjectMembershipReference.schema.json", + "description": "The membership reference can be a user email or groups references in the future" + }, + "project_reference": { + "$ref": "controlplane.v1.IdentityReference.schema.json", + "description": "IdentityReference is used to specify the project by either its ID or name" + } + }, + "required": [ + "project_reference", + "member_reference" + ], + "title": "Project Service Remove Member Request", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberResponse.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberResponse.jsonschema.json new file mode 100644 index 000000000..44d9b61bf --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberResponse.jsonschema.json @@ -0,0 +1,9 @@ +{ + "$id": "controlplane.v1.ProjectServiceRemoveMemberResponse.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceRemoveMemberResponse is returned upon successful removal of a user from a project", + "properties": {}, + "title": "Project Service Remove Member Response", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberResponse.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberResponse.schema.json new file mode 100644 index 000000000..7f4bf6bfd --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ProjectServiceRemoveMemberResponse.schema.json @@ -0,0 +1,9 @@ +{ + "$id": "controlplane.v1.ProjectServiceRemoveMemberResponse.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "ProjectServiceRemoveMemberResponse is returned upon successful removal of a user from a project", + "properties": {}, + "title": "Project Service Remove Member Response", + "type": "object" +} diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 9cb7fbc64..55cdbb1af 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -128,7 +128,7 @@ 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) + projectUseCase := biz.NewProjectsUseCase(logger, projectsRepo, membershipRepo, auditorUseCase) groupRepo := data.NewGroupRepo(dataData, logger) groupUseCase := biz.NewGroupUseCase(logger, groupRepo, membershipRepo, auditorUseCase) v5 := serviceOpts(logger, enforcer, projectUseCase, groupUseCase) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 2177af859..8c1324d0b 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -155,7 +155,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer } // Apply RBAC on the project - if _, err = s.userHasPermissionOnProject(ctx, org.ID, req.ProjectName, authz.PolicyWorkflowRunCreate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, org.ID, &cpAPI.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowRunCreate); err != nil { return nil, err } @@ -233,7 +233,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe } // Apply RBAC on the project - if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wf.Project}, authz.PolicyWorkflowRunCreate); err != nil { return nil, err } @@ -372,7 +372,7 @@ func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationS } // Apply RBAC on the project - if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunUpdate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wf.Project}, authz.PolicyWorkflowRunUpdate); err != nil { return nil, err } @@ -422,7 +422,7 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.Atte } // Apply RBAC on the project - if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wRun.Workflow.Project, authz.PolicyWorkflowRunCreate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wRun.Workflow.Project}, authz.PolicyWorkflowRunCreate); err != nil { return nil, err } @@ -673,7 +673,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // try to load project and apply RBAC if needed - if _, err := s.userHasPermissionOnProject(ctx, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); err != nil { + if _, err := s.userHasPermissionOnProject(ctx, apiToken.OrgID, &cpAPI.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowCreate); err != nil { // if the project is not found, we ignore the error, since we'll create the project in this call if !errors.IsNotFound(err) { return nil, err diff --git a/app/controlplane/internal/service/attestationstate.go b/app/controlplane/internal/service/attestationstate.go index 1167c007e..4ff75cb51 100644 --- a/app/controlplane/internal/service/attestationstate.go +++ b/app/controlplane/internal/service/attestationstate.go @@ -69,7 +69,7 @@ func (s *AttestationStateService) Initialized(ctx context.Context, req *cpAPI.At } // Apply RBAC on the project - if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wf.Project}, authz.PolicyWorkflowRunCreate); err != nil { return nil, err } @@ -96,7 +96,7 @@ func (s *AttestationStateService) Save(ctx context.Context, req *cpAPI.Attestati } // Apply RBAC on the project - if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wf.Project}, authz.PolicyWorkflowRunCreate); err != nil { return nil, err } @@ -129,7 +129,7 @@ func (s *AttestationStateService) Read(ctx context.Context, req *cpAPI.Attestati } // Apply RBAC on the project - if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunRead); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wf.Project}, authz.PolicyWorkflowRunRead); err != nil { return nil, err } @@ -163,7 +163,7 @@ func (s *AttestationStateService) Reset(ctx context.Context, req *cpAPI.Attestat } // Apply RBAC on the project - if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunUpdate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, &cpAPI.IdentityReference{Name: &wf.Project}, authz.PolicyWorkflowRunUpdate); err != nil { return nil, err } diff --git a/app/controlplane/internal/service/group.go b/app/controlplane/internal/service/group.go index e2c72d524..e47ab5ee1 100644 --- a/app/controlplane/internal/service/group.go +++ b/app/controlplane/internal/service/group.go @@ -17,6 +17,7 @@ package service import ( "context" + "fmt" pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" @@ -88,9 +89,9 @@ func (g *GroupService) Get(ctx context.Context, req *pb.GroupServiceGetRequest) } // Parse groupID and groupName from the request - id, name, err := g.parseIdentityReference(req.GetGroupReference()) + id, name, err := req.GetGroupReference().Parse() if err != nil { - return nil, err + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid group reference: %s", err.Error())) } // Initialize the options for getting the group @@ -173,9 +174,9 @@ func (g *GroupService) Update(ctx context.Context, req *pb.GroupServiceUpdateReq } // Parse groupID and groupName from the request - id, name, err := g.parseIdentityReference(req.GetGroupReference()) + id, name, err := req.GetGroupReference().Parse() if err != nil { - return nil, err + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) } // Update the group with the provided options @@ -212,9 +213,9 @@ func (g *GroupService) Delete(ctx context.Context, req *pb.GroupServiceDeleteReq idReference := &biz.IdentityReference{} // Parse groupID and groupName from the request - idReference.ID, idReference.Name, err = g.parseIdentityReference(req.GetGroupReference()) + idReference.ID, idReference.Name, err = req.GetGroupReference().Parse() if err != nil { - return nil, err + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) } err = g.groupUseCase.Delete(ctx, orgUUID, idReference) @@ -246,9 +247,9 @@ func (g *GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceList } // Parse groupID and groupName from the request - opts.ID, opts.Name, err = g.parseIdentityReference(req.GetGroupReference()) + opts.ID, opts.Name, err = req.GetGroupReference().Parse() if err != nil { - return nil, err + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) } // Initialize the pagination options, with default values @@ -309,9 +310,9 @@ func (g *GroupService) AddMember(ctx context.Context, req *pb.GroupServiceAddMem } // Parse groupID and groupName from the request - addOpts.ID, addOpts.Name, err = g.parseIdentityReference(req.GetGroupReference()) + addOpts.ID, addOpts.Name, err = req.GetGroupReference().Parse() if err != nil { - return nil, err + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) } // Call the business logic to add the member @@ -359,9 +360,9 @@ func (g *GroupService) RemoveMember(ctx context.Context, req *pb.GroupServiceRem } // Parse groupID and groupName from the request - removeOpts.ID, removeOpts.Name, err = g.parseIdentityReference(req.GetGroupReference()) + removeOpts.ID, removeOpts.Name, err = req.GetGroupReference().Parse() if err != nil { - return nil, err + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) } // Call the business logic to remove the member diff --git a/app/controlplane/internal/service/project.go b/app/controlplane/internal/service/project.go index 4a3e56b4d..6b2d1db33 100644 --- a/app/controlplane/internal/service/project.go +++ b/app/controlplane/internal/service/project.go @@ -17,24 +17,30 @@ package service import ( "context" + "fmt" "time" pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/timestamppb" ) type ProjectService struct { pb.UnimplementedProjectServiceServer *service - APITokenUseCase *biz.APITokenUseCase + // Use Cases + apiTokenUseCase *biz.APITokenUseCase } -func NewProjectService(uc *biz.APITokenUseCase, opts ...NewOpt) *ProjectService { +func NewProjectService(apiTokenUseCase *biz.APITokenUseCase, opts ...NewOpt) *ProjectService { return &ProjectService{ service: newService(opts...), - APITokenUseCase: uc, + apiTokenUseCase: apiTokenUseCase, } } @@ -45,7 +51,7 @@ func (s *ProjectService) APITokenCreate(ctx context.Context, req *pb.ProjectServ } // Make sure the provided project exists and the user has permission to create tokens in it - project, err := s.userHasPermissionOnProject(ctx, currentOrg.ID, req.ProjectName, authz.PolicyProjectAPITokenCreate) + project, err := s.userHasPermissionOnProject(ctx, currentOrg.ID, &pb.IdentityReference{Name: &req.ProjectName}, authz.PolicyProjectAPITokenCreate) if err != nil { return nil, err } @@ -56,7 +62,7 @@ func (s *ProjectService) APITokenCreate(ctx context.Context, req *pb.ProjectServ *expiresIn = req.ExpiresIn.AsDuration() } - token, err := s.APITokenUseCase.Create(ctx, req.Name, req.Description, expiresIn, currentOrg.ID, biz.APITokenWithProject(project)) + token, err := s.apiTokenUseCase.Create(ctx, req.Name, req.Description, expiresIn, currentOrg.ID, biz.APITokenWithProject(project)) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -76,12 +82,12 @@ func (s *ProjectService) APITokenList(ctx context.Context, req *pb.ProjectServic } // Make sure the provided project exists and the user has permission to create tokens in it - project, err := s.userHasPermissionOnProject(ctx, currentOrg.ID, req.ProjectName, authz.PolicyProjectAPITokenList) + project, err := s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectReference(), authz.PolicyProjectAPITokenList) if err != nil { return nil, err } - tokens, err := s.APITokenUseCase.List(ctx, currentOrg.ID, req.IncludeRevoked, biz.APITokenWithProject(project)) + tokens, err := s.apiTokenUseCase.List(ctx, currentOrg.ID, req.IncludeRevoked, biz.APITokenWithProject(project)) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -101,19 +107,254 @@ func (s *ProjectService) APITokenRevoke(ctx context.Context, req *pb.ProjectServ } // Make sure the provided project exists and the user has permission to create tokens in it - project, err := s.userHasPermissionOnProject(ctx, currentOrg.ID, req.ProjectName, authz.PolicyProjectAPITokenRevoke) + project, err := s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectReference(), authz.PolicyProjectAPITokenRevoke) if err != nil { return nil, err } - t, err := s.APITokenUseCase.FindByNameInOrg(ctx, currentOrg.ID, req.Name, biz.APITokenWithProject(project)) + t, err := s.apiTokenUseCase.FindByNameInOrg(ctx, currentOrg.ID, req.Name, biz.APITokenWithProject(project)) if err != nil { return nil, handleUseCaseErr(err, s.log) } - if err := s.APITokenUseCase.Revoke(ctx, currentOrg.ID, t.ID.String()); err != nil { + if err := s.apiTokenUseCase.Revoke(ctx, currentOrg.ID, t.ID.String()); err != nil { return nil, handleUseCaseErr(err, s.log) } return &pb.ProjectServiceAPITokenRevokeResponse{}, nil } + +// ListMembers lists the members of a project. +func (s *ProjectService) ListMembers(ctx context.Context, req *pb.ProjectServiceListMembersRequest) (*pb.ProjectServiceListMembersResponse, error) { + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + // Make sure the user has permission to list members of the project + _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.ProjectReference, authz.PolicyProjectListMemberships) + if err != nil { + return nil, err + } + + // Convert organization ID from string to UUID + orgUUID, err := uuid.Parse(currentOrg.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Create the identity reference for the project + identityRef := &biz.IdentityReference{} + + // Parse projectID and projectName from the request + identityRef.ID, identityRef.Name, err = req.GetProjectReference().Parse() + if err != nil { + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) + } + + // Initialize the pagination options, with default values + paginationOpts, err := initializePaginationOpts(req.GetPagination()) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Call the business logic to list members + members, total, err := s.projectUseCase.ListMembers(ctx, orgUUID, identityRef, paginationOpts) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Convert the project members to protobuf messages + result := make([]*pb.ProjectMember, 0, len(members)) + for _, mem := range members { + result = append(result, bizProjectMembershipToPb(mem)) + } + + return &pb.ProjectServiceListMembersResponse{ + Members: result, + Pagination: paginationToPb(total, paginationOpts.Offset(), paginationOpts.Limit()), + }, nil +} + +// AddMember adds a member to a project. +func (s *ProjectService) AddMember(ctx context.Context, req *pb.ProjectServiceAddMemberRequest) (*pb.ProjectServiceAddMemberResponse, error) { + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + // Make sure the user has permission to add members to the project + _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.ProjectReference, authz.PolicyProjectAddMemberships) + if err != nil { + return nil, err + } + + // Get current user ID from context + currentUser, err := requireCurrentUser(ctx) + if err != nil { + return nil, err + } + + requesterUUID, err := uuid.Parse(currentUser.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + orgUUID, err := uuid.Parse(currentOrg.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Create the identity reference for the project + identityRef := &biz.IdentityReference{} + + // Parse projectID and projectName from the request + identityRef.ID, identityRef.Name, err = req.GetProjectReference().Parse() + if err != nil { + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) + } + + // Extract the user email from the oneof MembershipReference field + userEmail, err := s.extractUserEmailFromMembershipReference(req.GetMemberReference()) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Convert from protobuf role to internal authorization role + role := mapProjectMemberRoleToAuthzRole(req.Role) + + // Prepare options for adding a member + opts := &biz.AddMemberToProjectOpts{ + ProjectReference: identityRef, + UserEmail: userEmail, + RequesterID: requesterUUID, + Role: role, + } + + // Call the business logic to add the member + _, err = s.projectUseCase.AddMemberToProject(ctx, orgUUID, opts) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + return &pb.ProjectServiceAddMemberResponse{}, nil +} + +// RemoveMember removes a member from a project. +func (s *ProjectService) RemoveMember(ctx context.Context, req *pb.ProjectServiceRemoveMemberRequest) (*pb.ProjectServiceRemoveMemberResponse, error) { + currentOrg, err := requireCurrentOrg(ctx) + if err != nil { + return nil, err + } + + // Make sure the user has permission to remove members from the project + _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.ProjectReference, authz.PolicyProjectRemoveMemberships) + if err != nil { + return nil, err + } + + // Get current user ID from context + currentUser, err := requireCurrentUser(ctx) + if err != nil { + return nil, err + } + + requesterUUID, err := uuid.Parse(currentUser.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + orgUUID, err := uuid.Parse(currentOrg.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Create the identity reference for the project + identityRef := &biz.IdentityReference{} + + // Parse projectID and projectName from the request + identityRef.ID, identityRef.Name, err = req.GetProjectReference().Parse() + if err != nil { + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) + } + + // Extract the user email from the oneof MembershipReference field + userEmail, err := s.extractUserEmailFromMembershipReference(req.GetMemberReference()) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Prepare options for removing a member + opts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: identityRef, + UserEmail: userEmail, + RequesterID: requesterUUID, + } + + // Call the business logic to remove the member + err = s.projectUseCase.RemoveMemberFromProject(ctx, orgUUID, opts) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + return &pb.ProjectServiceRemoveMemberResponse{}, nil +} + +// extractUserEmailFromMembershipReference extracts the user email from a membership reference +// Returns the user email and an error if the email is not provided or the membership reference is nil +func (s *ProjectService) extractUserEmailFromMembershipReference(membershipRef *pb.ProjectMembershipReference) (string, error) { + if membershipRef == nil { + return "", biz.NewErrValidationStr("membership reference is required") + } + + userEmail := membershipRef.GetUserEmail() + if userEmail == "" { + return "", biz.NewErrValidationStr("user email is required") + } + + return userEmail, nil +} + +// MapProjectMemberRoleToAuthzRole maps a ProjectMemberRole from protobuf to an authz.Role +func mapProjectMemberRoleToAuthzRole(role pb.ProjectMemberRole) authz.Role { + switch role { + case pb.ProjectMemberRole_PROJECT_MEMBER_ROLE_ADMIN: + return authz.RoleProjectAdmin + case pb.ProjectMemberRole_PROJECT_MEMBER_ROLE_VIEWER: + return authz.RoleProjectViewer + default: + // Default to viewer role for safety + return authz.RoleProjectViewer + } +} + +// bizProjectMembershipToPb converts a biz.ProjectMembership to a pb.ProjectMember +func bizProjectMembershipToPb(m *biz.ProjectMembership) *pb.ProjectMember { + var role pb.ProjectMemberRole + + // Map the role string back to a protobuf enum + switch m.Role { + case authz.RoleProjectAdmin: + role = pb.ProjectMemberRole_PROJECT_MEMBER_ROLE_ADMIN + case authz.RoleProjectViewer: + role = pb.ProjectMemberRole_PROJECT_MEMBER_ROLE_VIEWER + default: + role = pb.ProjectMemberRole_PROJECT_MEMBER_ROLE_UNSPECIFIED + } + + pbMember := &pb.ProjectMember{ + Subject: &pb.ProjectMember_User{ + User: bizUserToPb(m.User), + }, + Role: role, + } + + if m.CreatedAt != nil { + pbMember.CreatedAt = timestamppb.New(*m.CreatedAt) + } + if m.UpdatedAt != nil { + pbMember.UpdatedAt = timestamppb.New(*m.UpdatedAt) + } + + return pbMember +} diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index f2688743a..7a941429e 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -212,8 +212,15 @@ func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resou // by name in the given organization and ensures that the user has a role that allows that specific operation in the project. // check authorizeResource method // if it doesn't return an error, it means that the user has the permission and the project is returned -func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, pName string, policy *authz.Policy) (*biz.Project, error) { - p, err := s.projectUseCase.FindProjectByReference(ctx, orgID, &biz.EntityRef{Name: pName}) +func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, ref *pb.IdentityReference, policy *authz.Policy) (*biz.Project, error) { + // Parse entity ID and entity Name from the request + entityID, entityName, err := ref.Parse() + if err != nil { + return nil, errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) + } + + // Find the project by its reference + p, err := s.projectUseCase.FindProjectByReference(ctx, orgID, &biz.IdentityReference{ID: entityID, Name: entityName}) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -254,9 +261,9 @@ func (s *service) userHasPermissionOnGroupMembershipsWithPolicy(ctx context.Cont return nil } - groupID, groupName, err := s.parseIdentityReference(groupIdentifier) + groupID, groupName, err := groupIdentifier.Parse() if err != nil { - return handleUseCaseErr(err, s.log) + return errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) } orgUUID, err := uuid.Parse(orgID) @@ -316,25 +323,6 @@ 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 diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index 298304cc7..5caaae330 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -59,7 +59,7 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre return nil, err } - if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, &pb.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowCreate); err != nil { // if the project is not found, we ignore the error, since we'll create the project in this call if !errors.IsNotFound(err) { return nil, err @@ -100,7 +100,7 @@ func (s *WorkflowService) Update(ctx context.Context, req *pb.WorkflowServiceUpd return nil, err } - if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowUpdate); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, &pb.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowUpdate); err != nil { return nil, err } @@ -243,7 +243,7 @@ func (s *WorkflowService) Delete(ctx context.Context, req *pb.WorkflowServiceDel return nil, err } - if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowDelete); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, &pb.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowDelete); err != nil { return nil, err } @@ -266,7 +266,7 @@ func (s *WorkflowService) View(ctx context.Context, req *pb.WorkflowServiceViewR } // try to load project and apply RBAC if needed - if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.ProjectName, authz.PolicyWorkflowRead); err != nil { + if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, &pb.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowRead); err != nil { return nil, err } diff --git a/app/controlplane/pkg/auditor/events/project.go b/app/controlplane/pkg/auditor/events/project.go new file mode 100644 index 000000000..12fa26691 --- /dev/null +++ b/app/controlplane/pkg/auditor/events/project.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 events + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor" + + "github.com/google/uuid" +) + +var ( + _ auditor.LogEntry = (*ProjectMemberAdded)(nil) + _ auditor.LogEntry = (*ProjectMemberRemoved)(nil) +) + +const ( + ProjectType auditor.TargetType = "Project" + ProjectMemberAddedActionType string = "ProjectMemberAdded" + ProjectMemberRemovedActionType string = "ProjectMemberRemoved" +) + +// ProjectBase is the base struct for project events +type ProjectBase struct { + ProjectID *uuid.UUID `json:"project_id,omitempty"` + ProjectName string `json:"project_name,omitempty"` +} + +func (p *ProjectBase) RequiresActor() bool { + return true +} + +func (p *ProjectBase) TargetType() auditor.TargetType { + return ProjectType +} + +func (p *ProjectBase) TargetID() *uuid.UUID { + return p.ProjectID +} + +func (p *ProjectBase) ActionInfo() (json.RawMessage, error) { + if p.ProjectID == nil || p.ProjectName == "" { + return nil, errors.New("project id and name are required") + } + + return json.Marshal(&p) +} + +// ProjectMemberAdded represents the addition of a member to a project +type ProjectMemberAdded struct { + *ProjectBase + UserID *uuid.UUID `json:"user_id,omitempty"` + UserEmail string `json:"user_email,omitempty"` + Role string `json:"role,omitempty"` +} + +func (p *ProjectMemberAdded) ActionType() string { + return ProjectMemberAddedActionType +} + +func (p *ProjectMemberAdded) ActionInfo() (json.RawMessage, error) { + if _, err := p.ProjectBase.ActionInfo(); err != nil { + return nil, err + } + + if p.UserID == nil { + return nil, fmt.Errorf("user ID is required") + } + + return json.Marshal(&p) +} + +func (p *ProjectMemberAdded) Description() string { + // Convert the role to a prettier format + prettyRole := p.Role + if strings.HasPrefix(p.Role, "role:project:") { + prettyRole = strings.TrimPrefix(p.Role, "role:project:") + } + + roleDesc := fmt.Sprintf(" with role '%s'", prettyRole) + + return fmt.Sprintf("{{ if .ActorEmail }}{{ .ActorEmail }}{{ else }}API Token {{ .ActorID }}{{ end }} has added user %s to the project %s%s", + p.UserEmail, p.ProjectName, roleDesc) +} + +// ProjectMemberRemoved represents the removal of a member from a project +type ProjectMemberRemoved struct { + *ProjectBase + UserID *uuid.UUID `json:"user_id,omitempty"` + UserEmail string `json:"user_email,omitempty"` +} + +func (p *ProjectMemberRemoved) ActionType() string { + return ProjectMemberRemovedActionType +} + +func (p *ProjectMemberRemoved) ActionInfo() (json.RawMessage, error) { + if _, err := p.ProjectBase.ActionInfo(); err != nil { + return nil, err + } + + if p.UserID == nil { + return nil, fmt.Errorf("user ID is required") + } + + return json.Marshal(&p) +} + +func (p *ProjectMemberRemoved) Description() string { + return fmt.Sprintf("{{ if .ActorEmail }}{{ .ActorEmail }}{{ else }}API Token {{ .ActorID }}{{ end }} has removed user %s from the project %s", + p.UserEmail, p.ProjectName) +} diff --git a/app/controlplane/pkg/auditor/events/project_test.go b/app/controlplane/pkg/auditor/events/project_test.go new file mode 100644 index 000000000..7196826e9 --- /dev/null +++ b/app/controlplane/pkg/auditor/events/project_test.go @@ -0,0 +1,256 @@ +// +// 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 events_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" + + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProjectEvents(t *testing.T) { + userUUID, err := uuid.Parse("1089bb36-e27b-428b-8009-d015c8737c54") + require.NoError(t, err) + orgUUID, err := uuid.Parse("1089bb36-e27b-428b-8009-d015c8737c54") + require.NoError(t, err) + projectUUID, 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) + projectName := "test-project" + userEmail := "test@example.com" + + tests := []struct { + name string + event auditor.LogEntry + expected string + actor auditor.ActorType + actorID uuid.UUID + }{ + { + name: "ProjectMemberAdded", + event: &events.ProjectMemberAdded{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: projectName, + }, + UserID: &memberUUID, + UserEmail: userEmail, + Role: string(authz.RoleProjectViewer), + }, + expected: "testdata/projects/project_member_added.json", + actor: auditor.ActorTypeUser, + actorID: userUUID, + }, + { + name: "ProjectMemberAdded as admin", + event: &events.ProjectMemberAdded{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: projectName, + }, + UserID: &memberUUID, + UserEmail: userEmail, + Role: "role:project:admin", + }, + expected: "testdata/projects/project_member_added_as_admin.json", + actor: auditor.ActorTypeUser, + actorID: userUUID, + }, + { + name: "ProjectMemberAdded with API Token", + event: &events.ProjectMemberAdded{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: projectName, + }, + UserID: &memberUUID, + UserEmail: userEmail, + Role: string(authz.RoleProjectViewer), + }, + expected: "testdata/projects/project_member_added_with_api_token.json", + actor: auditor.ActorTypeAPIToken, + actorID: userUUID, + }, + { + name: "ProjectMemberRemoved", + event: &events.ProjectMemberRemoved{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: projectName, + }, + UserID: &memberUUID, + UserEmail: userEmail, + }, + expected: "testdata/projects/project_member_removed.json", + actor: auditor.ActorTypeUser, + actorID: userUUID, + }, + { + name: "ProjectMemberRemoved with API Token", + event: &events.ProjectMemberRemoved{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: projectName, + }, + UserID: &memberUUID, + UserEmail: userEmail, + }, + expected: "testdata/projects/project_member_removed_with_api_token.json", + actor: auditor.ActorTypeAPIToken, + actorID: userUUID, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := []auditor.GeneratorOption{ + auditor.WithOrgID(orgUUID), + } + if tt.actor == auditor.ActorTypeAPIToken { + opts = append(opts, auditor.WithActor(auditor.ActorTypeAPIToken, tt.actorID, "")) + } else { + opts = append(opts, auditor.WithActor(auditor.ActorTypeUser, tt.actorID, testEmail)) + } + + eventPayload, err := auditor.GenerateAuditEvent(tt.event, opts...) + require.NoError(t, err) + + want, err := json.MarshalIndent(eventPayload.Data, "", " ") + require.NoError(t, err) + + if updateGolden { + err := os.MkdirAll(filepath.Dir(tt.expected), 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Clean(tt.expected), want, 0600) + require.NoError(t, err) + } + + gotRaw, err := os.ReadFile(filepath.Clean(tt.expected)) + require.NoError(t, err) + + var gotPayload auditor.AuditEventPayload + err = json.Unmarshal(gotRaw, &gotPayload) + require.NoError(t, err) + got, err := json.MarshalIndent(gotPayload, "", " ") + require.NoError(t, err) + + assert.Equal(t, string(want), string(got)) + }) + } +} + +// TestProjectEventsFailed tests the behavior of project events when they are expected to fail +func TestProjectEventsFailed(t *testing.T) { + projectUUID, 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) + + tests := []struct { + name string + event auditor.LogEntry + expectedErr string + }{ + { + name: "Project member added with missing ProjectID", + event: &events.ProjectMemberAdded{ + ProjectBase: &events.ProjectBase{ + ProjectName: "test-project", + }, + UserID: &memberUUID, + UserEmail: "test@example.com", + Role: "admin", + }, + expectedErr: "project id and name are required", + }, + { + name: "Project member added with missing ProjectName", + event: &events.ProjectMemberAdded{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + }, + UserID: &memberUUID, + UserEmail: "test@example.com", + Role: "admin", + }, + expectedErr: "project id and name are required", + }, + { + name: "Project member added with missing UserID", + event: &events.ProjectMemberAdded{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: "test-project", + }, + UserEmail: "test@example.com", + Role: "admin", + }, + expectedErr: "user ID is required", + }, + { + name: "Project member removed with missing ProjectID", + event: &events.ProjectMemberRemoved{ + ProjectBase: &events.ProjectBase{ + ProjectName: "test-project", + }, + UserID: &memberUUID, + UserEmail: "test@example.com", + }, + expectedErr: "project id and name are required", + }, + { + name: "Project member removed with missing ProjectName", + event: &events.ProjectMemberRemoved{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + }, + UserID: &memberUUID, + UserEmail: "test@example.com", + }, + expectedErr: "project id and name are required", + }, + { + name: "Project member removed with missing UserID", + event: &events.ProjectMemberRemoved{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: "test-project", + }, + UserEmail: "test@example.com", + }, + expectedErr: "user ID is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := tt.event.ActionInfo() + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + }) + } +} diff --git a/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added.json b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added.json new file mode 100644 index 000000000..5fe9d963e --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added.json @@ -0,0 +1,18 @@ +{ + "ActionType": "ProjectMemberAdded", + "TargetType": "Project", + "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 project test-project with role 'viewer'", + "Info": { + "project_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "project_name": "test-project", + "user_id": "4089bb36-e27b-428b-8009-d015c8737c57", + "user_email": "test@example.com", + "role": "role:project:viewer" + }, + "Digest": "sha256:5fc10777868254785ad8fa2e3af9ce8227add2cdc753f999c5f916c8bcb8aeab" +} \ No newline at end of file diff --git a/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added_as_admin.json b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added_as_admin.json new file mode 100644 index 000000000..548ceeb6d --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added_as_admin.json @@ -0,0 +1,18 @@ +{ + "ActionType": "ProjectMemberAdded", + "TargetType": "Project", + "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 project test-project with role 'admin'", + "Info": { + "project_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "project_name": "test-project", + "user_id": "4089bb36-e27b-428b-8009-d015c8737c57", + "user_email": "test@example.com", + "role": "role:project:admin" + }, + "Digest": "sha256:2416ffd8c62ba3727a704e0aec3444e64623625909eba745249b73425bfbd70a" +} \ No newline at end of file diff --git a/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added_with_api_token.json b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added_with_api_token.json new file mode 100644 index 000000000..289671d33 --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_added_with_api_token.json @@ -0,0 +1,18 @@ +{ + "ActionType": "ProjectMemberAdded", + "TargetType": "Project", + "TargetID": "3089bb36-e27b-428b-8009-d015c8737c56", + "ActorType": "API_TOKEN", + "ActorID": "1089bb36-e27b-428b-8009-d015c8737c54", + "ActorEmail": "", + "OrgID": "1089bb36-e27b-428b-8009-d015c8737c54", + "Description": "API Token 1089bb36-e27b-428b-8009-d015c8737c54 has added user test@example.com to the project test-project with role 'viewer'", + "Info": { + "project_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "project_name": "test-project", + "user_id": "4089bb36-e27b-428b-8009-d015c8737c57", + "user_email": "test@example.com", + "role": "role:project:viewer" + }, + "Digest": "sha256:5fc10777868254785ad8fa2e3af9ce8227add2cdc753f999c5f916c8bcb8aeab" +} \ No newline at end of file diff --git a/app/controlplane/pkg/auditor/events/testdata/projects/project_member_removed.json b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_removed.json new file mode 100644 index 000000000..8af22182f --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_removed.json @@ -0,0 +1,17 @@ +{ + "ActionType": "ProjectMemberRemoved", + "TargetType": "Project", + "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 project test-project", + "Info": { + "project_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "project_name": "test-project", + "user_id": "4089bb36-e27b-428b-8009-d015c8737c57", + "user_email": "test@example.com" + }, + "Digest": "sha256:a558a075d42485114110e15e9f0bfd5effd80b7ba03b691501f709ee032f027a" +} \ No newline at end of file diff --git a/app/controlplane/pkg/auditor/events/testdata/projects/project_member_removed_with_api_token.json b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_removed_with_api_token.json new file mode 100644 index 000000000..5b6b6a61a --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/projects/project_member_removed_with_api_token.json @@ -0,0 +1,17 @@ +{ + "ActionType": "ProjectMemberRemoved", + "TargetType": "Project", + "TargetID": "3089bb36-e27b-428b-8009-d015c8737c56", + "ActorType": "API_TOKEN", + "ActorID": "1089bb36-e27b-428b-8009-d015c8737c54", + "ActorEmail": "", + "OrgID": "1089bb36-e27b-428b-8009-d015c8737c54", + "Description": "API Token 1089bb36-e27b-428b-8009-d015c8737c54 has removed user test@example.com from the project test-project", + "Info": { + "project_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "project_name": "test-project", + "user_id": "4089bb36-e27b-428b-8009-d015c8737c57", + "user_email": "test@example.com" + }, + "Digest": "sha256:a558a075d42485114110e15e9f0bfd5effd80b7ba03b691501f709ee032f027a" +} \ No newline at end of file diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index c719e6aad..a2d433811 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -138,11 +138,8 @@ var ( PolicyOrganizationRead = &Policy{Organization, ActionRead} PolicyOrganizationListMemberships = &Policy{Organization, ActionRead} // Groups - PolicyGroupCreate = &Policy{ResourceGroup, ActionCreate} - PolicyGroupUpdate = &Policy{ResourceGroup, ActionUpdate} - PolicyGroupDelete = &Policy{ResourceGroup, ActionDelete} - PolicyGroupList = &Policy{ResourceGroup, ActionList} - PolicyGroupRead = &Policy{ResourceGroup, ActionRead} + PolicyGroupList = &Policy{ResourceGroup, ActionList} + PolicyGroupRead = &Policy{ResourceGroup, ActionRead} // Group Memberships PolicyGroupListMemberships = &Policy{ResourceGroupMembership, ActionList} PolicyGroupAddMemberships = &Policy{ResourceGroupMembership, ActionCreate} @@ -151,6 +148,10 @@ var ( PolicyProjectAPITokenList = &Policy{ResourceProjectAPIToken, ActionList} PolicyProjectAPITokenCreate = &Policy{ResourceProjectAPIToken, ActionCreate} PolicyProjectAPITokenRevoke = &Policy{ResourceProjectAPIToken, ActionDelete} + // Project Memberships + PolicyProjectListMemberships = &Policy{ResourceGroupMembership, ActionList} + PolicyProjectAddMemberships = &Policy{ResourceGroupMembership, ActionCreate} + PolicyProjectRemoveMemberships = &Policy{ResourceGroupMembership, ActionDelete} ) // RolesMap The default list of policies for each role @@ -248,6 +249,11 @@ var RolesMap = map[Role][]*Policy{ PolicyProjectAPITokenList, PolicyProjectAPITokenCreate, PolicyProjectAPITokenRevoke, + + // Project Memberships + PolicyProjectListMemberships, + PolicyProjectAddMemberships, + PolicyProjectRemoveMemberships, }, // RoleProjectViewer: has read-only permissions on a project RoleProjectViewer: { @@ -280,6 +286,11 @@ var RolesMap = map[Role][]*Policy{ PolicyProjectAPITokenList, PolicyProjectAPITokenCreate, PolicyProjectAPITokenRevoke, + + // Project Memberships + PolicyProjectListMemberships, + PolicyProjectAddMemberships, + PolicyProjectRemoveMemberships, }, // RoleGroupMaintainer: represents a group maintainer role. RoleGroupMaintainer: { @@ -360,6 +371,10 @@ var ServerOperationsMap = map[string][]*Policy{ "/controlplane.v1.ProjectService/APITokenCreate": {PolicyProjectAPITokenCreate}, "/controlplane.v1.ProjectService/APITokenList": {PolicyProjectAPITokenList}, "/controlplane.v1.ProjectService/APITokenRevoke": {PolicyProjectAPITokenRevoke}, + // Project Memberships + "/controlplane.v1.ProjectService/ListMembers": {PolicyProjectListMemberships}, + "/controlplane.v1.ProjectService/AddMember": {PolicyProjectAddMemberships}, + "/controlplane.v1.ProjectService/RemoveMember": {PolicyProjectRemoveMemberships}, } // Implements https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues diff --git a/app/controlplane/pkg/biz/biz.go b/app/controlplane/pkg/biz/biz.go index 32501690b..355478de5 100644 --- a/app/controlplane/pkg/biz/biz.go +++ b/app/controlplane/pkg/biz/biz.go @@ -118,14 +118,6 @@ func ValidateVersion(version string) error { return nil } -// EntityRef is a reference to an entity -type EntityRef struct { - // ID is the unique identifier of the entity - ID string - // Name is the name of the entity - Name string -} - func ToPtr[T any](v T) *T { return &v } diff --git a/app/controlplane/pkg/biz/project.go b/app/controlplane/pkg/biz/project.go index f88c83831..bfa48634d 100644 --- a/app/controlplane/pkg/biz/project.go +++ b/app/controlplane/pkg/biz/project.go @@ -21,7 +21,9 @@ import ( "slices" "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/chainloop-dev/chainloop/pkg/servicelogger" "github.com/go-kratos/kratos/v2/log" @@ -34,13 +36,24 @@ type ProjectsRepo interface { FindProjectByOrgIDAndID(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID) (*Project, error) Create(ctx context.Context, orgID uuid.UUID, name string) (*Project, error) ListProjectsByOrgID(ctx context.Context, orgID uuid.UUID) ([]*Project, error) + // ListMembers retrieves a list of members in a project, optionally filtered by admin status. + ListMembers(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID, paginationOpts *pagination.OffsetPaginationOpts) ([]*ProjectMembership, int, error) + // AddMemberToProject adds a user to a project, optionally specifying if they are an admin. + AddMemberToProject(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID, userID uuid.UUID, role authz.Role) (*ProjectMembership, error) + // RemoveMemberFromProject removes a user from a project. + RemoveMemberFromProject(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID, userID uuid.UUID) error + // FindProjectMembershipByProjectAndID finds a project membership by project ID and user ID. + FindProjectMembershipByProjectAndID(ctx context.Context, projectID uuid.UUID, userID uuid.UUID) (*ProjectMembership, error) } // ProjectUseCase is a use case for projects type ProjectUseCase struct { logger *log.Helper + // Use Cases + auditorUC *AuditorUseCase // Repositories - projectsRepository ProjectsRepo + projectsRepository ProjectsRepo + membershipRepository MembershipRepo } // Project is a project in the organization @@ -57,15 +70,51 @@ type Project struct { UpdatedAt *time.Time } -func NewProjectsUseCase(logger log.Logger, projectsRepository ProjectsRepo) *ProjectUseCase { +// ProjectMembership represents a membership of a user in a project. +type ProjectMembership struct { + // User is the user who is a member of the project. + User *User + // Role represents the role of the user in the project (admin or viewer). + Role authz.Role + // CreatedAt is the timestamp when the user was added to the project. + CreatedAt *time.Time + // UpdatedAt is the timestamp when the membership was last updated. + UpdatedAt *time.Time +} + +// AddMemberToProjectOpts defines options for adding a member to a project. +type AddMemberToProjectOpts struct { + // ProjectReference is the reference to the project. + ProjectReference *IdentityReference + // UserEmail is the email of the user to add to the project. + UserEmail string + // RequesterID is the ID of the user who is requesting to add the member. + RequesterID uuid.UUID + // Role represents the role to assign to the user in the project. + Role authz.Role +} + +// RemoveMemberFromProjectOpts defines options for removing a member from a project. +type RemoveMemberFromProjectOpts struct { + // ProjectReference is the reference to the project. + ProjectReference *IdentityReference + // UserEmail is the email of the user to remove from the project. + UserEmail string + // RequesterID is the ID of the user who is requesting to remove the member. + RequesterID uuid.UUID +} + +func NewProjectsUseCase(logger log.Logger, projectsRepository ProjectsRepo, membershipRepository MembershipRepo, auditorUC *AuditorUseCase) *ProjectUseCase { return &ProjectUseCase{ - logger: servicelogger.ScopedHelper(logger, "biz/project"), - projectsRepository: projectsRepository, + logger: servicelogger.ScopedHelper(logger, "biz/project"), + projectsRepository: projectsRepository, + membershipRepository: membershipRepository, + auditorUC: auditorUC, } } // FindProjectByReference finds a project by reference, which can be either a project name or a project ID. -func (uc *ProjectUseCase) FindProjectByReference(ctx context.Context, orgID string, reference *EntityRef) (*Project, error) { +func (uc *ProjectUseCase) FindProjectByReference(ctx context.Context, orgID string, reference *IdentityReference) (*Project, error) { if reference == nil || orgID == "" { return nil, NewErrValidationStr("orgID or project reference are empty") } @@ -75,14 +124,10 @@ func (uc *ProjectUseCase) FindProjectByReference(ctx context.Context, orgID stri } switch { - case reference.Name != "": - return uc.projectsRepository.FindProjectByOrgIDAndName(ctx, orgUUID, reference.Name) - case reference.ID != "": - projectUUID, err := uuid.Parse(reference.ID) - if err != nil { - return nil, NewErrInvalidUUID(err) - } - return uc.projectsRepository.FindProjectByOrgIDAndID(ctx, orgUUID, projectUUID) + case reference.Name != nil && *reference.Name != "": + return uc.projectsRepository.FindProjectByOrgIDAndName(ctx, orgUUID, *reference.Name) + case reference.ID != nil && *reference.ID != uuid.Nil: + return uc.projectsRepository.FindProjectByOrgIDAndID(ctx, orgUUID, *reference.ID) default: return nil, NewErrValidationStr("project reference is empty") } @@ -101,6 +146,246 @@ func (uc *ProjectUseCase) Create(ctx context.Context, orgID, name string) (*Proj return uc.projectsRepository.Create(ctx, orgUUID, name) } +// ListMembers lists the members of a project with pagination. +func (uc *ProjectUseCase) ListMembers(ctx context.Context, orgID uuid.UUID, projectRef *IdentityReference, paginationOpts *pagination.OffsetPaginationOpts) ([]*ProjectMembership, int, error) { + if projectRef == nil { + return nil, 0, NewErrValidationStr("project reference cannot be nil") + } + + if orgID == uuid.Nil { + return nil, 0, NewErrValidationStr("organization ID cannot be empty") + } + + // Validate and resolve the project reference to a project ID + resolvedProjectID, err := uc.ValidateProjectIdentifier(ctx, orgID, projectRef) + if err != nil { + return nil, 0, err + } + + // Check the project exists + existingProject, err := uc.projectsRepository.FindProjectByOrgIDAndID(ctx, orgID, resolvedProjectID) + if err != nil { + return nil, 0, fmt.Errorf("failed to find project: %w", err) + } + + if existingProject == nil { + return nil, 0, NewErrNotFound("project") + } + + // Use default pagination options if none provided + pgOpts := pagination.NewDefaultOffsetPaginationOpts() + if paginationOpts != nil { + pgOpts = paginationOpts + } + + return uc.projectsRepository.ListMembers(ctx, orgID, resolvedProjectID, pgOpts) +} + +// AddMemberToProject adds a user to a project. +func (uc *ProjectUseCase) AddMemberToProject(ctx context.Context, orgID uuid.UUID, opts *AddMemberToProjectOpts) (*ProjectMembership, 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") + } + + // Validate the role + if opts.Role != authz.RoleProjectAdmin && opts.Role != authz.RoleProjectViewer { + return nil, NewErrValidationStr("role must be either 'admin' or 'viewer'") + } + + // Validate and resolve the project reference to a project ID + resolvedProjectID, err := uc.ValidateProjectIdentifier(ctx, orgID, opts.ProjectReference) + if err != nil { + return nil, err + } + + // Check the project exists + existingProject, err := uc.projectsRepository.FindProjectByOrgIDAndID(ctx, orgID, resolvedProjectID) + if err != nil { + return nil, fmt.Errorf("failed to find project: %w", err) + } + + if existingProject == nil { + return nil, NewErrNotFound("project") + } + + // Verify the requester has permissions to add members to the project + if err := uc.verifyRequesterHasPermissions(ctx, orgID, resolvedProjectID, opts.RequesterID); err != nil { + return nil, fmt.Errorf("requester does not have permission to add members to this project: %w", err) + } + + // Find the user by email in the organization + userMembership, err := uc.membershipRepository.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 project + existingMembership, err := uc.projectsRepository.FindProjectMembershipByProjectAndID(ctx, resolvedProjectID, 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 project") + } + + // Add the user to the project + membership, err := uc.projectsRepository.AddMemberToProject(ctx, orgID, resolvedProjectID, userUUID, opts.Role) + if err != nil { + return nil, fmt.Errorf("failed to add member to project: %w", err) + } + + // Dispatch event to the audit log for project membership addition + uc.auditorUC.Dispatch(ctx, &events.ProjectMemberAdded{ + ProjectBase: &events.ProjectBase{ + ProjectID: &resolvedProjectID, + ProjectName: existingProject.Name, + }, + UserID: &userUUID, + UserEmail: opts.UserEmail, + Role: string(opts.Role), + }, &orgID) + + return membership, nil +} + +// RemoveMemberFromProject removes a user from a project. +func (uc *ProjectUseCase) RemoveMemberFromProject(ctx context.Context, orgID uuid.UUID, opts *RemoveMemberFromProjectOpts) 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") + } + + // Validate and resolve the project reference to a project ID + resolvedProjectID, err := uc.ValidateProjectIdentifier(ctx, orgID, opts.ProjectReference) + if err != nil { + return err + } + + // Check the project exists + existingProject, err := uc.projectsRepository.FindProjectByOrgIDAndID(ctx, orgID, resolvedProjectID) + if err != nil { + return fmt.Errorf("failed to find project: %w", err) + } + + if existingProject == nil { + return NewErrNotFound("project") + } + + // Verify the requester has permissions to remove members from the project + if err := uc.verifyRequesterHasPermissions(ctx, orgID, resolvedProjectID, opts.RequesterID); err != nil { + return fmt.Errorf("requester does not have permission to remove members from this project: %w", err) + } + + // Find the user by email in the organization + userMembership, err := uc.membershipRepository.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") + } + + userUUID := uuid.MustParse(userMembership.User.ID) + + // Check if the user is a member of the project + existingMembership, err := uc.projectsRepository.FindProjectMembershipByProjectAndID(ctx, resolvedProjectID, 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 project") + } + + // Remove the user from the project + if err := uc.projectsRepository.RemoveMemberFromProject(ctx, orgID, resolvedProjectID, userUUID); err != nil { + return fmt.Errorf("failed to remove member from project: %w", err) + } + + // Dispatch event to the audit log for project membership removal + uc.auditorUC.Dispatch(ctx, &events.ProjectMemberRemoved{ + ProjectBase: &events.ProjectBase{ + ProjectID: &resolvedProjectID, + ProjectName: existingProject.Name, + }, + UserID: &userUUID, + UserEmail: opts.UserEmail, + }, &orgID) + + return nil +} + +// ValidateProjectIdentifier validates and resolves the project reference to a project ID. +func (uc *ProjectUseCase) ValidateProjectIdentifier(ctx context.Context, orgID uuid.UUID, projectRef *IdentityReference) (uuid.UUID, error) { + if projectRef == nil { + return uuid.Nil, NewErrValidationStr("project reference cannot be nil") + } + + if projectRef.ID == nil && projectRef.Name == nil { + return uuid.Nil, NewErrValidationStr("either project ID or project name must be provided") + } + + if projectRef.ID != nil { + return *projectRef.ID, nil + } + + // If project ID is not provided, try to find the project by name + project, err := uc.projectsRepository.FindProjectByOrgIDAndName(ctx, orgID, *projectRef.Name) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to find project by name: %w", err) + } + if project == nil { + return uuid.Nil, NewErrNotFound("project") + } + + return project.ID, nil +} + +// verifyRequesterHasPermissions checks if the requester has the required permissions to perform an action on a project. +func (uc *ProjectUseCase) verifyRequesterHasPermissions(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID, requesterID uuid.UUID) error { + // Check if the requester is part of the organization + requesterMembership, err := uc.membershipRepository.FindByOrgAndUser(ctx, orgID, 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 project + if !isAdminOrOwner { + // Check if the requester is a maintainer of this project + requesterGroupMembership, err := uc.membershipRepository.FindByUserAndResourceID(ctx, requesterID, projectID) + if err != nil && !IsNotFound(err) { + return fmt.Errorf("failed to check requester's project membership: %w", err) + } + + // If not a maintainer of this project, deny access + if requesterGroupMembership == nil || requesterGroupMembership.Role != authz.RoleProjectAdmin { + return NewErrValidationStr("requester does not have permission to operate on this project") + } + } + + // If the requester is an admin/owner or a maintainer of this project, allow the action + return nil +} + // getProjectsWithMembership returns the list of project IDs in the org for which the user has a membership func getProjectsWithMembership(ctx context.Context, projectsRepo ProjectsRepo, orgID uuid.UUID, memberships []*Membership) ([]uuid.UUID, error) { ids := make([]uuid.UUID, 0) diff --git a/app/controlplane/pkg/biz/project_integration_test.go b/app/controlplane/pkg/biz/project_integration_test.go new file mode 100644 index 000000000..96b425bcf --- /dev/null +++ b/app/controlplane/pkg/biz/project_integration_test.go @@ -0,0 +1,1002 @@ +// +// 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 biz_test + +import ( + "context" + "fmt" + "testing" + + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/testhelpers" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// Run the tests +func TestProjectUseCase(t *testing.T) { + suite.Run(t, new(projectMembersIntegrationTestSuite)) + suite.Run(t, new(projectAdminPermissionsTestSuite)) + suite.Run(t, new(projectPermissionsTestSuite)) +} + +// Utility struct for project members tests +type projectMembersIntegrationTestSuite struct { + testhelpers.UseCasesEachTestSuite + org *biz.Organization + user *biz.User + project *biz.Project +} + +func (s *projectMembersIntegrationTestSuite) SetupTest() { + var err error + assert := assert.New(s.T()) + s.TestingUseCases = testhelpers.NewTestingUseCases(s.T()) + + ctx := context.Background() + s.org, err = s.Organization.CreateWithRandomName(ctx) + assert.NoError(err) + + // Create a user for membership tests - this user will be an org admin by default + s.user, err = s.User.UpsertByEmail(ctx, fmt.Sprintf("test-user-%s@example.com", uuid.New().String()), nil) + assert.NoError(err) + + // Add user to organization as an admin + _, err = s.Membership.Create(ctx, s.org.ID, s.user.ID, biz.WithMembershipRole(authz.RoleAdmin)) + assert.NoError(err) + + // Create a project for membership tests + s.project, err = s.Project.Create(ctx, s.org.ID, "test-members-project") + assert.NoError(err) +} + +// TearDownTest cleans up resources after each test has completed +func (s *projectMembersIntegrationTestSuite) TearDownTest() { + ctx := context.Background() + // Clean up database tables to avoid test interference + _, _ = s.Data.DB.Membership.Delete().Exec(ctx) + _, _ = s.Data.DB.Project.Delete().Exec(ctx) +} + +// Test listing project members +func (s *projectMembersIntegrationTestSuite) TestListMembers() { + ctx := context.Background() + + // Create additional users + user2, err := s.User.UpsertByEmail(ctx, "user2@example.com", nil) + require.NoError(s.T(), err) + + user3, err := s.User.UpsertByEmail(ctx, "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) + + // Add users to the project + projectID := s.project.ID + projectRef := &biz.IdentityReference{ + ID: &projectID, + } + + opts1 := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectViewer, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts1) + require.NoError(s.T(), err) + + opts2 := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "user3@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectAdmin, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts2) + require.NoError(s.T(), err) + + s.Run("list all members", func() { + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(2, len(members)) + s.Equal(2, count) + + // Verify we have both a regular member and an admin + adminCount := 0 + for _, member := range members { + if member.Role == authz.RoleProjectAdmin { + adminCount++ + } + } + s.Equal(1, adminCount, "Should have exactly one admin member") + }) + + s.Run("list members with pagination", func() { + paginationOpts, err := pagination.NewOffsetPaginationOpts(1, 1) + require.NoError(s.T(), err) + + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, paginationOpts) + s.NoError(err) + s.Equal(1, len(members)) + s.Equal(2, count) // Total count should be 2 + + // Get the next page + paginationOpts, err = pagination.NewOffsetPaginationOpts(2, 1) + require.NoError(s.T(), err) + + secondPageMembers, secondCount, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, paginationOpts) + s.NoError(err) + s.Equal(1, len(secondPageMembers)) + s.Equal(2, secondCount) + + // Verify the two pages contain different members + s.NotEqual(members[0].User.ID, secondPageMembers[0].User.ID) + }) + + s.Run("list members with project name", func() { + projectName := s.project.Name + nameRef := &biz.IdentityReference{ + Name: &projectName, + } + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), nameRef, nil) + s.NoError(err) + s.Equal(2, len(members)) + s.Equal(2, count) + }) + + s.Run("list members with non-existent project", func() { + nonExistentID := uuid.New() + invalidRef := &biz.IdentityReference{ + ID: &nonExistentID, + } + _, _, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), invalidRef, nil) + s.Error(err) + s.True(biz.IsNotFound(err)) + }) + + s.Run("list members with wrong organization", func() { + org2, err := s.Organization.CreateWithRandomName(ctx) + require.NoError(s.T(), err) + + _, _, err = s.Project.ListMembers(ctx, uuid.MustParse(org2.ID), projectRef, nil) + s.Error(err) + s.True(biz.IsNotFound(err)) + }) +} + +// Test adding members to projects +func (s *projectMembersIntegrationTestSuite) TestAddMemberToProject() { + 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) + + projectID := s.project.ID + projectRef := &biz.IdentityReference{ + ID: &projectID, + } + + s.Run("add member using project ID", func() { + // Add user2 as a viewer + opts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "add-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectViewer, + } + + membership, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts) + s.NoError(err) + s.NotNil(membership) + s.Equal(user2.ID, membership.User.ID) + s.Equal(authz.RoleProjectViewer, membership.Role) + + // Verify the member was added by listing members + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(1, len(members)) + s.Equal(1, count) + }) + + s.Run("add member using project name", func() { + // Add user3 as an admin + projectName := s.project.Name + nameRef := &biz.IdentityReference{ + Name: &projectName, + } + opts := &biz.AddMemberToProjectOpts{ + ProjectReference: nameRef, + UserEmail: "add-user3@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectAdmin, + } + + membership, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts) + s.NoError(err) + s.NotNil(membership) + s.Equal(user3.ID, membership.User.ID) + s.Equal(authz.RoleProjectAdmin, membership.Role) + + // Verify the member was added by listing members + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(2, len(members)) + s.Equal(2, count) + }) + + s.Run("add member to project 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 project in the wrong organization + opts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "add-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectViewer, + } + + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(org2.ID), opts) + s.Error(err) + s.True(biz.IsNotFound(err)) + }) + + s.Run("add member to non-existent project", func() { + nonExistentProjectID := uuid.New() + invalidRef := &biz.IdentityReference{ + ID: &nonExistentProjectID, + } + opts := &biz.AddMemberToProjectOpts{ + ProjectReference: invalidRef, + UserEmail: "add-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectViewer, + } + + _, err := s.Project.AddMemberToProject(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.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "not-in-org@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectViewer, + } + + _, err = s.Project.AddMemberToProject(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 project", func() { + // Try to add user2 again (who we added in the first test) + opts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "add-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectAdmin, + } + + _, err := s.Project.AddMemberToProject(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.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(2, count) // still the original 2 members + }) +} + +// Test removing members from projects +func (s *projectMembersIntegrationTestSuite) TestRemoveMemberFromProject() { + 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) + + projectID := s.project.ID + projectRef := &biz.IdentityReference{ + ID: &projectID, + } + + // Add users to the project + opts1 := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "remove-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectViewer, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts1) + require.NoError(s.T(), err) + + opts2 := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "remove-user3@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectAdmin, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts2) + require.NoError(s.T(), err) + + opts3 := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "remove-user4@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectViewer, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts3) + require.NoError(s.T(), err) + + // Verify initial member count + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(3, len(members)) + s.Equal(3, count) + + s.Run("remove a regular member from project", func() { + // Remove user2 (regular member) + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: "remove-user2@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err := s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.NoError(err) + + // Verify member was removed + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(2, len(members)) + s.Equal(2, count) + + // Verify the removed user is not in the list + for _, member := range members { + s.NotEqual(user2.ID, member.User.ID) + } + }) + + s.Run("remove an admin member from project", func() { + // Remove user3 (admin) + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: "remove-user3@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err := s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.NoError(err) + + // Verify member was removed + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(1, len(members)) + s.Equal(1, count) + + // Check remaining members - user3 should not be present + for _, member := range members { + s.NotEqual(user3.ID, member.User.ID) + } + }) + + s.Run("try to remove non-existent member", func() { + // Create a user who's not in the project + 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 project + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: "non-member@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err = s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.True(biz.IsErrValidation(err)) + + // Member count should remain unchanged + _, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(1, count) + }) + + s.Run("remove member from wrong organization", func() { + // Create a new organization + org2, err := s.Organization.CreateWithRandomName(ctx) + require.NoError(s.T(), err) + + // Try to remove user4 using the wrong org ID + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: "remove-user4@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err = s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(org2.ID), removeOpts) + s.Error(err) + s.True(biz.IsNotFound(err)) + + // Member count should remain unchanged + _, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(1, count) + }) + + s.Run("remove member from non-existent project", func() { + nonExistentProjectID := uuid.New() + invalidRef := &biz.IdentityReference{ + ID: &nonExistentProjectID, + } + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: invalidRef, + UserEmail: "remove-user4@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err = s.Project.RemoveMemberFromProject(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) + + // Try to remove a member with an external user as requester + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: "remove-user4@example.com", + RequesterID: uuid.MustParse(externalUser.ID), + } + + err = s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.Contains(err.Error(), "requester is not a member of the organization") + }) + + s.Run("non-existent user email", func() { + // Try to remove a non-existent user + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: "non-existent-user@example.com", + RequesterID: uuid.MustParse(s.user.ID), + } + + err = s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.Contains(err.Error(), "not a member of the organization") + }) +} + +// projectAdminPermissionsTestSuite tests the permissions of project admins +type projectAdminPermissionsTestSuite struct { + testhelpers.UseCasesEachTestSuite + org *biz.Organization + user *biz.User + project *biz.Project +} + +func (s *projectAdminPermissionsTestSuite) SetupTest() { + var err error + assert := assert.New(s.T()) + s.TestingUseCases = testhelpers.NewTestingUseCases(s.T()) + + ctx := context.Background() + s.org, err = s.Organization.CreateWithRandomName(ctx) + assert.NoError(err) + + // Create a user for admin tests - this user will be an org admin by default + s.user, err = s.User.UpsertByEmail(ctx, fmt.Sprintf("admin-user-%s@example.com", uuid.New().String()), nil) + assert.NoError(err) + + // Add user to organization as an admin + _, err = s.Membership.Create(ctx, s.org.ID, s.user.ID, biz.WithMembershipRole(authz.RoleAdmin)) + assert.NoError(err) + + // Create a project for admin tests + s.project, err = s.Project.Create(ctx, s.org.ID, "test-admin-project") + assert.NoError(err) +} + +// TearDownTest cleans up resources after each test has completed +func (s *projectAdminPermissionsTestSuite) TearDownTest() { + ctx := context.Background() + // Clean up database tables to avoid test interference + _, _ = s.Data.DB.Membership.Delete().Exec(ctx) + _, _ = s.Data.DB.Project.Delete().Exec(ctx) +} +func (s *projectAdminPermissionsTestSuite) TestAdminPermissions() { + ctx := context.Background() + + // Create a regular user + user2, err := s.User.UpsertByEmail(ctx, "regular-user@example.com", nil) + require.NoError(s.T(), err) + + // Add the user to the organization + _, err = s.Membership.Create(ctx, s.org.ID, user2.ID) + require.NoError(s.T(), err) + + // Grant project admin role to the user + projectID := s.project.ID + projectRef := &biz.IdentityReference{ + ID: &projectID, + } + + opts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "regular-user@example.com", + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectAdmin, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts) + require.NoError(s.T(), err) + + s.Run("admin can add member to project", func() { + // Create a new user and add them to the organization first + newUserEmail := "new-member@example.com" + newUser, err := s.User.UpsertByEmail(ctx, newUserEmail, nil) + require.NoError(s.T(), err) + + // Add the new user to the organization + _, err = s.Membership.Create(ctx, s.org.ID, newUser.ID) + require.NoError(s.T(), err) + + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: newUserEmail, + RequesterID: uuid.MustParse(user2.ID), + Role: authz.RoleProjectViewer, + } + + membership, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + s.NoError(err) + s.NotNil(membership) + s.Equal(newUser.ID, membership.User.ID) + s.Equal(authz.RoleProjectViewer, membership.Role) + + // Verify the member was added + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(2, len(members)) + s.Equal(2, count) + }) + + s.Run("admin can remove member from project", func() { + // Admin user removes a member from the project + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: "regular-user@example.com", + RequesterID: uuid.MustParse(user2.ID), + } + + err := s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.NoError(err) + + // Verify the member was removed + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(1, len(members)) + s.Equal(1, count) + }) + + s.Run("admin can add themselves as a member", func() { + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: s.user.Email, + RequesterID: uuid.MustParse(s.user.ID), + Role: authz.RoleProjectViewer, + } + + _, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + s.NoError(err) + }) + + s.Run("admin can remove themselves from the project", func() { + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: s.user.Email, + RequesterID: uuid.MustParse(s.user.ID), + } + + err := s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.NoError(err) + }) +} + +type projectPermissionsTestSuite struct { + testhelpers.UseCasesEachTestSuite + org *biz.Organization + adminUser *biz.User + projectAdminUser *biz.User + regularUser *biz.User + project *biz.Project +} + +func (s *projectPermissionsTestSuite) SetupTest() { + var err error + assert := assert.New(s.T()) + s.TestingUseCases = testhelpers.NewTestingUseCases(s.T()) + + ctx := context.Background() + s.org, err = s.Organization.CreateWithRandomName(ctx) + assert.NoError(err) + + // Create an org admin user + s.adminUser, err = s.User.UpsertByEmail(ctx, fmt.Sprintf("admin-user-%s@example.com", uuid.New().String()), nil) + assert.NoError(err) + + // Add admin user to organization as an admin + _, err = s.Membership.Create(ctx, s.org.ID, s.adminUser.ID, biz.WithMembershipRole(authz.RoleAdmin)) + assert.NoError(err) + + // Create a project admin user + s.projectAdminUser, err = s.User.UpsertByEmail(ctx, fmt.Sprintf("project-admin-%s@example.com", uuid.New().String()), nil) + assert.NoError(err) + + // Add project admin user to organization as a regular member + _, err = s.Membership.Create(ctx, s.org.ID, s.projectAdminUser.ID) + assert.NoError(err) + + // Create a regular user + s.regularUser, err = s.User.UpsertByEmail(ctx, fmt.Sprintf("regular-user-%s@example.com", uuid.New().String()), nil) + assert.NoError(err) + + // Add regular user to organization as a regular member + _, err = s.Membership.Create(ctx, s.org.ID, s.regularUser.ID) + assert.NoError(err) + + // Create a project for tests + s.project, err = s.Project.Create(ctx, s.org.ID, "test-permissions-project") + assert.NoError(err) + + // Add project admin user to the project as admin + projectID := s.project.ID + projectRef := &biz.IdentityReference{ + ID: &projectID, + } + + opts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: s.projectAdminUser.Email, + RequesterID: uuid.MustParse(s.adminUser.ID), + Role: authz.RoleProjectAdmin, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts) + assert.NoError(err) + + // Add regular user to the project as regular member + opts = &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: s.regularUser.Email, + RequesterID: uuid.MustParse(s.adminUser.ID), + Role: authz.RoleProjectViewer, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), opts) + assert.NoError(err) +} + +// TearDownTest cleans up resources after each test has completed +func (s *projectPermissionsTestSuite) TearDownTest() { + ctx := context.Background() + // Clean up database tables to avoid test interference + _, _ = s.Data.DB.Membership.Delete().Exec(ctx) + _, _ = s.Data.DB.Project.Delete().Exec(ctx) + _, _ = s.Data.DB.Membership.Delete().Exec(ctx) +} + +// TestRegularUserPermissions verifies that regular users can't modify project memberships +func (s *projectPermissionsTestSuite) TestRegularUserPermissions() { + ctx := context.Background() + projectID := s.project.ID + projectRef := &biz.IdentityReference{ + ID: &projectID, + } + + // Create a new user to try adding to the project + newUser, err := s.User.UpsertByEmail(ctx, "new-user@example.com", nil) + require.NoError(s.T(), err) + + // Add the user to the organization + _, err = s.Membership.Create(ctx, s.org.ID, newUser.ID) + require.NoError(s.T(), err) + + s.Run("regular user cannot add member to project", func() { + // Regular user tries to add a new member + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "new-user@example.com", + RequesterID: uuid.MustParse(s.regularUser.ID), + Role: authz.RoleProjectViewer, + } + + _, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + s.Error(err) + s.Contains(err.Error(), "does not have permission") + }) + + s.Run("regular user cannot remove member from project", func() { + // Regular user tries to remove a member + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: s.projectAdminUser.Email, + RequesterID: uuid.MustParse(s.regularUser.ID), + } + + err := s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.Contains(err.Error(), "does not have permission") + }) + + s.Run("regular user cannot grant admin to others", func() { + // First, let admin add the new user to the project + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "new-user@example.com", + RequesterID: uuid.MustParse(s.adminUser.ID), + Role: authz.RoleProjectViewer, + } + + _, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + s.NoError(err) + + // Now try to update the new user to admin with regular user permissions + // Note: There's no direct "update" method, so we would need to remove and re-add + // with admin permission, which would fail at the removal step + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: "new-user@example.com", + RequesterID: uuid.MustParse(s.regularUser.ID), + } + + err = s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.Contains(err.Error(), "does not have permission") + }) +} + +// TestProjectAdminPermissions verifies that project admins can modify project memberships +func (s *projectPermissionsTestSuite) TestProjectAdminPermissions() { + ctx := context.Background() + projectID := s.project.ID + projectRef := &biz.IdentityReference{ + ID: &projectID, + } + + // Create a new user to add to the project + newUser, err := s.User.UpsertByEmail(ctx, "new-user-2@example.com", nil) + require.NoError(s.T(), err) + + // Add the user to the organization + _, err = s.Membership.Create(ctx, s.org.ID, newUser.ID) + require.NoError(s.T(), err) + + s.Run("project admin can add member to project", func() { + // Project admin adds a new member + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: "new-user-2@example.com", + RequesterID: uuid.MustParse(s.projectAdminUser.ID), + Role: authz.RoleProjectViewer, + } + + membership, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + s.NoError(err) + s.NotNil(membership) + s.Equal(newUser.ID, membership.User.ID) + s.Equal(authz.RoleProjectViewer, membership.Role) + }) + + s.Run("project admin can remove member from project", func() { + // Project admin removes a member + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: projectRef, + UserEmail: s.regularUser.Email, + RequesterID: uuid.MustParse(s.projectAdminUser.ID), + } + + err := s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.NoError(err) + + // Verify the member was removed + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), projectRef, nil) + s.NoError(err) + s.Equal(2, count) // Project admin + new user + + // Verify the regularUser is not in the list + for _, member := range members { + s.NotEqual(s.regularUser.ID, member.User.ID) + } + }) + + s.Run("project admin can grant admin privileges to others", func() { + // First, add the regular user back + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: projectRef, + UserEmail: s.regularUser.Email, + RequesterID: uuid.MustParse(s.projectAdminUser.ID), + Role: authz.RoleProjectAdmin, // Make them an admin this time + } + + membership, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + s.NoError(err) + s.NotNil(membership) + s.Equal(s.regularUser.ID, membership.User.ID) + s.True(membership.Role == authz.RoleProjectAdmin) + }) +} + +// TestProjectIsolation verifies that project admins can't modify other projects +func (s *projectPermissionsTestSuite) TestProjectIsolation() { + ctx := context.Background() + + // Create a second project + secondProject, err := s.Project.Create(ctx, s.org.ID, "test-other-project") + require.NoError(s.T(), err) + + secondProjectID := secondProject.ID + secondProjectRef := &biz.IdentityReference{ + ID: &secondProjectID, + } + + // Create a new user to try to add to the second project + newUser, err := s.User.UpsertByEmail(ctx, "new-user-3@example.com", nil) + require.NoError(s.T(), err) + + // Add the user to the organization + _, err = s.Membership.Create(ctx, s.org.ID, newUser.ID) + require.NoError(s.T(), err) + + s.Run("project admin cannot add member to another project", func() { + // Project admin tries to add a member to a different project + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: secondProjectRef, + UserEmail: "new-user-3@example.com", + RequesterID: uuid.MustParse(s.projectAdminUser.ID), + Role: authz.RoleProjectViewer, + } + + _, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + s.Error(err) + s.Contains(err.Error(), "does not have permission") + }) + + // Add the new user to the second project with org admin permissions + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: secondProjectRef, + UserEmail: "new-user-3@example.com", + RequesterID: uuid.MustParse(s.adminUser.ID), + Role: authz.RoleProjectViewer, + } + _, err = s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + require.NoError(s.T(), err) + + s.Run("project admin cannot remove member from another project", func() { + // Project admin tries to remove a member from a different project + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: secondProjectRef, + UserEmail: "new-user-3@example.com", + RequesterID: uuid.MustParse(s.projectAdminUser.ID), + } + + err := s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.Error(err) + s.Contains(err.Error(), "does not have permission") + }) +} + +// TestOrgAdminPermissions verifies that organization admins can modify any project membership +func (s *projectPermissionsTestSuite) TestOrgAdminPermissions() { + ctx := context.Background() + + // Create a second project + secondProject, err := s.Project.Create(ctx, s.org.ID, "test-admin-other-project") + require.NoError(s.T(), err) + + secondProjectID := secondProject.ID + secondProjectRef := &biz.IdentityReference{ + ID: &secondProjectID, + } + + // Create a new user to add to the second project + newUser, err := s.User.UpsertByEmail(ctx, "new-user-4@example.com", nil) + require.NoError(s.T(), err) + + // Add the user to the organization + _, err = s.Membership.Create(ctx, s.org.ID, newUser.ID) + require.NoError(s.T(), err) + + s.Run("organization admin can add member to any project", func() { + // Org admin adds a member to a project + addOpts := &biz.AddMemberToProjectOpts{ + ProjectReference: secondProjectRef, + UserEmail: "new-user-4@example.com", + RequesterID: uuid.MustParse(s.adminUser.ID), + Role: authz.RoleProjectViewer, + } + + membership, err := s.Project.AddMemberToProject(ctx, uuid.MustParse(s.org.ID), addOpts) + s.NoError(err) + s.NotNil(membership) + s.Equal(newUser.ID, membership.User.ID) + s.Equal(authz.RoleProjectViewer, membership.Role) + }) + + s.Run("organization admin can remove member from any project", func() { + // Org admin removes a member from a project + removeOpts := &biz.RemoveMemberFromProjectOpts{ + ProjectReference: secondProjectRef, + UserEmail: "new-user-4@example.com", + RequesterID: uuid.MustParse(s.adminUser.ID), + } + + err := s.Project.RemoveMemberFromProject(ctx, uuid.MustParse(s.org.ID), removeOpts) + s.NoError(err) + + // Verify the member was removed + members, count, err := s.Project.ListMembers(ctx, uuid.MustParse(s.org.ID), secondProjectRef, nil) + s.NoError(err) + s.Equal(0, count) + s.Empty(members) + }) +} diff --git a/app/controlplane/pkg/biz/testhelpers/wire_gen.go b/app/controlplane/pkg/biz/testhelpers/wire_gen.go index 720770173..8de60dc8c 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire_gen.go +++ b/app/controlplane/pkg/biz/testhelpers/wire_gen.go @@ -151,7 +151,7 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r } projectVersionRepo := data.NewProjectVersionRepo(dataData, logger) projectVersionUseCase := biz.NewProjectVersionUseCase(projectVersionRepo, logger) - projectUseCase := biz.NewProjectsUseCase(logger, projectsRepo) + projectUseCase := biz.NewProjectsUseCase(logger, projectsRepo, membershipRepo, auditorUseCase) groupRepo := data.NewGroupRepo(dataData, logger) groupUseCase := biz.NewGroupUseCase(logger, groupRepo, membershipRepo, auditorUseCase) testingRepos := &TestingRepos{ diff --git a/app/controlplane/pkg/data/project.go b/app/controlplane/pkg/data/project.go index e5b40770b..96946ff4e 100644 --- a/app/controlplane/pkg/data/project.go +++ b/app/controlplane/pkg/data/project.go @@ -19,10 +19,13 @@ import ( "context" "fmt" + "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/membership" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" @@ -91,6 +94,157 @@ func (r *ProjectRepo) Create(ctx context.Context, orgID uuid.UUID, name string) return entProjectToBiz(pro), nil } +// ListMembers lists all members of a project +func (r *ProjectRepo) ListMembers(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID, paginationOpts *pagination.OffsetPaginationOpts) ([]*biz.ProjectMembership, int, error) { + // Check if the project exists and belongs to the organization + existingProject, err := r.FindProjectByOrgIDAndID(ctx, orgID, projectID) + if err != nil { + return nil, 0, fmt.Errorf("failed to find project: %w", err) + } + if existingProject == nil { + return nil, 0, biz.NewErrNotFound("project") + } + + // Build the query with base conditions + query := r.data.DB.Membership.Query(). + Where( + membership.ResourceTypeEQ(authz.ResourceTypeProject), + membership.ResourceID(projectID), + membership.MembershipTypeEQ(authz.MembershipTypeUser), + ) + + // Get total count before applying pagination + totalCount, err := query.Count(ctx) + if err != nil { + return nil, 0, fmt.Errorf("failed to count project members: %w", err) + } + + // Apply pagination + if paginationOpts != nil { + query = query. + Order(ent.Desc(membership.FieldCreatedAt)). + Limit(paginationOpts.Limit()). + Offset(paginationOpts.Offset()) + } + + // Execute the query + memberships, err := query.All(ctx) + if err != nil { + return nil, 0, fmt.Errorf("failed to list project members: %w", err) + } + + // Convert to biz.ProjectMembership objects + result := make([]*biz.ProjectMembership, 0, len(memberships)) + for _, m := range memberships { + u, uErr := r.data.DB.User.Get(ctx, m.MemberID) + if uErr != nil { + if ent.IsNotFound(uErr) { + return nil, 0, biz.NewErrNotFound("user") + } + return nil, 0, fmt.Errorf("failed to find user: %w", uErr) + } + + result = append(result, entProjectMembershipToBiz(m, u)) + } + + return result, totalCount, nil +} + +// AddMemberToProject adds a user to a project with a specific role +func (r *ProjectRepo) AddMemberToProject(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID, userID uuid.UUID, role authz.Role) (*biz.ProjectMembership, error) { + // Check if the project exists and belongs to the organization + existingProject, err := r.FindProjectByOrgIDAndID(ctx, orgID, projectID) + if err != nil { + return nil, fmt.Errorf("failed to find project: %w", err) + } + if existingProject == nil { + return nil, biz.NewErrNotFound("project") + } + + if role != authz.RoleProjectAdmin && role != authz.RoleProjectViewer { + return nil, biz.NewErrValidationStr("invalid role, must be either 'admin' or 'viewer'") + } + + // Create the membership + if _, err := r.data.DB.Membership.Create(). + SetMembershipType(authz.MembershipTypeUser). + SetMemberID(userID). + SetResourceType(authz.ResourceTypeProject). + SetResourceID(projectID). + SetRole(role). + Save(ctx); err != nil { + return nil, fmt.Errorf("failed to create project membership: %w", err) + } + + // Return the created membership + return r.FindProjectMembershipByProjectAndID(ctx, projectID, userID) +} + +// RemoveMemberFromProject removes a user from a project +func (r *ProjectRepo) RemoveMemberFromProject(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID, userID uuid.UUID) error { + // Check if the project exists and belongs to the organization + existingProject, err := r.FindProjectByOrgIDAndID(ctx, orgID, projectID) + if err != nil { + return fmt.Errorf("failed to find project: %w", err) + } + if existingProject == nil { + return biz.NewErrNotFound("project") + } + + // Find the membership to delete + m, err := r.data.DB.Membership.Query(). + Where( + membership.MembershipTypeEQ(authz.MembershipTypeUser), + membership.MemberID(userID), + membership.ResourceTypeEQ(authz.ResourceTypeProject), + membership.ResourceID(projectID), + ).Only(ctx) + + if err != nil { + if ent.IsNotFound(err) { + return biz.NewErrNotFound("membership") + } + return fmt.Errorf("failed to find membership: %w", err) + } + + // Delete the membership + if err := r.data.DB.Membership.DeleteOne(m).Exec(ctx); err != nil { + return fmt.Errorf("failed to delete membership: %w", err) + } + + return nil +} + +// FindProjectMembershipByProjectAndID finds a project membership by project ID and user ID +func (r *ProjectRepo) FindProjectMembershipByProjectAndID(ctx context.Context, projectID uuid.UUID, userID uuid.UUID) (*biz.ProjectMembership, error) { + // Find the membership + m, err := r.data.DB.Membership.Query(). + Where( + membership.MembershipTypeEQ(authz.MembershipTypeUser), + membership.MemberID(userID), + membership.ResourceTypeEQ(authz.ResourceTypeProject), + membership.ResourceID(projectID), + ). + Only(ctx) + + if err != nil { + if ent.IsNotFound(err) { + return nil, nil // Return nil when no membership found + } + return nil, fmt.Errorf("failed to find membership: %w", err) + } + + u, err := r.data.DB.User.Get(ctx, userID) + if err != nil { + if ent.IsNotFound(err) { + return nil, biz.NewErrNotFound("user") + } + return nil, fmt.Errorf("failed to find user: %w", err) + } + + return entProjectMembershipToBiz(m, u), nil +} + // entProjectToBiz converts an ent.Project to a biz.Project func entProjectToBiz(pro *ent.Project) *biz.Project { return &biz.Project{ @@ -101,3 +255,13 @@ func entProjectToBiz(pro *ent.Project) *biz.Project { UpdatedAt: &pro.CreatedAt, } } + +// entProjectMembershipToBiz converts an ent.Membership to a biz.ProjectMembership +func entProjectMembershipToBiz(m *ent.Membership, u *ent.User) *biz.ProjectMembership { + return &biz.ProjectMembership{ + User: entUserToBizUser(u), + Role: m.Role, + CreatedAt: &m.CreatedAt, + UpdatedAt: &m.UpdatedAt, + } +} From b30514726e8187140d673a53c7d12637acb00c33 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Tue, 1 Jul 2025 13:36:50 +0200 Subject: [PATCH 2/2] fix resource type Signed-off-by: Javier Rodriguez --- app/controlplane/pkg/authz/authz.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index a2d433811..0841c5434 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -50,6 +50,7 @@ const ( ResourceGroup = "group" ResourceGroupMembership = "group_membership" ResourceProjectAPIToken = "project_api_token" + ResourceProjectMembership = "project_membership" // We have for now three roles, viewer, admin and owner // The owner of an org @@ -149,9 +150,9 @@ var ( PolicyProjectAPITokenCreate = &Policy{ResourceProjectAPIToken, ActionCreate} PolicyProjectAPITokenRevoke = &Policy{ResourceProjectAPIToken, ActionDelete} // Project Memberships - PolicyProjectListMemberships = &Policy{ResourceGroupMembership, ActionList} - PolicyProjectAddMemberships = &Policy{ResourceGroupMembership, ActionCreate} - PolicyProjectRemoveMemberships = &Policy{ResourceGroupMembership, ActionDelete} + PolicyProjectListMemberships = &Policy{ResourceProjectMembership, ActionList} + PolicyProjectAddMemberships = &Policy{ResourceProjectMembership, ActionCreate} + PolicyProjectRemoveMemberships = &Policy{ResourceProjectMembership, ActionDelete} ) // RolesMap The default list of policies for each role