From 99254548d97550d5b738c5672bb3957bc39aa862 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Thu, 13 Jul 2023 22:22:28 +0200 Subject: [PATCH] feat(controlplane): update cas-backends Signed-off-by: Miguel Martinez Trivino --- app/cli/cmd/casbackend.go | 15 +- app/cli/cmd/casbackend_add_oci.go | 37 ++- app/cli/cmd/casbackend_update_oci.go | 116 +++++++ ...asbackend_add_oci.go => casbackend_add.go} | 27 +- app/cli/internal/action/casbackend_update.go | 64 ++++ .../api/controlplane/v1/cas_backends.pb.go | 226 ++++++++++++-- .../v1/cas_backends.pb.validate.go | 291 ++++++++++++++++++ .../api/controlplane/v1/cas_backends.proto | 22 +- .../controlplane/v1/cas_backends_grpc.pb.go | 37 +++ .../frontend/controlplane/v1/cas_backends.ts | 215 +++++++++++++ app/controlplane/internal/biz/casbackend.go | 68 +++- .../internal/biz/mocks/CASBackendRepo.go | 26 ++ app/controlplane/internal/data/casbackend.go | 32 +- .../internal/service/casbackend.go | 33 +- internal/ociauth/auth.go | 2 +- 15 files changed, 1138 insertions(+), 73 deletions(-) create mode 100644 app/cli/cmd/casbackend_update_oci.go rename app/cli/internal/action/{casbackend_add_oci.go => casbackend_add.go} (70%) create mode 100644 app/cli/internal/action/casbackend_update.go diff --git a/app/cli/cmd/casbackend.go b/app/cli/cmd/casbackend.go index f81cdabab..033855ee4 100644 --- a/app/cli/cmd/casbackend.go +++ b/app/cli/cmd/casbackend.go @@ -25,7 +25,7 @@ func newCASBackendCmd() *cobra.Command { Short: "Operations on Artifact CAS backends", } - cmd.AddCommand(newCASBackendListCmd(), newCASBackendAddCmd()) + cmd.AddCommand(newCASBackendListCmd(), newCASBackendAddCmd(), newCASBackendUpdateCmd()) return cmd } @@ -41,3 +41,16 @@ func newCASBackendAddCmd() *cobra.Command { cmd.AddCommand(newCASBackendAddOCICmd()) return cmd } + +func newCASBackendUpdateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Update a CAS backend description, credentials or default status", + } + + cmd.PersistentFlags().Bool("default", false, "set the backend as default in your organization") + cmd.PersistentFlags().String("description", "", "descriptive information for this registration") + + cmd.AddCommand(newCASBackendUpdateOCICmd()) + return cmd +} diff --git a/app/cli/cmd/casbackend_add_oci.go b/app/cli/cmd/casbackend_add_oci.go index e2f293014..f77c628a0 100644 --- a/app/cli/cmd/casbackend_add_oci.go +++ b/app/cli/cmd/casbackend_add_oci.go @@ -19,6 +19,7 @@ import ( "fmt" "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/go-kratos/kratos/v2/log" "github.com/spf13/cobra" ) @@ -37,18 +38,25 @@ func newCASBackendAddOCICmd() *cobra.Command { cobra.CheckErr(err) if isDefault { - if confirmed, err := confirmDefaultCASBackendOverride(actionOpts); err != nil { + if confirmed, err := confirmDefaultCASBackendOverride(actionOpts, ""); err != nil { return err } else if !confirmed { - fmt.Println("Aborting...") + log.Info("Aborting...") + return nil } } - opts := &action.NewCASBackendOCIAddOpts{ - Repo: repo, Username: username, Password: password, Default: isDefault, Description: description, + opts := &action.NewCASBackendAddOpts{ + Location: repo, Description: description, + Provider: "OCI", + Credentials: map[string]any{ + "username": username, + "password": password, + }, + Default: isDefault, } - res, err := action.NewCASBackendAddOCI(actionOpts).Run(opts) + res, err := action.NewCASBackendAdd(actionOpts).Run(opts) if err != nil { return err } else if res == nil { @@ -74,24 +82,25 @@ func newCASBackendAddOCICmd() *cobra.Command { } // confirmDefaultCASBackendOverride asks the user to confirm the override of the default CAS backend -// in the event that there is one already set. -func confirmDefaultCASBackendOverride(actionOpts *action.ActionsOpts) (bool, error) { +// in the event that there is one already set and its not the same as the one we are setting +func confirmDefaultCASBackendOverride(actionOpts *action.ActionsOpts, id string) (bool, error) { // get existing backends backends, err := action.NewCASBackendList(actionOpts).Run() if err != nil { return false, fmt.Errorf("failed to list existing CAS backends: %w", err) } - var hasDefault bool + // Find the default + var defaultB *action.CASBackendItem for _, b := range backends { if b.Default { - hasDefault = true + defaultB = b break } } - // If there is none, we are done - if !hasDefault { + // If there is none or there is but it's the same as the one we are setting, we are ok + if defaultB == nil || (id != "" && id == defaultB.ID) { return true, nil } @@ -101,9 +110,5 @@ func confirmDefaultCASBackendOverride(actionOpts *action.ActionsOpts) (bool, err fmt.Scanln(&gotChallenge) // If the user does not confirm, we are done - if gotChallenge != "y" && gotChallenge != "Y" { - return false, nil - } - - return true, nil + return gotChallenge == "y" || gotChallenge == "Y", nil } diff --git a/app/cli/cmd/casbackend_update_oci.go b/app/cli/cmd/casbackend_update_oci.go new file mode 100644 index 000000000..aeb5dfc92 --- /dev/null +++ b/app/cli/cmd/casbackend_update_oci.go @@ -0,0 +1,116 @@ +// +// Copyright 2023 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/go-kratos/kratos/v2/log" + "github.com/spf13/cobra" +) + +func newCASBackendUpdateOCICmd() *cobra.Command { + var backendID, username, password string + cmd := &cobra.Command{ + Use: "oci", + Short: "Update a OCI CAS Backend description, credentials or default status", + RunE: func(cmd *cobra.Command, args []string) error { + // If we are setting the default, we list existing CAS backends + // and ask the user to confirm the rewrite + isDefault, err := cmd.Flags().GetBool("default") + cobra.CheckErr(err) + + description, err := cmd.Flags().GetString("description") + cobra.CheckErr(err) + + // If we are overriding the default we ask for confirmation + if isDefault { + if confirmed, err := confirmDefaultCASBackendOverride(actionOpts, backendID); err != nil { + return err + } else if !confirmed { + log.Info("Aborting...") + return nil + } + } else { // If we are removing the default we ask for confirmation too + if confirmed, err := confirmDefaultCASBackendRemoval(actionOpts, backendID); err != nil { + return err + } else if !confirmed { + log.Info("Aborting...") + return nil + } + } + + opts := &action.NewCASBackendUpdateOpts{ + ID: backendID, + Description: description, + Credentials: map[string]any{ + "username": username, + "password": password, + }, + Default: isDefault, + } + + if username == "" && password == "" { + opts.Credentials = nil + } + + res, err := action.NewCASBackendUpdate(actionOpts).Run(opts) + if err != nil { + return err + } else if res == nil { + return nil + } + + return encodeOutput([]*action.CASBackendItem{res}, casBackendListTableOutput) + }, + } + + cmd.Flags().StringVar(&backendID, "id", "", "CAS Backend ID") + err := cmd.MarkFlagRequired("id") + cobra.CheckErr(err) + + cmd.Flags().StringVarP(&username, "username", "u", "", "registry username") + + cmd.Flags().StringVarP(&password, "password", "p", "", "registry password") + return cmd +} + +// If we are removing the default we confirm too +func confirmDefaultCASBackendRemoval(actionOpts *action.ActionsOpts, id string) (bool, error) { + // get existing backends + backends, err := action.NewCASBackendList(actionOpts).Run() + if err != nil { + return false, fmt.Errorf("failed to list existing CAS backends: %w", err) + } + + for _, b := range backends { + // We are removing ourselves as the default, ask the user to confirm + if b.Default && b.ID == id { + // Ask the user to confirm the override + fmt.Println("This is the default CAS backend and your are setting default=false.\nPlease confirm to continue y/N: ") + var gotChallenge string + fmt.Scanln(&gotChallenge) + + // If the user does not confirm, we are done + if gotChallenge != "y" && gotChallenge != "Y" { + return false, nil + } + } + } + + return true, nil +} diff --git a/app/cli/internal/action/casbackend_add_oci.go b/app/cli/internal/action/casbackend_add.go similarity index 70% rename from app/cli/internal/action/casbackend_add_oci.go rename to app/cli/internal/action/casbackend_add.go index f9832e06c..368e7f9b6 100644 --- a/app/cli/internal/action/casbackend_add_oci.go +++ b/app/cli/internal/action/casbackend_add.go @@ -23,34 +23,33 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -type CASBackendAddOCI struct { +type CASBackendAdd struct { cfg *ActionsOpts } -type NewCASBackendOCIAddOpts struct { - Repo, Username, Password string - Description string - Default bool +type NewCASBackendAddOpts struct { + Location string + Provider string + Description string + Default bool + Credentials map[string]any } -func NewCASBackendAddOCI(cfg *ActionsOpts) *CASBackendAddOCI { - return &CASBackendAddOCI{cfg} +func NewCASBackendAdd(cfg *ActionsOpts) *CASBackendAdd { + return &CASBackendAdd{cfg} } -func (action *CASBackendAddOCI) Run(opts *NewCASBackendOCIAddOpts) (*CASBackendItem, error) { +func (action *CASBackendAdd) Run(opts *NewCASBackendAddOpts) (*CASBackendItem, error) { // Custom configuration for OCI - credentials, err := structpb.NewStruct(map[string]any{ - "username": opts.Username, - "password": opts.Password, - }) + credentials, err := structpb.NewStruct(opts.Credentials) if err != nil { return nil, fmt.Errorf("failed to parse arguments: %w", err) } client := pb.NewCASBackendServiceClient(action.cfg.CPConnection) resp, err := client.Create(context.Background(), &pb.CASBackendServiceCreateRequest{ - Location: opts.Repo, - Provider: "OCI", + Location: opts.Location, + Provider: opts.Provider, Description: opts.Description, Default: opts.Default, Credentials: credentials, diff --git a/app/cli/internal/action/casbackend_update.go b/app/cli/internal/action/casbackend_update.go new file mode 100644 index 000000000..a1092fb5d --- /dev/null +++ b/app/cli/internal/action/casbackend_update.go @@ -0,0 +1,64 @@ +// +// Copyright 2023 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" + "fmt" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "google.golang.org/protobuf/types/known/structpb" +) + +type CASBackendUpdate struct { + cfg *ActionsOpts +} + +type NewCASBackendUpdateOpts struct { + ID string + Description string + Default bool + Credentials map[string]any +} + +func NewCASBackendUpdate(cfg *ActionsOpts) *CASBackendUpdate { + return &CASBackendUpdate{cfg} +} + +func (action *CASBackendUpdate) Run(opts *NewCASBackendUpdateOpts) (*CASBackendItem, error) { + var credentials *structpb.Struct + var err error + + if opts.Credentials != nil { + credentials, err = structpb.NewStruct(opts.Credentials) + if err != nil { + return nil, fmt.Errorf("failed to parse arguments: %w", err) + } + } + + client := pb.NewCASBackendServiceClient(action.cfg.CPConnection) + resp, err := client.Update(context.Background(), &pb.CASBackendServiceUpdateRequest{ + Id: opts.ID, + Description: opts.Description, + Default: opts.Default, + Credentials: credentials, + }) + if err != nil { + return nil, err + } + + return pbCASBackendItemToAction(resp.Result), nil +} diff --git a/app/controlplane/api/controlplane/v1/cas_backends.pb.go b/app/controlplane/api/controlplane/v1/cas_backends.pb.go index ba6da5f7f..aad3e6d71 100644 --- a/app/controlplane/api/controlplane/v1/cas_backends.pb.go +++ b/app/controlplane/api/controlplane/v1/cas_backends.pb.go @@ -253,6 +253,132 @@ func (x *CASBackendServiceCreateResponse) GetResult() *CASBackendItem { return nil } +// Update a CAS backend is limited to +// - description +// - set is as default +// - rotate credentials +type CASBackendServiceUpdateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // UUID of the workflow to attach + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Descriptive name + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // Set as default in your organization + Default bool `protobuf:"varint,3,opt,name=default,proto3" json:"default,omitempty"` + // Credentials, useful for rotation + Credentials *structpb.Struct `protobuf:"bytes,4,opt,name=credentials,proto3" json:"credentials,omitempty"` +} + +func (x *CASBackendServiceUpdateRequest) Reset() { + *x = CASBackendServiceUpdateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_cas_backends_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CASBackendServiceUpdateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CASBackendServiceUpdateRequest) ProtoMessage() {} + +func (x *CASBackendServiceUpdateRequest) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_cas_backends_proto_msgTypes[4] + 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 CASBackendServiceUpdateRequest.ProtoReflect.Descriptor instead. +func (*CASBackendServiceUpdateRequest) Descriptor() ([]byte, []int) { + return file_controlplane_v1_cas_backends_proto_rawDescGZIP(), []int{4} +} + +func (x *CASBackendServiceUpdateRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *CASBackendServiceUpdateRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *CASBackendServiceUpdateRequest) GetDefault() bool { + if x != nil { + return x.Default + } + return false +} + +func (x *CASBackendServiceUpdateRequest) GetCredentials() *structpb.Struct { + if x != nil { + return x.Credentials + } + return nil +} + +type CASBackendServiceUpdateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Result *CASBackendItem `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` +} + +func (x *CASBackendServiceUpdateResponse) Reset() { + *x = CASBackendServiceUpdateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controlplane_v1_cas_backends_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CASBackendServiceUpdateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CASBackendServiceUpdateResponse) ProtoMessage() {} + +func (x *CASBackendServiceUpdateResponse) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_cas_backends_proto_msgTypes[5] + 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 CASBackendServiceUpdateResponse.ProtoReflect.Descriptor instead. +func (*CASBackendServiceUpdateResponse) Descriptor() ([]byte, []int) { + return file_controlplane_v1_cas_backends_proto_rawDescGZIP(), []int{5} +} + +func (x *CASBackendServiceUpdateResponse) GetResult() *CASBackendItem { + if x != nil { + return x.Result + } + return nil +} + var File_controlplane_v1_cas_backends_proto protoreflect.FileDescriptor var file_controlplane_v1_cas_backends_proto_rawDesc = []byte{ @@ -293,7 +419,24 @@ var file_controlplane_v1_cas_backends_proto_rawDesc = []byte{ 0x12, 0x37, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x74, 0x65, - 0x6d, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0xe7, 0x01, 0x0a, 0x11, 0x43, 0x41, + 0x6d, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0xb1, 0x01, 0x0a, 0x1e, 0x43, 0x41, + 0x53, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0xb0, + 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, + 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x12, 0x39, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, + 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0x5a, 0x0a, + 0x1f, 0x43, 0x41, 0x53, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x37, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x74, 0x65, + 0x6d, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0xd4, 0x02, 0x0a, 0x11, 0x43, 0x41, 0x53, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x65, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, 0x42, 0x61, 0x63, @@ -308,12 +451,19 @@ var file_controlplane_v1_cas_backends_proto_rawDesc = []byte{ 0x74, 0x1a, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 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, + 0x6e, 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x41, 0x53, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 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, 0x43, 0x41, 0x53, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 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, } var ( @@ -328,28 +478,34 @@ func file_controlplane_v1_cas_backends_proto_rawDescGZIP() []byte { return file_controlplane_v1_cas_backends_proto_rawDescData } -var file_controlplane_v1_cas_backends_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_controlplane_v1_cas_backends_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_controlplane_v1_cas_backends_proto_goTypes = []interface{}{ (*CASBackendServiceListRequest)(nil), // 0: controlplane.v1.CASBackendServiceListRequest (*CASBackendServiceListResponse)(nil), // 1: controlplane.v1.CASBackendServiceListResponse (*CASBackendServiceCreateRequest)(nil), // 2: controlplane.v1.CASBackendServiceCreateRequest (*CASBackendServiceCreateResponse)(nil), // 3: controlplane.v1.CASBackendServiceCreateResponse - (*CASBackendItem)(nil), // 4: controlplane.v1.CASBackendItem - (*structpb.Struct)(nil), // 5: google.protobuf.Struct + (*CASBackendServiceUpdateRequest)(nil), // 4: controlplane.v1.CASBackendServiceUpdateRequest + (*CASBackendServiceUpdateResponse)(nil), // 5: controlplane.v1.CASBackendServiceUpdateResponse + (*CASBackendItem)(nil), // 6: controlplane.v1.CASBackendItem + (*structpb.Struct)(nil), // 7: google.protobuf.Struct } var file_controlplane_v1_cas_backends_proto_depIdxs = []int32{ - 4, // 0: controlplane.v1.CASBackendServiceListResponse.result:type_name -> controlplane.v1.CASBackendItem - 5, // 1: controlplane.v1.CASBackendServiceCreateRequest.credentials:type_name -> google.protobuf.Struct - 4, // 2: controlplane.v1.CASBackendServiceCreateResponse.result:type_name -> controlplane.v1.CASBackendItem - 0, // 3: controlplane.v1.CASBackendService.List:input_type -> controlplane.v1.CASBackendServiceListRequest - 2, // 4: controlplane.v1.CASBackendService.Create:input_type -> controlplane.v1.CASBackendServiceCreateRequest - 1, // 5: controlplane.v1.CASBackendService.List:output_type -> controlplane.v1.CASBackendServiceListResponse - 3, // 6: controlplane.v1.CASBackendService.Create:output_type -> controlplane.v1.CASBackendServiceCreateResponse - 5, // [5:7] is the sub-list for method output_type - 3, // [3:5] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 6, // 0: controlplane.v1.CASBackendServiceListResponse.result:type_name -> controlplane.v1.CASBackendItem + 7, // 1: controlplane.v1.CASBackendServiceCreateRequest.credentials:type_name -> google.protobuf.Struct + 6, // 2: controlplane.v1.CASBackendServiceCreateResponse.result:type_name -> controlplane.v1.CASBackendItem + 7, // 3: controlplane.v1.CASBackendServiceUpdateRequest.credentials:type_name -> google.protobuf.Struct + 6, // 4: controlplane.v1.CASBackendServiceUpdateResponse.result:type_name -> controlplane.v1.CASBackendItem + 0, // 5: controlplane.v1.CASBackendService.List:input_type -> controlplane.v1.CASBackendServiceListRequest + 2, // 6: controlplane.v1.CASBackendService.Create:input_type -> controlplane.v1.CASBackendServiceCreateRequest + 4, // 7: controlplane.v1.CASBackendService.Update:input_type -> controlplane.v1.CASBackendServiceUpdateRequest + 1, // 8: controlplane.v1.CASBackendService.List:output_type -> controlplane.v1.CASBackendServiceListResponse + 3, // 9: controlplane.v1.CASBackendService.Create:output_type -> controlplane.v1.CASBackendServiceCreateResponse + 5, // 10: controlplane.v1.CASBackendService.Update:output_type -> controlplane.v1.CASBackendServiceUpdateResponse + 8, // [8:11] is the sub-list for method output_type + 5, // [5:8] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_controlplane_v1_cas_backends_proto_init() } @@ -407,6 +563,30 @@ func file_controlplane_v1_cas_backends_proto_init() { return nil } } + file_controlplane_v1_cas_backends_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CASBackendServiceUpdateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controlplane_v1_cas_backends_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CASBackendServiceUpdateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -414,7 +594,7 @@ func file_controlplane_v1_cas_backends_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controlplane_v1_cas_backends_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, diff --git a/app/controlplane/api/controlplane/v1/cas_backends.pb.validate.go b/app/controlplane/api/controlplane/v1/cas_backends.pb.validate.go index 5ecf1fa71..691e02a5f 100644 --- a/app/controlplane/api/controlplane/v1/cas_backends.pb.validate.go +++ b/app/controlplane/api/controlplane/v1/cas_backends.pb.validate.go @@ -35,6 +35,9 @@ var ( _ = sort.Sort ) +// define the regex for a UUID once up-front +var _cas_backends_uuidPattern = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") + // Validate checks the field values on CASBackendServiceListRequest with the // rules defined in the proto definition for this message. If any rules are // violated, the first error encountered is returned, or nil if there are no violations. @@ -575,3 +578,291 @@ var _ interface { Cause() error ErrorName() string } = CASBackendServiceCreateResponseValidationError{} + +// Validate checks the field values on CASBackendServiceUpdateRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *CASBackendServiceUpdateRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CASBackendServiceUpdateRequest with +// the rules defined in the proto definition for this message. If any rules +// are violated, the result is a list of violation errors wrapped in +// CASBackendServiceUpdateRequestMultiError, or nil if none found. +func (m *CASBackendServiceUpdateRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *CASBackendServiceUpdateRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if err := m._validateUuid(m.GetId()); err != nil { + err = CASBackendServiceUpdateRequestValidationError{ + field: "Id", + reason: "value must be a valid UUID", + cause: err, + } + if !all { + return err + } + errors = append(errors, err) + } + + // no validation rules for Description + + // no validation rules for Default + + if all { + switch v := interface{}(m.GetCredentials()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CASBackendServiceUpdateRequestValidationError{ + field: "Credentials", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CASBackendServiceUpdateRequestValidationError{ + field: "Credentials", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetCredentials()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CASBackendServiceUpdateRequestValidationError{ + field: "Credentials", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return CASBackendServiceUpdateRequestMultiError(errors) + } + + return nil +} + +func (m *CASBackendServiceUpdateRequest) _validateUuid(uuid string) error { + if matched := _cas_backends_uuidPattern.MatchString(uuid); !matched { + return errors.New("invalid uuid format") + } + + return nil +} + +// CASBackendServiceUpdateRequestMultiError is an error wrapping multiple +// validation errors returned by CASBackendServiceUpdateRequest.ValidateAll() +// if the designated constraints aren't met. +type CASBackendServiceUpdateRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CASBackendServiceUpdateRequestMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CASBackendServiceUpdateRequestMultiError) AllErrors() []error { return m } + +// CASBackendServiceUpdateRequestValidationError is the validation error +// returned by CASBackendServiceUpdateRequest.Validate if the designated +// constraints aren't met. +type CASBackendServiceUpdateRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CASBackendServiceUpdateRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CASBackendServiceUpdateRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CASBackendServiceUpdateRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CASBackendServiceUpdateRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CASBackendServiceUpdateRequestValidationError) ErrorName() string { + return "CASBackendServiceUpdateRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e CASBackendServiceUpdateRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCASBackendServiceUpdateRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CASBackendServiceUpdateRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CASBackendServiceUpdateRequestValidationError{} + +// Validate checks the field values on CASBackendServiceUpdateResponse with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *CASBackendServiceUpdateResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CASBackendServiceUpdateResponse with +// the rules defined in the proto definition for this message. If any rules +// are violated, the result is a list of violation errors wrapped in +// CASBackendServiceUpdateResponseMultiError, or nil if none found. +func (m *CASBackendServiceUpdateResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *CASBackendServiceUpdateResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetResult()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CASBackendServiceUpdateResponseValidationError{ + field: "Result", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CASBackendServiceUpdateResponseValidationError{ + field: "Result", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetResult()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CASBackendServiceUpdateResponseValidationError{ + field: "Result", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return CASBackendServiceUpdateResponseMultiError(errors) + } + + return nil +} + +// CASBackendServiceUpdateResponseMultiError is an error wrapping multiple +// validation errors returned by CASBackendServiceUpdateResponse.ValidateAll() +// if the designated constraints aren't met. +type CASBackendServiceUpdateResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CASBackendServiceUpdateResponseMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CASBackendServiceUpdateResponseMultiError) AllErrors() []error { return m } + +// CASBackendServiceUpdateResponseValidationError is the validation error +// returned by CASBackendServiceUpdateResponse.Validate if the designated +// constraints aren't met. +type CASBackendServiceUpdateResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CASBackendServiceUpdateResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CASBackendServiceUpdateResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CASBackendServiceUpdateResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CASBackendServiceUpdateResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CASBackendServiceUpdateResponseValidationError) ErrorName() string { + return "CASBackendServiceUpdateResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e CASBackendServiceUpdateResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCASBackendServiceUpdateResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CASBackendServiceUpdateResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CASBackendServiceUpdateResponseValidationError{} diff --git a/app/controlplane/api/controlplane/v1/cas_backends.proto b/app/controlplane/api/controlplane/v1/cas_backends.proto index 8f45e31e9..e8cf96bde 100644 --- a/app/controlplane/api/controlplane/v1/cas_backends.proto +++ b/app/controlplane/api/controlplane/v1/cas_backends.proto @@ -26,6 +26,7 @@ import "google/protobuf/struct.proto"; service CASBackendService { rpc List (CASBackendServiceListRequest) returns (CASBackendServiceListResponse); rpc Create (CASBackendServiceCreateRequest) returns (CASBackendServiceCreateResponse); + rpc Update (CASBackendServiceUpdateRequest) returns (CASBackendServiceUpdateResponse); } message CASBackendServiceListRequest {} @@ -41,7 +42,7 @@ message CASBackendServiceCreateRequest { string provider = 2 [(validate.rules).string.min_len = 1]; // Descriptive name string description = 3; - // Set as default in your organization + // Set as default in your organization bool default = 4; // Arbitrary configuration for the integration google.protobuf.Struct credentials = 5 [(validate.rules).message.required = true]; @@ -49,4 +50,23 @@ message CASBackendServiceCreateRequest { message CASBackendServiceCreateResponse { CASBackendItem result = 1; +} + +// Update a CAS backend is limited to +// - description +// - set is as default +// - rotate credentials +message CASBackendServiceUpdateRequest { + // UUID of the workflow to attach + string id = 1 [(validate.rules).string.uuid = true]; + // Descriptive name + string description = 2; + // Set as default in your organization + bool default = 3; + // Credentials, useful for rotation + google.protobuf.Struct credentials = 4; +} + +message CASBackendServiceUpdateResponse { + CASBackendItem result = 1; } \ No newline at end of file diff --git a/app/controlplane/api/controlplane/v1/cas_backends_grpc.pb.go b/app/controlplane/api/controlplane/v1/cas_backends_grpc.pb.go index 7ce731e2a..8745b7bf8 100644 --- a/app/controlplane/api/controlplane/v1/cas_backends_grpc.pb.go +++ b/app/controlplane/api/controlplane/v1/cas_backends_grpc.pb.go @@ -36,6 +36,7 @@ const _ = grpc.SupportPackageIsVersion7 const ( CASBackendService_List_FullMethodName = "/controlplane.v1.CASBackendService/List" CASBackendService_Create_FullMethodName = "/controlplane.v1.CASBackendService/Create" + CASBackendService_Update_FullMethodName = "/controlplane.v1.CASBackendService/Update" ) // CASBackendServiceClient is the client API for CASBackendService service. @@ -44,6 +45,7 @@ const ( type CASBackendServiceClient interface { List(ctx context.Context, in *CASBackendServiceListRequest, opts ...grpc.CallOption) (*CASBackendServiceListResponse, error) Create(ctx context.Context, in *CASBackendServiceCreateRequest, opts ...grpc.CallOption) (*CASBackendServiceCreateResponse, error) + Update(ctx context.Context, in *CASBackendServiceUpdateRequest, opts ...grpc.CallOption) (*CASBackendServiceUpdateResponse, error) } type cASBackendServiceClient struct { @@ -72,12 +74,22 @@ func (c *cASBackendServiceClient) Create(ctx context.Context, in *CASBackendServ return out, nil } +func (c *cASBackendServiceClient) Update(ctx context.Context, in *CASBackendServiceUpdateRequest, opts ...grpc.CallOption) (*CASBackendServiceUpdateResponse, error) { + out := new(CASBackendServiceUpdateResponse) + err := c.cc.Invoke(ctx, CASBackendService_Update_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // CASBackendServiceServer is the server API for CASBackendService service. // All implementations must embed UnimplementedCASBackendServiceServer // for forward compatibility type CASBackendServiceServer interface { List(context.Context, *CASBackendServiceListRequest) (*CASBackendServiceListResponse, error) Create(context.Context, *CASBackendServiceCreateRequest) (*CASBackendServiceCreateResponse, error) + Update(context.Context, *CASBackendServiceUpdateRequest) (*CASBackendServiceUpdateResponse, error) mustEmbedUnimplementedCASBackendServiceServer() } @@ -91,6 +103,9 @@ func (UnimplementedCASBackendServiceServer) List(context.Context, *CASBackendSer func (UnimplementedCASBackendServiceServer) Create(context.Context, *CASBackendServiceCreateRequest) (*CASBackendServiceCreateResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") } +func (UnimplementedCASBackendServiceServer) Update(context.Context, *CASBackendServiceUpdateRequest) (*CASBackendServiceUpdateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} func (UnimplementedCASBackendServiceServer) mustEmbedUnimplementedCASBackendServiceServer() {} // UnsafeCASBackendServiceServer may be embedded to opt out of forward compatibility for this service. @@ -140,6 +155,24 @@ func _CASBackendService_Create_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _CASBackendService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CASBackendServiceUpdateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CASBackendServiceServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CASBackendService_Update_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CASBackendServiceServer).Update(ctx, req.(*CASBackendServiceUpdateRequest)) + } + return interceptor(ctx, in, info, handler) +} + // CASBackendService_ServiceDesc is the grpc.ServiceDesc for CASBackendService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -155,6 +188,10 @@ var CASBackendService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Create", Handler: _CASBackendService_Create_Handler, }, + { + MethodName: "Update", + Handler: _CASBackendService_Update_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "controlplane/v1/cas_backends.proto", diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/cas_backends.ts b/app/controlplane/api/gen/frontend/controlplane/v1/cas_backends.ts index bf148ad33..5d712a902 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/cas_backends.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/cas_backends.ts @@ -31,6 +31,27 @@ export interface CASBackendServiceCreateResponse { result?: CASBackendItem; } +/** + * Update a CAS backend is limited to + * - description + * - set is as default + * - rotate credentials + */ +export interface CASBackendServiceUpdateRequest { + /** UUID of the workflow to attach */ + id: string; + /** Descriptive name */ + description: string; + /** Set as default in your organization */ + default: boolean; + /** Credentials, useful for rotation */ + credentials?: { [key: string]: any }; +} + +export interface CASBackendServiceUpdateResponse { + result?: CASBackendItem; +} + function createBaseCASBackendServiceListRequest(): CASBackendServiceListRequest { return {}; } @@ -309,6 +330,165 @@ export const CASBackendServiceCreateResponse = { }, }; +function createBaseCASBackendServiceUpdateRequest(): CASBackendServiceUpdateRequest { + return { id: "", description: "", default: false, credentials: undefined }; +} + +export const CASBackendServiceUpdateRequest = { + encode(message: CASBackendServiceUpdateRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.description !== "") { + writer.uint32(18).string(message.description); + } + if (message.default === true) { + writer.uint32(24).bool(message.default); + } + if (message.credentials !== undefined) { + Struct.encode(Struct.wrap(message.credentials), writer.uint32(34).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): CASBackendServiceUpdateRequest { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCASBackendServiceUpdateRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.description = reader.string(); + continue; + case 3: + if (tag !== 24) { + break; + } + + message.default = reader.bool(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.credentials = Struct.unwrap(Struct.decode(reader, reader.uint32())); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): CASBackendServiceUpdateRequest { + return { + id: isSet(object.id) ? String(object.id) : "", + description: isSet(object.description) ? String(object.description) : "", + default: isSet(object.default) ? Boolean(object.default) : false, + credentials: isObject(object.credentials) ? object.credentials : undefined, + }; + }, + + toJSON(message: CASBackendServiceUpdateRequest): unknown { + const obj: any = {}; + message.id !== undefined && (obj.id = message.id); + message.description !== undefined && (obj.description = message.description); + message.default !== undefined && (obj.default = message.default); + message.credentials !== undefined && (obj.credentials = message.credentials); + return obj; + }, + + create, I>>(base?: I): CASBackendServiceUpdateRequest { + return CASBackendServiceUpdateRequest.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): CASBackendServiceUpdateRequest { + const message = createBaseCASBackendServiceUpdateRequest(); + message.id = object.id ?? ""; + message.description = object.description ?? ""; + message.default = object.default ?? false; + message.credentials = object.credentials ?? undefined; + return message; + }, +}; + +function createBaseCASBackendServiceUpdateResponse(): CASBackendServiceUpdateResponse { + return { result: undefined }; +} + +export const CASBackendServiceUpdateResponse = { + encode(message: CASBackendServiceUpdateResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.result !== undefined) { + CASBackendItem.encode(message.result, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): CASBackendServiceUpdateResponse { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCASBackendServiceUpdateResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.result = CASBackendItem.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): CASBackendServiceUpdateResponse { + return { result: isSet(object.result) ? CASBackendItem.fromJSON(object.result) : undefined }; + }, + + toJSON(message: CASBackendServiceUpdateResponse): unknown { + const obj: any = {}; + message.result !== undefined && (obj.result = message.result ? CASBackendItem.toJSON(message.result) : undefined); + return obj; + }, + + create, I>>(base?: I): CASBackendServiceUpdateResponse { + return CASBackendServiceUpdateResponse.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): CASBackendServiceUpdateResponse { + const message = createBaseCASBackendServiceUpdateResponse(); + message.result = (object.result !== undefined && object.result !== null) + ? CASBackendItem.fromPartial(object.result) + : undefined; + return message; + }, +}; + export interface CASBackendService { List( request: DeepPartial, @@ -318,6 +498,10 @@ export interface CASBackendService { request: DeepPartial, metadata?: grpc.Metadata, ): Promise; + Update( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise; } export class CASBackendServiceClientImpl implements CASBackendService { @@ -327,6 +511,7 @@ export class CASBackendServiceClientImpl implements CASBackendService { this.rpc = rpc; this.List = this.List.bind(this); this.Create = this.Create.bind(this); + this.Update = this.Update.bind(this); } List( @@ -342,6 +527,13 @@ export class CASBackendServiceClientImpl implements CASBackendService { ): Promise { return this.rpc.unary(CASBackendServiceCreateDesc, CASBackendServiceCreateRequest.fromPartial(request), metadata); } + + Update( + request: DeepPartial, + metadata?: grpc.Metadata, + ): Promise { + return this.rpc.unary(CASBackendServiceUpdateDesc, CASBackendServiceUpdateRequest.fromPartial(request), metadata); + } } export const CASBackendServiceDesc = { serviceName: "controlplane.v1.CASBackendService" }; @@ -392,6 +584,29 @@ export const CASBackendServiceCreateDesc: UnaryMethodDefinitionish = { } as any, }; +export const CASBackendServiceUpdateDesc: UnaryMethodDefinitionish = { + methodName: "Update", + service: CASBackendServiceDesc, + requestStream: false, + responseStream: false, + requestType: { + serializeBinary() { + return CASBackendServiceUpdateRequest.encode(this).finish(); + }, + } as any, + responseType: { + deserializeBinary(data: Uint8Array) { + const value = CASBackendServiceUpdateResponse.decode(data); + return { + ...value, + toObject() { + return value; + }, + }; + }, + } as any, +}; + interface UnaryMethodDefinitionishR extends grpc.UnaryMethodDefinition { requestStream: any; responseStream: any; diff --git a/app/controlplane/internal/biz/casbackend.go b/app/controlplane/internal/biz/casbackend.go index a299f768c..3f3b0138d 100644 --- a/app/controlplane/internal/biz/casbackend.go +++ b/app/controlplane/internal/biz/casbackend.go @@ -74,6 +74,7 @@ type CASBackendUpdateOpts struct { type CASBackendRepo interface { FindDefaultBackend(ctx context.Context, orgID uuid.UUID) (*CASBackend, error) FindByID(ctx context.Context, ID uuid.UUID) (*CASBackend, error) + FindByIDInOrg(ctx context.Context, OrgID, ID uuid.UUID) (*CASBackend, error) List(ctx context.Context, orgID uuid.UUID) ([]*CASBackend, error) UpdateValidationStatus(ctx context.Context, ID uuid.UUID, status CASBackendValidationStatus) error Create(context.Context, *CASBackendCreateOpts) (*CASBackend, error) @@ -136,12 +137,7 @@ func (uc *CASBackendUseCase) FindByID(ctx context.Context, id string) (*CASBacke return backend, nil } -func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, location, description string, provider CASBackendProvider, configJSON []byte, defaultB bool) (*CASBackend, error) { - orgUUID, err := uuid.Parse(orgID) - if err != nil { - return nil, NewErrInvalidUUID(err) - } - +func validateAndExtractCredentials(provider CASBackendProvider, location string, credsJSON []byte) (any, error) { // TODO: (miguel) this logic (marshalling from struct + validation) will be moved to the actual backend implementation // This endpoint will support other backends in the future if provider != CASBackendOCI { @@ -153,7 +149,7 @@ func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, location, descri Username string `json:"username"` }{} - if err := json.Unmarshal(configJSON, &ociConfig); err != nil { + if err := json.Unmarshal(credsJSON, &ociConfig); err != nil { return nil, NewErrValidation(err) } @@ -179,6 +175,21 @@ func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, location, descri return nil, NewErrValidation(err) } + return creds, nil +} + +func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, location, description string, provider CASBackendProvider, credsJSON []byte, defaultB bool) (*CASBackend, error) { + orgUUID, err := uuid.Parse(orgID) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + // Validate and store the secret in the external secrets manager + creds, err := validateAndExtractCredentials(provider, location, credsJSON) + if err != nil { + return nil, NewErrValidation(err) + } + secretName, err := uc.credsRW.SaveCredentials(ctx, orgID, creds) if err != nil { return nil, fmt.Errorf("storing the credentials: %w", err) @@ -193,8 +204,51 @@ func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, location, descri }) } +// Update will update credentials, description or default status +func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id, description string, credsJSON []byte, defaultB bool) (*CASBackend, error) { + orgUUID, err := uuid.Parse(orgID) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + uuid, err := uuid.Parse(id) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + repo, err := uc.repo.FindByIDInOrg(ctx, orgUUID, uuid) + if err != nil { + return nil, err + } else if repo == nil { + return nil, NewErrNotFound("CAS Backend") + } + + var secretName string + // We want to rotate credentials + if credsJSON != nil { + // Validate and store the secret in the external secrets manager + creds, err := validateAndExtractCredentials(repo.Provider, repo.Location, credsJSON) + if err != nil { + return nil, NewErrValidation(err) + } + + secretName, err = uc.credsRW.SaveCredentials(ctx, orgID, creds) + if err != nil { + return nil, fmt.Errorf("storing the credentials: %w", err) + } + } + + return uc.repo.Update(ctx, &CASBackendUpdateOpts{ + ID: uuid, + CASBackendOpts: &CASBackendOpts{ + SecretName: secretName, Default: defaultB, Description: description, + }, + }) +} + // TODO(miguel): we need to think about the update mechanism and add some guardrails // for example, we might only allow updating credentials but not the repository itself or the provider +// Deprecated: use Create and update methods separately instead func (uc *CASBackendUseCase) CreateOrUpdate(ctx context.Context, orgID, name, username, password string, provider CASBackendProvider, defaultB bool) (*CASBackend, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { diff --git a/app/controlplane/internal/biz/mocks/CASBackendRepo.go b/app/controlplane/internal/biz/mocks/CASBackendRepo.go index 8bdd89bd6..b15c64829 100644 --- a/app/controlplane/internal/biz/mocks/CASBackendRepo.go +++ b/app/controlplane/internal/biz/mocks/CASBackendRepo.go @@ -83,6 +83,32 @@ func (_m *CASBackendRepo) FindByID(ctx context.Context, ID uuid.UUID) (*biz.CASB return r0, r1 } +// FindByIDInOrg provides a mock function with given fields: ctx, OrgID, ID +func (_m *CASBackendRepo) FindByIDInOrg(ctx context.Context, OrgID uuid.UUID, ID uuid.UUID) (*biz.CASBackend, error) { + ret := _m.Called(ctx, OrgID, ID) + + var r0 *biz.CASBackend + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) (*biz.CASBackend, error)); ok { + return rf(ctx, OrgID, ID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) *biz.CASBackend); ok { + r0 = rf(ctx, OrgID, ID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*biz.CASBackend) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, uuid.UUID) error); ok { + r1 = rf(ctx, OrgID, ID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FindDefaultBackend provides a mock function with given fields: ctx, orgID func (_m *CASBackendRepo) FindDefaultBackend(ctx context.Context, orgID uuid.UUID) (*biz.CASBackend, error) { ret := _m.Called(ctx, orgID) diff --git a/app/controlplane/internal/data/casbackend.go b/app/controlplane/internal/data/casbackend.go index dd78a8b97..5f13ba99a 100644 --- a/app/controlplane/internal/data/casbackend.go +++ b/app/controlplane/internal/data/casbackend.go @@ -107,11 +107,18 @@ func (r *CASBackendRepo) Create(ctx context.Context, opts *biz.CASBackendCreateO } func (r *CASBackendRepo) Update(ctx context.Context, opts *biz.CASBackendUpdateOpts) (*biz.CASBackend, error) { - backend, err := r.data.db.CASBackend.UpdateOneID(opts.ID). - SetDescription(opts.Description). - SetDefault(opts.Default). - SetSecretName(opts.SecretName). - Save(ctx) + updateChain := r.data.db.CASBackend.UpdateOneID(opts.ID).SetDefault(opts.Default) + // If description is provided we set it + if opts.Description != "" { + updateChain = updateChain.SetDescription(opts.Description) + } + + // If secretName is provided we set it + if opts.SecretName != "" { + updateChain = updateChain.SetSecretName(opts.SecretName) + } + + backend, err := updateChain.Save(ctx) if err != nil { return nil, err } @@ -119,7 +126,7 @@ func (r *CASBackendRepo) Update(ctx context.Context, opts *biz.CASBackendUpdateO return r.FindByID(ctx, backend.ID) } -// FindByID finds a CAS backend by ID in the given organization. +// FindByID finds a CAS backend by ID // If not found, returns nil and no error func (r *CASBackendRepo) FindByID(ctx context.Context, id uuid.UUID) (*biz.CASBackend, error) { backend, err := r.data.db.CASBackend.Query().WithOrganization().Where(casbackend.ID(id)).Only(ctx) @@ -132,6 +139,19 @@ func (r *CASBackendRepo) FindByID(ctx context.Context, id uuid.UUID) (*biz.CASBa return entCASBackendToBiz(backend), nil } +// FindByIDInOrg finds a CAS backend by ID in the given organization. +// If not found, returns nil and no error +func (r *CASBackendRepo) FindByIDInOrg(ctx context.Context, orgID, id uuid.UUID) (*biz.CASBackend, error) { + backend, err := orgScopedQuery(r.data.db, orgID).QueryCasBackends().WithOrganization().Where(casbackend.ID(id)).Only(ctx) + if err != nil && !ent.IsNotFound(err) { + return nil, err + } else if backend == nil { + return nil, nil + } + + return entCASBackendToBiz(backend), nil +} + func (r *CASBackendRepo) Delete(ctx context.Context, id uuid.UUID) error { return r.data.db.CASBackend.DeleteOneID(id).Exec(ctx) } diff --git a/app/controlplane/internal/service/casbackend.go b/app/controlplane/internal/service/casbackend.go index e05b85123..fe97738e0 100644 --- a/app/controlplane/internal/service/casbackend.go +++ b/app/controlplane/internal/service/casbackend.go @@ -64,10 +64,6 @@ func (s *CASBackendService) Create(ctx context.Context, req *pb.CASBackendServic return nil, err } - if err := req.ValidateAll(); err != nil { - return nil, errors.BadRequest("invalid config", err.Error()) - } - credsJSON, err := req.Credentials.MarshalJSON() if err != nil { return nil, errors.BadRequest("invalid config", "config is invalid") @@ -84,6 +80,35 @@ func (s *CASBackendService) Create(ctx context.Context, req *pb.CASBackendServic return &pb.CASBackendServiceCreateResponse{Result: bizOCASBackendToPb(res)}, nil } +func (s *CASBackendService) Update(ctx context.Context, req *pb.CASBackendServiceUpdateRequest) (*pb.CASBackendServiceUpdateResponse, error) { + _, currentOrg, err := loadCurrentUserAndOrg(ctx) + if err != nil { + return nil, err + } + + var credsJSON []byte + if req.Credentials != nil { + credsJSON, err = req.Credentials.MarshalJSON() + if err != nil { + return nil, errors.BadRequest("invalid config", "config is invalid") + } + } + + // For now we only support one backend which is set as default + res, err := s.uc.Update(ctx, currentOrg.ID, req.Id, req.Description, credsJSON, req.Default) + + switch { + case err != nil && biz.IsErrValidation(err): + return nil, errors.BadRequest("invalid CAS backend", err.Error()) + case err != nil && biz.IsNotFound(err): + return nil, errors.NotFound("CAS backend not found", err.Error()) + case err != nil: + return nil, sl.LogAndMaskErr(err, s.log) + } + + return &pb.CASBackendServiceUpdateResponse{Result: bizOCASBackendToPb(res)}, nil +} + func bizOCASBackendToPb(in *biz.CASBackend) *pb.CASBackendItem { r := &pb.CASBackendItem{ Id: in.ID.String(), Location: in.Location, Description: in.Description, diff --git a/internal/ociauth/auth.go b/internal/ociauth/auth.go index da3ed236b..19a36ca78 100644 --- a/internal/ociauth/auth.go +++ b/internal/ociauth/auth.go @@ -68,7 +68,7 @@ func (repo *Credentials) Authorization() (*authn.AuthConfig, error) { // They include a username, password and a valid (RFC 3986 URI authority) serverName func validateOCICredentials(c *Credentials) (authn.Keychain, error) { if c.username == "" || c.password == "" || c.server == "" { - return nil, errors.New("OCI credentials require an username, password and a server name") + return nil, errors.New("OCI credentials require a username, password and a server name") } // name.NewRepository parses incorrectly URIs with schemas