From 0815b6ac533aca8cae12691971ed5e675092a39f Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Thu, 12 Oct 2023 21:59:45 +0200 Subject: [PATCH 1/4] feat: Azure KeyVault support Signed-off-by: Miguel Martinez Trivino --- app/artifact-cas/configs/samples/config.yaml | 6 + app/controlplane/configs/samples/config.yaml | 8 +- app/controlplane/internal/biz/casbackend.go | 2 +- go.mod | 2 + go.sum | 4 + .../api/credentials/v1/config.pb.go | 218 ++++++++++++++---- .../api/credentials/v1/config.pb.validate.go | 189 +++++++++++++++ .../api/credentials/v1/config.proto | 12 + internal/credentials/aws/secretmanager.go | 4 +- internal/credentials/azurekv/keyvault.go | 207 +++++++++++++++++ internal/credentials/manager/manager.go | 28 +++ 11 files changed, 627 insertions(+), 53 deletions(-) create mode 100644 internal/credentials/azurekv/keyvault.go diff --git a/app/artifact-cas/configs/samples/config.yaml b/app/artifact-cas/configs/samples/config.yaml index c00c670ac..be3e16fd0 100644 --- a/app/artifact-cas/configs/samples/config.yaml +++ b/app/artifact-cas/configs/samples/config.yaml @@ -33,6 +33,12 @@ credentials_service: # project_id: 522312304548 # auth_key: "./configs/gcp_auth_key.json" + # azure_key_vault: + # tenant_id: AD-tenant-id + # client_id: Service Principal ID + # client_secret: Service Principal Secret + # vault_uri: https://myvault.vault.azure.net/ + observability: sentry: dsn: "http://sentryDomain" diff --git a/app/controlplane/configs/samples/config.yaml b/app/controlplane/configs/samples/config.yaml index e6bc3f23e..58c46b680 100644 --- a/app/controlplane/configs/samples/config.yaml +++ b/app/controlplane/configs/samples/config.yaml @@ -40,4 +40,10 @@ credentials_service: # gcp_secret_manager: # project_id: 522312304548 - # auth_key: "./configs/gcp_auth_key.json" \ No newline at end of file + # auth_key: "./configs/gcp_auth_key.json" + + # azure_key_vault: + # tenant_id: AD-tenant-id + # client_id: Service Principal ID + # client_secret: Service Principal Secret + # vault_uri: https://myvault.vault.azure.net/ \ No newline at end of file diff --git a/app/controlplane/internal/biz/casbackend.go b/app/controlplane/internal/biz/casbackend.go index c18df04ad..95a5b6d7b 100644 --- a/app/controlplane/internal/biz/casbackend.go +++ b/app/controlplane/internal/biz/casbackend.go @@ -386,7 +386,7 @@ func (uc *CASBackendUseCase) Delete(ctx context.Context, id string) error { uc.logger.Infow("msg", "deleting CAS backend external secrets", "ID", id, "secretName", backend.SecretName) // Delete the secret in the external secrets manager if err := uc.credsRW.DeleteCredentials(ctx, backend.SecretName); err != nil { - return fmt.Errorf("deleting the credentials: %w", err) + uc.logger.Errorw("msg", "deleting CAS backend external secrets", "ID", id, "secretName", backend.SecretName, "error", err) } uc.logger.Infow("msg", "CAS Backend deleted", "ID", id) diff --git a/go.mod b/go.mod index 9b0dd3339..9e5777d05 100644 --- a/go.mod +++ b/go.mod @@ -82,6 +82,7 @@ require ( cloud.google.com/go/pubsub v1.33.0 // indirect dario.cat/mergo v1.0.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 // indirect @@ -123,6 +124,7 @@ require ( cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect diff --git a/go.sum b/go.sum index e6b04ae7b..d300c6d92 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,10 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybI github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 h1:xnO4sFyG8UH2fElBkcqLTOZsAajvKfnSlgBBW8dXYjw= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0/go.mod h1:XD3DIOOVgBCO03OleB1fHjgktVRFxlT++KwKgIOewdM= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U= diff --git a/internal/credentials/api/credentials/v1/config.pb.go b/internal/credentials/api/credentials/v1/config.pb.go index b28a3908e..b840cf679 100644 --- a/internal/credentials/api/credentials/v1/config.pb.go +++ b/internal/credentials/api/credentials/v1/config.pb.go @@ -47,6 +47,7 @@ type Credentials struct { // *Credentials_AwsSecretManager // *Credentials_Vault_ // *Credentials_GcpSecretManager + // *Credentials_AzureKeyVault_ Backend isCredentials_Backend `protobuf_oneof:"backend"` // prefix used while writing a new secret SecretPrefix string `protobuf:"bytes,4,opt,name=secret_prefix,json=secretPrefix,proto3" json:"secret_prefix,omitempty"` @@ -112,6 +113,13 @@ func (x *Credentials) GetGcpSecretManager() *Credentials_GCPSecretManager { return nil } +func (x *Credentials) GetAzureKeyVault() *Credentials_AzureKeyVault { + if x, ok := x.GetBackend().(*Credentials_AzureKeyVault_); ok { + return x.AzureKeyVault + } + return nil +} + func (x *Credentials) GetSecretPrefix() string { if x != nil { return x.SecretPrefix @@ -135,12 +143,18 @@ type Credentials_GcpSecretManager struct { GcpSecretManager *Credentials_GCPSecretManager `protobuf:"bytes,3,opt,name=gcp_secret_manager,json=gcpSecretManager,proto3,oneof"` } +type Credentials_AzureKeyVault_ struct { + AzureKeyVault *Credentials_AzureKeyVault `protobuf:"bytes,5,opt,name=azure_key_vault,json=azureKeyVault,proto3,oneof"` +} + func (*Credentials_AwsSecretManager) isCredentials_Backend() {} func (*Credentials_Vault_) isCredentials_Backend() {} func (*Credentials_GcpSecretManager) isCredentials_Backend() {} +func (*Credentials_AzureKeyVault_) isCredentials_Backend() {} + // Top level is deprecated now type Credentials_AWSSecretManager struct { state protoimpl.MessageState @@ -321,6 +335,81 @@ func (x *Credentials_GCPSecretManager) GetServiceAccountKey() string { return "" } +type Credentials_AzureKeyVault struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Active Directory Tenant ID + TenantId string `protobuf:"bytes,1,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"` + // Registered application / service principal client ID + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + // Registered application / service principal client secret + ClientSecret string `protobuf:"bytes,3,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty"` + // Azure Key Vault URL + VaultUri string `protobuf:"bytes,4,opt,name=vault_uri,json=vaultUri,proto3" json:"vault_uri,omitempty"` +} + +func (x *Credentials_AzureKeyVault) Reset() { + *x = Credentials_AzureKeyVault{} + if protoimpl.UnsafeEnabled { + mi := &file_credentials_v1_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Credentials_AzureKeyVault) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Credentials_AzureKeyVault) ProtoMessage() {} + +func (x *Credentials_AzureKeyVault) ProtoReflect() protoreflect.Message { + mi := &file_credentials_v1_config_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 Credentials_AzureKeyVault.ProtoReflect.Descriptor instead. +func (*Credentials_AzureKeyVault) Descriptor() ([]byte, []int) { + return file_credentials_v1_config_proto_rawDescGZIP(), []int{0, 3} +} + +func (x *Credentials_AzureKeyVault) GetTenantId() string { + if x != nil { + return x.TenantId + } + return "" +} + +func (x *Credentials_AzureKeyVault) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *Credentials_AzureKeyVault) GetClientSecret() string { + if x != nil { + return x.ClientSecret + } + return "" +} + +func (x *Credentials_AzureKeyVault) GetVaultUri() string { + if x != nil { + return x.VaultUri + } + return "" +} + type Credentials_AWSSecretManager_Creds struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -333,7 +422,7 @@ type Credentials_AWSSecretManager_Creds struct { func (x *Credentials_AWSSecretManager_Creds) Reset() { *x = Credentials_AWSSecretManager_Creds{} if protoimpl.UnsafeEnabled { - mi := &file_credentials_v1_config_proto_msgTypes[4] + mi := &file_credentials_v1_config_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -346,7 +435,7 @@ func (x *Credentials_AWSSecretManager_Creds) String() string { func (*Credentials_AWSSecretManager_Creds) ProtoMessage() {} func (x *Credentials_AWSSecretManager_Creds) ProtoReflect() protoreflect.Message { - mi := &file_credentials_v1_config_proto_msgTypes[4] + mi := &file_credentials_v1_config_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -383,7 +472,7 @@ var file_credentials_v1_config_proto_rawDesc = []byte{ 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf2, 0x05, 0x0a, 0x0b, 0x43, 0x72, 0x65, 0x64, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfa, 0x07, 0x0a, 0x0b, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x12, 0x5c, 0x0a, 0x12, 0x61, 0x77, 0x73, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, @@ -399,44 +488,60 @@ var file_credentials_v1_config_proto_rawDesc = []byte{ 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x47, 0x43, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x48, 0x00, 0x52, 0x10, 0x67, 0x63, 0x70, - 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x23, 0x0a, - 0x0d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x50, 0x72, 0x65, 0x66, - 0x69, 0x78, 0x1a, 0xe0, 0x01, 0x0a, 0x10, 0x41, 0x57, 0x53, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x52, 0x0a, 0x05, 0x63, 0x72, 0x65, 0x64, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x53, 0x0a, + 0x0f, 0x61, 0x7a, 0x75, 0x72, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x76, 0x61, 0x75, 0x6c, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x61, 0x6c, 0x73, 0x2e, 0x41, 0x57, 0x53, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x72, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x73, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, - 0x01, 0x02, 0x10, 0x01, 0x52, 0x05, 0x63, 0x72, 0x65, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x06, 0x72, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, - 0x72, 0x02, 0x10, 0x01, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x1a, 0x57, 0x0a, 0x05, - 0x43, 0x72, 0x65, 0x64, 0x73, 0x12, 0x26, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, - 0x10, 0x01, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, - 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x4b, 0x65, 0x79, 0x1a, 0x68, 0x0a, 0x05, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x1d, - 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, - 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, - 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, - 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x1a, - 0x6a, 0x0a, 0x10, 0x47, 0x43, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, - 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x13, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x42, 0x0e, 0x0a, 0x07, 0x62, - 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x03, 0xf8, 0x42, 0x01, 0x42, 0x4f, 0x5a, 0x4d, 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, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x6c, 0x73, 0x2e, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x75, 0x6c, + 0x74, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x7a, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x75, + 0x6c, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x1a, 0xe0, 0x01, 0x0a, 0x10, 0x41, 0x57, 0x53, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x52, 0x0a, 0x05, + 0x63, 0x72, 0x65, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x63, 0x72, + 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x41, 0x57, 0x53, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x73, 0x42, + 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, 0x10, 0x01, 0x52, 0x05, 0x63, 0x72, 0x65, 0x64, 0x73, + 0x12, 0x1f, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x1a, 0x57, 0x0a, 0x05, 0x43, 0x72, 0x65, 0x64, 0x73, 0x12, 0x26, 0x0a, 0x0a, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, + 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, + 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x1a, 0x68, 0x0a, 0x05, 0x56, 0x61, + 0x75, 0x6c, 0x74, 0x12, 0x1d, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x07, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, + 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x50, 0x61, 0x74, 0x68, 0x1a, 0x6a, 0x0a, 0x10, 0x47, 0x43, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, + 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4b, 0x65, 0x79, + 0x1a, 0xb0, 0x01, 0x0a, 0x0d, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x75, + 0x6c, 0x74, 0x12, 0x24, 0x0a, 0x09, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x08, + 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, + 0x72, 0x02, 0x10, 0x01, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x2c, + 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0c, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x25, 0x0a, 0x09, + 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x76, 0x61, 0x75, 0x6c, 0x74, + 0x55, 0x72, 0x69, 0x42, 0x0e, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x03, + 0xf8, 0x42, 0x01, 0x42, 0x4f, 0x5a, 0x4d, 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, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2f, 0x76, + 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -451,24 +556,26 @@ func file_credentials_v1_config_proto_rawDescGZIP() []byte { return file_credentials_v1_config_proto_rawDescData } -var file_credentials_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_credentials_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_credentials_v1_config_proto_goTypes = []interface{}{ (*Credentials)(nil), // 0: credentials.v1.Credentials (*Credentials_AWSSecretManager)(nil), // 1: credentials.v1.Credentials.AWSSecretManager (*Credentials_Vault)(nil), // 2: credentials.v1.Credentials.Vault (*Credentials_GCPSecretManager)(nil), // 3: credentials.v1.Credentials.GCPSecretManager - (*Credentials_AWSSecretManager_Creds)(nil), // 4: credentials.v1.Credentials.AWSSecretManager.Creds + (*Credentials_AzureKeyVault)(nil), // 4: credentials.v1.Credentials.AzureKeyVault + (*Credentials_AWSSecretManager_Creds)(nil), // 5: credentials.v1.Credentials.AWSSecretManager.Creds } var file_credentials_v1_config_proto_depIdxs = []int32{ 1, // 0: credentials.v1.Credentials.aws_secret_manager:type_name -> credentials.v1.Credentials.AWSSecretManager 2, // 1: credentials.v1.Credentials.vault:type_name -> credentials.v1.Credentials.Vault 3, // 2: credentials.v1.Credentials.gcp_secret_manager:type_name -> credentials.v1.Credentials.GCPSecretManager - 4, // 3: credentials.v1.Credentials.AWSSecretManager.creds:type_name -> credentials.v1.Credentials.AWSSecretManager.Creds - 4, // [4:4] is the sub-list for method output_type - 4, // [4:4] 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 + 4, // 3: credentials.v1.Credentials.azure_key_vault:type_name -> credentials.v1.Credentials.AzureKeyVault + 5, // 4: credentials.v1.Credentials.AWSSecretManager.creds:type_name -> credentials.v1.Credentials.AWSSecretManager.Creds + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] 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_credentials_v1_config_proto_init() } @@ -526,6 +633,18 @@ func file_credentials_v1_config_proto_init() { } } file_credentials_v1_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Credentials_AzureKeyVault); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_credentials_v1_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Credentials_AWSSecretManager_Creds); i { case 0: return &v.state @@ -542,6 +661,7 @@ func file_credentials_v1_config_proto_init() { (*Credentials_AwsSecretManager)(nil), (*Credentials_Vault_)(nil), (*Credentials_GcpSecretManager)(nil), + (*Credentials_AzureKeyVault_)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -549,7 +669,7 @@ func file_credentials_v1_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_credentials_v1_config_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/credentials/api/credentials/v1/config.pb.validate.go b/internal/credentials/api/credentials/v1/config.pb.validate.go index 68e3bf48b..daf8769e1 100644 --- a/internal/credentials/api/credentials/v1/config.pb.validate.go +++ b/internal/credentials/api/credentials/v1/config.pb.validate.go @@ -187,6 +187,48 @@ func (m *Credentials) validate(all bool) error { } } + case *Credentials_AzureKeyVault_: + if v == nil { + err := CredentialsValidationError{ + field: "Backend", + reason: "oneof value cannot be a typed-nil", + } + if !all { + return err + } + errors = append(errors, err) + } + oneofBackendPresent = true + + if all { + switch v := interface{}(m.GetAzureKeyVault()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CredentialsValidationError{ + field: "AzureKeyVault", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CredentialsValidationError{ + field: "AzureKeyVault", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetAzureKeyVault()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CredentialsValidationError{ + field: "AzureKeyVault", + reason: "embedded message failed validation", + cause: err, + } + } + } + default: _ = v // ensures v is used } @@ -674,6 +716,153 @@ var _ interface { ErrorName() string } = Credentials_GCPSecretManagerValidationError{} +// Validate checks the field values on Credentials_AzureKeyVault 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 *Credentials_AzureKeyVault) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on Credentials_AzureKeyVault 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 +// Credentials_AzureKeyVaultMultiError, or nil if none found. +func (m *Credentials_AzureKeyVault) ValidateAll() error { + return m.validate(true) +} + +func (m *Credentials_AzureKeyVault) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if utf8.RuneCountInString(m.GetTenantId()) < 1 { + err := Credentials_AzureKeyVaultValidationError{ + field: "TenantId", + reason: "value length must be at least 1 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + if utf8.RuneCountInString(m.GetClientId()) < 1 { + err := Credentials_AzureKeyVaultValidationError{ + field: "ClientId", + reason: "value length must be at least 1 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + if utf8.RuneCountInString(m.GetClientSecret()) < 1 { + err := Credentials_AzureKeyVaultValidationError{ + field: "ClientSecret", + reason: "value length must be at least 1 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + if _, err := url.Parse(m.GetVaultUri()); err != nil { + err = Credentials_AzureKeyVaultValidationError{ + field: "VaultUri", + reason: "value must be a valid URI", + cause: err, + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return Credentials_AzureKeyVaultMultiError(errors) + } + + return nil +} + +// Credentials_AzureKeyVaultMultiError is an error wrapping multiple validation +// errors returned by Credentials_AzureKeyVault.ValidateAll() if the +// designated constraints aren't met. +type Credentials_AzureKeyVaultMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m Credentials_AzureKeyVaultMultiError) 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 Credentials_AzureKeyVaultMultiError) AllErrors() []error { return m } + +// Credentials_AzureKeyVaultValidationError is the validation error returned by +// Credentials_AzureKeyVault.Validate if the designated constraints aren't met. +type Credentials_AzureKeyVaultValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e Credentials_AzureKeyVaultValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e Credentials_AzureKeyVaultValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e Credentials_AzureKeyVaultValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e Credentials_AzureKeyVaultValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e Credentials_AzureKeyVaultValidationError) ErrorName() string { + return "Credentials_AzureKeyVaultValidationError" +} + +// Error satisfies the builtin error interface +func (e Credentials_AzureKeyVaultValidationError) 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 %sCredentials_AzureKeyVault.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = Credentials_AzureKeyVaultValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = Credentials_AzureKeyVaultValidationError{} + // Validate checks the field values on Credentials_AWSSecretManager_Creds 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 diff --git a/internal/credentials/api/credentials/v1/config.proto b/internal/credentials/api/credentials/v1/config.proto index ed70f45fa..01579bf37 100644 --- a/internal/credentials/api/credentials/v1/config.proto +++ b/internal/credentials/api/credentials/v1/config.proto @@ -27,6 +27,7 @@ message Credentials { AWSSecretManager aws_secret_manager = 1; Vault vault = 2; GCPSecretManager gcp_secret_manager = 3; + AzureKeyVault azure_key_vault = 5; } // prefix used while writing a new secret @@ -59,4 +60,15 @@ message Credentials { // path to service account key in json format string service_account_key = 2; } + + message AzureKeyVault { + // Active Directory Tenant ID + string tenant_id = 1 [(validate.rules).string.min_len = 1]; + // Registered application / service principal client ID + string client_id = 2 [(validate.rules).string.min_len = 1]; + // Registered application / service principal client secret + string client_secret = 3 [(validate.rules).string.min_len = 1]; + // Azure Key Vault URL + string vault_uri = 4 [(validate.rules).string.uri_ref = true]; + } } \ No newline at end of file diff --git a/internal/credentials/aws/secretmanager.go b/internal/credentials/aws/secretmanager.go index a53b1f8d7..09633b39e 100644 --- a/internal/credentials/aws/secretmanager.go +++ b/internal/credentials/aws/secretmanager.go @@ -31,7 +31,7 @@ import ( "github.com/aws/smithy-go" "github.com/chainloop-dev/chainloop/internal/credentials" "github.com/chainloop-dev/chainloop/internal/servicelogger" - "github.com/docker/distribution/uuid" + "github.com/google/uuid" "github.com/go-kratos/kratos/v2/log" ) @@ -87,7 +87,7 @@ func NewManager(opts *NewManagerOpts) (*Manager, error) { // Save Credentials, this is a generic function that can be used to save any type of credentials // as long as they can be passed to json.Marshal func (m *Manager) SaveCredentials(ctx context.Context, orgID string, creds any) (string, error) { - secretName := strings.Join([]string{m.secretPrefix, orgID, uuid.Generate().String()}, "/") + secretName := strings.Join([]string{m.secretPrefix, orgID, uuid.New().String()}, "/") // Store the credentials as json key pairs c, err := json.Marshal(creds) diff --git a/internal/credentials/azurekv/keyvault.go b/internal/credentials/azurekv/keyvault.go new file mode 100644 index 000000000..8cde5dc4c --- /dev/null +++ b/internal/credentials/azurekv/keyvault.go @@ -0,0 +1,207 @@ +// +// 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 azurekv + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" + "github.com/chainloop-dev/chainloop/internal/credentials" + "github.com/google/uuid" + + "github.com/chainloop-dev/chainloop/internal/servicelogger" + "github.com/go-kratos/kratos/v2/log" +) + +const ( + healthCheckSecret = "chainloop-healthcheck" + healthCheckNonExisting = "chainloop-non-existing" +) + +type Manager struct { + client *azsecrets.Client + secretPrefix string + logger *log.Helper +} + +type NewManagerOpts struct { + // Active Directory Tenant ID + TenantID string + // Registered application / service principal client ID + ClientID string + // Registered application / service principal client secret + ClientSecret string + // Vault URL + VaultURI string + // Optional secret prefix + SecretPrefix string + Logger log.Logger + Role credentials.Role +} + +var ErrValidation = errors.New("credentials validation error") + +func (o *NewManagerOpts) Validate() error { + if o.TenantID == "" { + return fmt.Errorf("%w: missing tenant ID", ErrValidation) + } + + if o.ClientID == "" { + return fmt.Errorf("%w: missing client ID", ErrValidation) + } + + if o.ClientSecret == "" { + return fmt.Errorf("%w: missing client secret", ErrValidation) + } + + if o.VaultURI == "" { + return fmt.Errorf("%w: missing VAULT URI", ErrValidation) + } + + return nil +} + +func NewManager(opts *NewManagerOpts) (*Manager, error) { + l := opts.Logger + if l == nil { + l = log.NewStdLogger(io.Discard) + } + + logger := servicelogger.ScopedHelper(l, "credentials/azure-key-vault") + logger.Infow("msg", "configuring Azure KeyVault", "URI", opts.VaultURI, "role", opts.Role, "prefix", opts.SecretPrefix) + + if err := opts.Validate(); err != nil { + return nil, fmt.Errorf("invalid credentials: %w", err) + } + + credential, err := azidentity.NewClientSecretCredential(opts.TenantID, opts.ClientID, opts.ClientSecret, nil) + if err != nil { + return nil, fmt.Errorf("failed to create Azure Service principal Credential: %w", err) + } + + // Establish a connection to the Key Vault client + client, err := azsecrets.NewClient(opts.VaultURI, credential, nil) + if err != nil { + log.Fatalf("failed to create a client: %v", err) + } + + if opts.Role == credentials.RoleReader { + if err := validateReaderClient(client, opts.SecretPrefix); err != nil { + return nil, fmt.Errorf("validating client: %w", err) + } + } else { + if err := validateWriterClient(client, opts.SecretPrefix); err != nil { + return nil, fmt.Errorf("validating client: %w", err) + } + } + + logger.Infow("msg", "Azure KeyVault configured", "URI", opts.VaultURI, "role", opts.Role, "prefix", opts.SecretPrefix) + + return &Manager{ + secretPrefix: opts.SecretPrefix, + client: client, + logger: logger, + }, nil +} + +// SaveCredentials saves credentials +func (m *Manager) SaveCredentials(ctx context.Context, orgID string, creds any) (string, error) { + secretName := strings.Join([]string{m.secretPrefix, orgID, uuid.New().String()}, "-") + // Store the credentials as json key pairs + c, err := json.Marshal(creds) + if err != nil { + return "", fmt.Errorf("marshaling credentials to be stored: %w", err) + } + + if _, err := m.client.SetSecret(ctx, secretName, azsecrets.SetSecretParameters{Value: strPtr(string(c))}, nil); err != nil { + return "", fmt.Errorf("failed to set secret: %w", err) + } + + return secretName, nil +} + +// ReadCredentials reads the latest version of the credentials +func (m *Manager) ReadCredentials(ctx context.Context, secretName string, creds any) error { + // retrieve latest version of the secret + resp, err := m.client.GetSecret(ctx, secretName, "", nil) + var respErr *azcore.ResponseError + if err != nil { + if errors.As(err, &respErr) && respErr.StatusCode == 404 { + return fmt.Errorf("%w: path=%s", credentials.ErrNotFound, secretName) + } + + return fmt.Errorf("failed to get secret: %w", err) + } + + return json.Unmarshal([]byte(*resp.Value), creds) +} + +// DeleteCredentials deletes credentials and versions +func (m *Manager) DeleteCredentials(ctx context.Context, secretName string) error { + _, err := m.client.DeleteSecret(ctx, secretName, nil) + if err != nil { + return fmt.Errorf("failed to delete secret: %w", err) + } + + return nil +} + +// validateWriterClient checks if the client is valid by writing and deleting a secret +// in the provided mount path. +func validateWriterClient(client *azsecrets.Client, pathPrefix string) error { + secretName := strings.Join([]string{pathPrefix, healthCheckSecret, uuid.New().String()}, "-") + + ctx := context.Background() + if _, err := client.SetSecret(ctx, secretName, azsecrets.SetSecretParameters{Value: strPtr("")}, nil); err != nil { + return fmt.Errorf("failed to set secret: %w", err) + } + + if _, err := client.DeleteSecret(ctx, secretName, nil); err != nil { + return fmt.Errorf("failed to delete secret: %w", err) + } + + return nil +} + +// string to pointer +func strPtr(s string) *string { + return &s +} + +func validateReaderClient(client *azsecrets.Client, pathPrefix string) error { + // try to retrieve a non-existing key + // if we get 404 means that we have permissions to read in that path + secretName := strings.Join([]string{pathPrefix, healthCheckNonExisting}, "-") + _, err := client.GetSecret(context.Background(), secretName, "", nil) + var respErr *azcore.ResponseError + if err != nil { + if errors.As(err, &respErr) && respErr.StatusCode == 404 { + // Everything is ok + return nil + } + + return fmt.Errorf("failed to get secret: %w", err) + } + + return nil +} diff --git a/internal/credentials/manager/manager.go b/internal/credentials/manager/manager.go index 66fff3e5f..300db095e 100644 --- a/internal/credentials/manager/manager.go +++ b/internal/credentials/manager/manager.go @@ -23,6 +23,7 @@ import ( "github.com/chainloop-dev/chainloop/internal/credentials" api "github.com/chainloop-dev/chainloop/internal/credentials/api/credentials/v1" "github.com/chainloop-dev/chainloop/internal/credentials/aws" + "github.com/chainloop-dev/chainloop/internal/credentials/azurekv" "github.com/chainloop-dev/chainloop/internal/credentials/gcp" "github.com/chainloop-dev/chainloop/internal/credentials/vault" "github.com/go-kratos/kratos/v2/log" @@ -45,9 +46,36 @@ func NewFromConfig(conf *api.Credentials, role credentials.Role, l log.Logger) ( return newVaultCredentialsManager(vaultc, conf.SecretPrefix, role, l) } + if creds := conf.GetAzureKeyVault(); creds != nil { + return newAzureKBManager(creds, conf.SecretPrefix, role, l) + } + return nil, errors.New("no credentials manager configuration found") } +func newAzureKBManager(conf *api.Credentials_AzureKeyVault, prefix string, r credentials.Role, l log.Logger) (*azurekv.Manager, error) { + if err := conf.ValidateAll(); err != nil { + return nil, fmt.Errorf("uncompleted configuration for Azure Key Vault: %w", err) + } + + opts := &azurekv.NewManagerOpts{ + TenantID: conf.GetTenantId(), + ClientID: conf.GetClientId(), + ClientSecret: conf.GetClientSecret(), + VaultURI: conf.GetVaultUri(), + Logger: l, + SecretPrefix: prefix, + Role: r, + } + + m, err := azurekv.NewManager(opts) + if err != nil { + return nil, fmt.Errorf("configuring the secrets manager: %w", err) + } + + return m, nil +} + func newAWSCredentialsManager(conf *api.Credentials_AWSSecretManager, prefix string, r credentials.Role, l log.Logger) (*aws.Manager, error) { if err := conf.ValidateAll(); err != nil { return nil, fmt.Errorf("uncompleted configuration for AWS secret manager: %w", err) From e66fc398b8320173393ebc64fda6f874092bb303 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 13 Oct 2023 17:25:02 +0200 Subject: [PATCH 2/4] chore: add tests Signed-off-by: Miguel Martinez Trivino --- app/controlplane/internal/biz/organization.go | 17 +- internal/credentials/azurekv/keyvault.go | 36 ++- internal/credentials/azurekv/keyvault_test.go | 228 ++++++++++++++++++ .../credentials/azurekv/mocks/SecretsRW.go | 103 ++++++++ internal/credentials/manager/manager.go | 10 + 5 files changed, 364 insertions(+), 30 deletions(-) create mode 100644 internal/credentials/azurekv/keyvault_test.go create mode 100644 internal/credentials/azurekv/mocks/SecretsRW.go diff --git a/app/controlplane/internal/biz/organization.go b/app/controlplane/internal/biz/organization.go index eb6056336..f9f293604 100644 --- a/app/controlplane/internal/biz/organization.go +++ b/app/controlplane/internal/biz/organization.go @@ -17,6 +17,7 @@ package biz import ( "context" + "fmt" "time" "github.com/go-kratos/kratos/v2/log" @@ -101,18 +102,14 @@ func (uc *OrganizationUseCase) Delete(ctx context.Context, id string) error { } } - // Delete the associated repository - // Currently there is only one repository per organization - ociRepository, err := uc.casBackendUseCase.FindDefaultBackend(ctx, org.ID) - if err != nil && !IsNotFound(err) { - return err + backends, err := uc.casBackendUseCase.List(ctx, org.ID) + if err != nil { + return fmt.Errorf("failed to list backends: %w", err) } - if ociRepository != nil { - // We make sure to call the OCI repository use case to delete the repository - // including the external secret - if err := uc.casBackendUseCase.Delete(ctx, ociRepository.ID.String()); err != nil { - return err + for _, b := range backends { + if err := uc.casBackendUseCase.Delete(ctx, b.ID.String()); err != nil { + return fmt.Errorf("failed to delete backend: %w", err) } } diff --git a/internal/credentials/azurekv/keyvault.go b/internal/credentials/azurekv/keyvault.go index 8cde5dc4c..a1ed87d72 100644 --- a/internal/credentials/azurekv/keyvault.go +++ b/internal/credentials/azurekv/keyvault.go @@ -39,11 +39,17 @@ const ( ) type Manager struct { - client *azsecrets.Client + client SecretsRW secretPrefix string logger *log.Helper } +type SecretsRW interface { + SetSecret(ctx context.Context, secretName string, params azsecrets.SetSecretParameters, options *azsecrets.SetSecretOptions) (azsecrets.SetSecretResponse, error) + GetSecret(ctx context.Context, secretName string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) + DeleteSecret(ctx context.Context, secretName string, options *azsecrets.DeleteSecretOptions) (azsecrets.DeleteSecretResponse, error) +} + type NewManagerOpts struct { // Active Directory Tenant ID TenantID string @@ -105,16 +111,6 @@ func NewManager(opts *NewManagerOpts) (*Manager, error) { log.Fatalf("failed to create a client: %v", err) } - if opts.Role == credentials.RoleReader { - if err := validateReaderClient(client, opts.SecretPrefix); err != nil { - return nil, fmt.Errorf("validating client: %w", err) - } - } else { - if err := validateWriterClient(client, opts.SecretPrefix); err != nil { - return nil, fmt.Errorf("validating client: %w", err) - } - } - logger.Infow("msg", "Azure KeyVault configured", "URI", opts.VaultURI, "role", opts.Role, "prefix", opts.SecretPrefix) return &Manager{ @@ -166,17 +162,17 @@ func (m *Manager) DeleteCredentials(ctx context.Context, secretName string) erro return nil } -// validateWriterClient checks if the client is valid by writing and deleting a secret +// ValidateWriterClient checks if the client is valid by writing and deleting a secret // in the provided mount path. -func validateWriterClient(client *azsecrets.Client, pathPrefix string) error { - secretName := strings.Join([]string{pathPrefix, healthCheckSecret, uuid.New().String()}, "-") +func ValidateWriterClient(m *Manager, pathPrefix string) error { + secretName := strings.Join([]string{pathPrefix, healthCheckSecret, uuid.NewString()}, "-") ctx := context.Background() - if _, err := client.SetSecret(ctx, secretName, azsecrets.SetSecretParameters{Value: strPtr("")}, nil); err != nil { + if _, err := m.client.SetSecret(ctx, secretName, azsecrets.SetSecretParameters{Value: strPtr("")}, nil); err != nil { return fmt.Errorf("failed to set secret: %w", err) } - if _, err := client.DeleteSecret(ctx, secretName, nil); err != nil { + if _, err := m.client.DeleteSecret(ctx, secretName, nil); err != nil { return fmt.Errorf("failed to delete secret: %w", err) } @@ -188,11 +184,11 @@ func strPtr(s string) *string { return &s } -func validateReaderClient(client *azsecrets.Client, pathPrefix string) error { +func ValidateReaderClient(m *Manager, pathPrefix string) error { // try to retrieve a non-existing key // if we get 404 means that we have permissions to read in that path - secretName := strings.Join([]string{pathPrefix, healthCheckNonExisting}, "-") - _, err := client.GetSecret(context.Background(), secretName, "", nil) + secretName := strings.Join([]string{pathPrefix, healthCheckNonExisting, uuid.NewString()}, "-") + _, err := m.client.GetSecret(context.Background(), secretName, "", nil) var respErr *azcore.ResponseError if err != nil { if errors.As(err, &respErr) && respErr.StatusCode == 404 { @@ -203,5 +199,5 @@ func validateReaderClient(client *azsecrets.Client, pathPrefix string) error { return fmt.Errorf("failed to get secret: %w", err) } - return nil + return errors.New("expected error") } diff --git a/internal/credentials/azurekv/keyvault_test.go b/internal/credentials/azurekv/keyvault_test.go new file mode 100644 index 000000000..edb838a91 --- /dev/null +++ b/internal/credentials/azurekv/keyvault_test.go @@ -0,0 +1,228 @@ +// +// 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 azurekv + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" + "github.com/chainloop-dev/chainloop/internal/credentials" + "github.com/chainloop-dev/chainloop/internal/credentials/azurekv/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func (s *testSuite) TestNewManager() { + testCases := []struct { + name string + tenantID string + clientID string + clientSecret string + vaultURI string + Role credentials.Role + expectedError bool + }{ + {name: "missing tenantID", tenantID: "", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, + {name: "missing clientID", tenantID: "tenantID", clientID: "", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, + {name: "missing clientSecret", tenantID: "tenantID", clientID: "clientID", clientSecret: "", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, + {name: "missing vaultURI", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "", Role: credentials.RoleReader, expectedError: true}, + {name: "valid reader configuration", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader}, + {name: "valid writer configuration", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleWriter}, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + opts := &NewManagerOpts{TenantID: tc.tenantID, ClientID: tc.clientID, ClientSecret: tc.clientSecret, VaultURI: tc.vaultURI, Role: tc.Role} + _, err := NewManager(opts) + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func (s *testSuite) TestValidateWriterClient() { + s.Run("happy path", func() { + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("SetSecret", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(azsecrets.SetSecretResponse{}, nil) + secretsRW.On("DeleteSecret", mock.Anything, mock.Anything, mock.Anything).Return(azsecrets.DeleteSecretResponse{}, nil) + s.NoError(ValidateWriterClient(&Manager{client: secretsRW}, "prefix")) + }) + + s.Run("can't write", func() { + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("SetSecret", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(azsecrets.SetSecretResponse{}, errors.New("can't write")) + s.Error(ValidateWriterClient(&Manager{client: secretsRW}, "prefix")) + }) + + s.Run("can't delete", func() { + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("SetSecret", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(azsecrets.SetSecretResponse{}, nil) + secretsRW.On("DeleteSecret", mock.Anything, mock.Anything, mock.Anything).Return(azsecrets.DeleteSecretResponse{}, errors.New("can't delete")) + s.Error(ValidateWriterClient(&Manager{client: secretsRW}, "prefix")) + }) +} + +func (s *testSuite) TestValidateReaderClient() { + s.Run("the secret is found means error", func() { + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("GetSecret", mock.Anything, mock.Anything, "", mock.Anything).Return(azsecrets.GetSecretResponse{}, nil) + s.Error(ValidateReaderClient(&Manager{client: secretsRW}, "prefix")) + }) + + s.Run("secret not found but can read", func() { + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("GetSecret", mock.Anything, mock.Anything, "", mock.Anything). + Return(azsecrets.GetSecretResponse{}, &azcore.ResponseError{StatusCode: 404}) + s.NoError(ValidateReaderClient(&Manager{client: secretsRW}, "prefix")) + }) + + s.Run("can't read", func() { + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("GetSecret", mock.Anything, mock.Anything, "", mock.Anything). + Return(azsecrets.GetSecretResponse{}, &azcore.ResponseError{StatusCode: 401}) + s.Error(ValidateReaderClient(&Manager{client: secretsRW}, "prefix")) + }) +} + +func (s *testSuite) TestDeleteCredentials() { + s.Run("happy path", func() { + ctx := context.Background() + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("DeleteSecret", ctx, "my-secret", mock.Anything).Return(azsecrets.DeleteSecretResponse{}, nil) + s.NoError((&Manager{client: secretsRW}).DeleteCredentials(ctx, "my-secret")) + }) + + s.Run("can't delete", func() { + ctx := context.Background() + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("DeleteSecret", ctx, "my-secret", mock.Anything).Return(azsecrets.DeleteSecretResponse{}, errors.New("can't delete")) + s.Error((&Manager{client: secretsRW}).DeleteCredentials(ctx, "my-secret")) + }) +} + +func (s *testSuite) TestSaveCredentials() { + creds := &credentials.APICreds{ + Host: "myhost", + Key: "mykey", + } + + toStoreCreds, err := json.Marshal(creds) + s.NoError(err) + + s.Run("happy path", func() { + ctx := context.Background() + secretsRW := mocks.NewSecretsRW(s.T()) + var want string + secretsRW.On("SetSecret", ctx, mock.Anything, azsecrets.SetSecretParameters{Value: strPtr(string(toStoreCreds))}, mock.Anything). + Return(azsecrets.SetSecretResponse{}, nil).Run(func(args mock.Arguments) { + want = args.Get(1).(string) + }) + + m := &Manager{client: secretsRW, secretPrefix: "my-prefix"} + got, err := m.SaveCredentials(ctx, "my-org", creds) + s.NoError(err) + s.Equal(want, got) + }) + + s.Run("can't save", func() { + ctx := context.Background() + secretsRW := mocks.NewSecretsRW(s.T()) + upstreamErr := errors.New("upstream error") + secretsRW.On("SetSecret", ctx, mock.Anything, azsecrets.SetSecretParameters{Value: strPtr(string(toStoreCreds))}, mock.Anything). + Return(azsecrets.SetSecretResponse{}, upstreamErr) + + m := &Manager{client: secretsRW, secretPrefix: "my-prefix"} + got, err := m.SaveCredentials(ctx, "my-org", creds) + s.ErrorIs(err, upstreamErr) + s.Empty(got) + }) +} + +func (s *testSuite) TestReadCredentials() { + want := &credentials.APICreds{ + Host: "myhost", + Key: "mykey", + } + + wantRaw, err := json.Marshal(want) + s.NoError(err) + + s.Run("happy path", func() { + ctx := context.Background() + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("GetSecret", mock.Anything, mock.Anything, "", mock.Anything). + Return(azsecrets.GetSecretResponse{ + SecretBundle: azsecrets.SecretBundle{ + Value: strPtr(string(wantRaw)), + }, + }, nil) + + m := &Manager{client: secretsRW} + got := &credentials.APICreds{} + s.NoError(m.ReadCredentials(ctx, "my-secret", got)) + s.Equal(want, got) + }) + + s.Run("not found", func() { + ctx := context.Background() + secretsRW := mocks.NewSecretsRW(s.T()) + secretsRW.On("GetSecret", mock.Anything, mock.Anything, "", mock.Anything). + Return(azsecrets.GetSecretResponse{}, &azcore.ResponseError{StatusCode: 404}) + + m := &Manager{client: secretsRW} + got := &credentials.APICreds{} + err := m.ReadCredentials(ctx, "my-secret", got) + s.Error(err) + s.ErrorIs(err, credentials.ErrNotFound) + }) + + s.Run("other error", func() { + ctx := context.Background() + secretsRW := mocks.NewSecretsRW(s.T()) + upstreamErr := errors.New("upstream error") + secretsRW.On("GetSecret", mock.Anything, mock.Anything, "", mock.Anything). + Return(azsecrets.GetSecretResponse{}, upstreamErr) + + m := &Manager{client: secretsRW} + got := &credentials.APICreds{} + err := m.ReadCredentials(ctx, "my-secret", got) + s.Error(err) + s.ErrorIs(err, upstreamErr) + }) +} + +type testSuite struct { + suite.Suite + secretsRW *mocks.SecretsRW + m *Manager +} + +func (s *testSuite) SetupTest() { + s.secretsRW = mocks.NewSecretsRW(s.T()) + s.m = &Manager{client: s.secretsRW} +} + +func TestSuite(t *testing.T) { + suite.Run(t, new(testSuite)) +} diff --git a/internal/credentials/azurekv/mocks/SecretsRW.go b/internal/credentials/azurekv/mocks/SecretsRW.go new file mode 100644 index 000000000..e32dff4dd --- /dev/null +++ b/internal/credentials/azurekv/mocks/SecretsRW.go @@ -0,0 +1,103 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + azsecrets "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" + + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// SecretsRW is an autogenerated mock type for the SecretsRW type +type SecretsRW struct { + mock.Mock +} + +// DeleteSecret provides a mock function with given fields: ctx, secretName, options +func (_m *SecretsRW) DeleteSecret(ctx context.Context, secretName string, options *azsecrets.DeleteSecretOptions) (azsecrets.DeleteSecretResponse, error) { + ret := _m.Called(ctx, secretName, options) + + var r0 azsecrets.DeleteSecretResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *azsecrets.DeleteSecretOptions) (azsecrets.DeleteSecretResponse, error)); ok { + return rf(ctx, secretName, options) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *azsecrets.DeleteSecretOptions) azsecrets.DeleteSecretResponse); ok { + r0 = rf(ctx, secretName, options) + } else { + r0 = ret.Get(0).(azsecrets.DeleteSecretResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *azsecrets.DeleteSecretOptions) error); ok { + r1 = rf(ctx, secretName, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSecret provides a mock function with given fields: ctx, secretName, version, options +func (_m *SecretsRW) GetSecret(ctx context.Context, secretName string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + ret := _m.Called(ctx, secretName, version, options) + + var r0 azsecrets.GetSecretResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error)); ok { + return rf(ctx, secretName, version, options) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, *azsecrets.GetSecretOptions) azsecrets.GetSecretResponse); ok { + r0 = rf(ctx, secretName, version, options) + } else { + r0 = ret.Get(0).(azsecrets.GetSecretResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, *azsecrets.GetSecretOptions) error); ok { + r1 = rf(ctx, secretName, version, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetSecret provides a mock function with given fields: ctx, secretName, params, options +func (_m *SecretsRW) SetSecret(ctx context.Context, secretName string, params azsecrets.SetSecretParameters, options *azsecrets.SetSecretOptions) (azsecrets.SetSecretResponse, error) { + ret := _m.Called(ctx, secretName, params, options) + + var r0 azsecrets.SetSecretResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, azsecrets.SetSecretParameters, *azsecrets.SetSecretOptions) (azsecrets.SetSecretResponse, error)); ok { + return rf(ctx, secretName, params, options) + } + if rf, ok := ret.Get(0).(func(context.Context, string, azsecrets.SetSecretParameters, *azsecrets.SetSecretOptions) azsecrets.SetSecretResponse); ok { + r0 = rf(ctx, secretName, params, options) + } else { + r0 = ret.Get(0).(azsecrets.SetSecretResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, azsecrets.SetSecretParameters, *azsecrets.SetSecretOptions) error); ok { + r1 = rf(ctx, secretName, params, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewSecretsRW interface { + mock.TestingT + Cleanup(func()) +} + +// NewSecretsRW creates a new instance of SecretsRW. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewSecretsRW(t mockConstructorTestingTNewSecretsRW) *SecretsRW { + mock := &SecretsRW{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/credentials/manager/manager.go b/internal/credentials/manager/manager.go index 300db095e..2e27600c0 100644 --- a/internal/credentials/manager/manager.go +++ b/internal/credentials/manager/manager.go @@ -73,6 +73,16 @@ func newAzureKBManager(conf *api.Credentials_AzureKeyVault, prefix string, r cre return nil, fmt.Errorf("configuring the secrets manager: %w", err) } + if opts.Role == credentials.RoleReader { + if err := azurekv.ValidateReaderClient(m, prefix); err != nil { + return nil, fmt.Errorf("validating client: %w", err) + } + } else { + if err := azurekv.ValidateWriterClient(m, prefix); err != nil { + return nil, fmt.Errorf("validating client: %w", err) + } + } + return m, nil } From 53cda36c828e556b9a505fc9cf105129db826472 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 13 Oct 2023 17:29:48 +0200 Subject: [PATCH 3/4] chore: add tests Signed-off-by: Miguel Martinez Trivino --- internal/credentials/manager/manager_test.go | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/internal/credentials/manager/manager_test.go b/internal/credentials/manager/manager_test.go index 432e94ea6..c0300a7c9 100644 --- a/internal/credentials/manager/manager_test.go +++ b/internal/credentials/manager/manager_test.go @@ -61,6 +61,42 @@ func validVaultConfig(s *testSuite) *v1.Credentials { } } +func (s *testSuite) TestNewAzureManagerFromConfig() { + testCases := []struct { + name string + tenantID string + clientID string + clientSecret string + vaultURI string + Role credentials.Role + expectedError bool + }{ + {name: "missing tenantID", tenantID: "", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, + {name: "missing clientID", tenantID: "tenantID", clientID: "", clientSecret: "clientSecret", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, + {name: "missing clientSecret", tenantID: "tenantID", clientID: "clientID", clientSecret: "", vaultURI: "vaultURI", Role: credentials.RoleReader, expectedError: true}, + {name: "missing vaultURI", tenantID: "tenantID", clientID: "clientID", clientSecret: "clientSecret", vaultURI: "", Role: credentials.RoleReader, expectedError: true}, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + conf := &v1.Credentials{ + Backend: &v1.Credentials_AzureKeyVault_{ + AzureKeyVault: &v1.Credentials_AzureKeyVault{ + TenantId: tc.tenantID, ClientId: tc.clientID, ClientSecret: tc.clientSecret, VaultUri: tc.vaultURI, + }, + }, + } + + _, err := manager.NewFromConfig(conf, tc.Role, nil) + if tc.expectedError { + assert.Error(s.T(), err) + } else { + assert.NoError(s.T(), err) + } + }) + } +} + func (s *testSuite) TestNewFromConfig() { testCases := []struct { name string From 8a11aff30abe741ea497698e99e72bedf31851d8 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 13 Oct 2023 21:20:50 +0200 Subject: [PATCH 4/4] chore: add tests Signed-off-by: Miguel Martinez Trivino --- internal/credentials/manager/manager.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/credentials/manager/manager.go b/internal/credentials/manager/manager.go index 2e27600c0..03d704b02 100644 --- a/internal/credentials/manager/manager.go +++ b/internal/credentials/manager/manager.go @@ -75,11 +75,11 @@ func newAzureKBManager(conf *api.Credentials_AzureKeyVault, prefix string, r cre if opts.Role == credentials.RoleReader { if err := azurekv.ValidateReaderClient(m, prefix); err != nil { - return nil, fmt.Errorf("validating client: %w", err) + return nil, fmt.Errorf("validating Azure KeyVault reader client: %w", err) } } else { if err := azurekv.ValidateWriterClient(m, prefix); err != nil { - return nil, fmt.Errorf("validating client: %w", err) + return nil, fmt.Errorf("validating Azure KeyVault writer client: %w", err) } }