From 50fef93875db22242aebc6e6507d21d8f3047a98 Mon Sep 17 00:00:00 2001 From: Lakshmi Javadekar <103459615+lakshmimsft@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:02:55 -0700 Subject: [PATCH] Add new secret types to Applications.Core/secretstores (#7816) # Description Add new types to Applications.Core/secretstores (basicAuthentication, azureWorkloadIdentity, awsIRSA) Update convertor, tests. Update existing ValidateAndMutateRequest() in /pkg/corerp/frontend/controller/secretstores/kubernetes.go to check if required secret keys exist for current secret type. Add to existing unit tests. ## Type of change - This pull request fixes a bug in Radius and has an approved issue (#6917 ). Fixes: Part of #6917 --- .../2023-10-01-preview/types.json | 144 +++++++++++------ hack/bicep-types-radius/generated/index.json | 4 +- .../secretstore_conversion.go | 12 ++ .../zz_generated_constants.go | 12 ++ .../v20231001preview/zz_generated_models.go | 2 +- pkg/corerp/datamodel/secretstore.go | 6 + .../controller/secretstores/kubernetes.go | 21 ++- .../secretstores/kubernetes_test.go | 153 ++++++++++++------ .../secretstores_datamodel_awsirsa.json | 29 ++++ .../testdata/secretstores_datamodel_azwi.json | 32 ++++ .../secretstores_datamodel_basicauth.json | 32 ++++ ...retstores_datamodel_basicauth_invalid.json | 29 ++++ .../frontend/controller/secretstores/types.go | 7 + .../preview/2023-10-01-preview/openapi.json | 22 ++- typespec/Applications.Core/environments.tsp | 2 +- typespec/Applications.Core/secretstores.tsp | 9 ++ 16 files changed, 412 insertions(+), 104 deletions(-) create mode 100644 pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_awsirsa.json create mode 100644 pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_azwi.json create mode 100644 pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_basicauth.json create mode 100644 pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_basicauth_invalid.json diff --git a/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json b/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json index a8fad28162..10edcb4a28 100644 --- a/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json +++ b/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json @@ -2652,7 +2652,7 @@ }, "tags": { "type": { - "$ref": "#/219" + "$ref": "#/222" }, "flags": 0, "description": "Resource tags." @@ -2707,14 +2707,14 @@ }, "type": { "type": { - "$ref": "#/212" + "$ref": "#/215" }, "flags": 0, "description": "The type of SecretStore data" }, "data": { "type": { - "$ref": "#/218" + "$ref": "#/221" }, "flags": 1, "description": "An object to represent key-value type secrets" @@ -2790,6 +2790,18 @@ "$type": "StringLiteralType", "value": "certificate" }, + { + "$type": "StringLiteralType", + "value": "basicAuthentication" + }, + { + "$type": "StringLiteralType", + "value": "azureWorkloadIdentity" + }, + { + "$type": "StringLiteralType", + "value": "awsIRSA" + }, { "$type": "UnionType", "elements": [ @@ -2798,6 +2810,15 @@ }, { "$ref": "#/211" + }, + { + "$ref": "#/212" + }, + { + "$ref": "#/213" + }, + { + "$ref": "#/214" } ] }, @@ -2807,7 +2828,7 @@ "properties": { "encoding": { "type": { - "$ref": "#/216" + "$ref": "#/219" }, "flags": 0, "description": "The type of SecretValue Encoding" @@ -2821,7 +2842,7 @@ }, "valueFrom": { "type": { - "$ref": "#/217" + "$ref": "#/220" }, "flags": 0, "description": "The Secret value source properties" @@ -2840,10 +2861,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/214" + "$ref": "#/217" }, { - "$ref": "#/215" + "$ref": "#/218" } ] }, @@ -2872,7 +2893,7 @@ "name": "SecretStorePropertiesData", "properties": {}, "additionalProperties": { - "$ref": "#/213" + "$ref": "#/216" } }, { @@ -2889,14 +2910,14 @@ "properties": { "type": { "type": { - "$ref": "#/223" + "$ref": "#/229" }, "flags": 2, "description": "The type of SecretStore data" }, "data": { "type": { - "$ref": "#/224" + "$ref": "#/230" }, "flags": 2, "description": "An object to represent key-value type secrets" @@ -2911,14 +2932,35 @@ "$type": "StringLiteralType", "value": "certificate" }, + { + "$type": "StringLiteralType", + "value": "basicAuthentication" + }, + { + "$type": "StringLiteralType", + "value": "azureWorkloadIdentity" + }, + { + "$type": "StringLiteralType", + "value": "awsIRSA" + }, { "$type": "UnionType", "elements": [ { - "$ref": "#/221" + "$ref": "#/224" }, { - "$ref": "#/222" + "$ref": "#/225" + }, + { + "$ref": "#/226" + }, + { + "$ref": "#/227" + }, + { + "$ref": "#/228" } ] }, @@ -2927,14 +2969,14 @@ "name": "SecretStoreListSecretsResultData", "properties": {}, "additionalProperties": { - "$ref": "#/213" + "$ref": "#/216" } }, { "$type": "FunctionType", "parameters": [], "output": { - "$ref": "#/220" + "$ref": "#/223" } }, { @@ -2948,7 +2990,7 @@ "functions": { "listSecrets": { "type": { - "$ref": "#/225" + "$ref": "#/231" }, "description": "listSecrets" } @@ -2982,28 +3024,28 @@ }, "type": { "type": { - "$ref": "#/227" + "$ref": "#/233" }, "flags": 10, "description": "The resource type" }, "apiVersion": { "type": { - "$ref": "#/228" + "$ref": "#/234" }, "flags": 10, "description": "The resource api version" }, "properties": { "type": { - "$ref": "#/230" + "$ref": "#/236" }, "flags": 1, "description": "Volume properties" }, "tags": { "type": { - "$ref": "#/262" + "$ref": "#/268" }, "flags": 0, "description": "Resource tags." @@ -3045,7 +3087,7 @@ }, "provisioningState": { "type": { - "$ref": "#/238" + "$ref": "#/244" }, "flags": 2, "description": "Provisioning state of the resource at the time the operation was called" @@ -3060,7 +3102,7 @@ }, "elements": { "azure.com.keyvault": { - "$ref": "#/239" + "$ref": "#/245" } } }, @@ -3096,25 +3138,25 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/231" + "$ref": "#/237" }, { - "$ref": "#/232" + "$ref": "#/238" }, { - "$ref": "#/233" + "$ref": "#/239" }, { - "$ref": "#/234" + "$ref": "#/240" }, { - "$ref": "#/235" + "$ref": "#/241" }, { - "$ref": "#/236" + "$ref": "#/242" }, { - "$ref": "#/237" + "$ref": "#/243" } ] }, @@ -3124,14 +3166,14 @@ "properties": { "certificates": { "type": { - "$ref": "#/252" + "$ref": "#/258" }, "flags": 0, "description": "The KeyVault certificates that this volume exposes" }, "keys": { "type": { - "$ref": "#/254" + "$ref": "#/260" }, "flags": 0, "description": "The KeyVault keys that this volume exposes" @@ -3145,14 +3187,14 @@ }, "secrets": { "type": { - "$ref": "#/260" + "$ref": "#/266" }, "flags": 0, "description": "The KeyVault secrets that this volume exposes" }, "kind": { "type": { - "$ref": "#/261" + "$ref": "#/267" }, "flags": 1, "description": "Discriminator property for VolumeProperties." @@ -3172,14 +3214,14 @@ }, "encoding": { "type": { - "$ref": "#/244" + "$ref": "#/250" }, "flags": 0, "description": "Represents secret encodings" }, "format": { "type": { - "$ref": "#/247" + "$ref": "#/253" }, "flags": 0, "description": "Represents certificate formats" @@ -3193,7 +3235,7 @@ }, "certType": { "type": { - "$ref": "#/251" + "$ref": "#/257" }, "flags": 0, "description": "Represents certificate types" @@ -3223,13 +3265,13 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/241" + "$ref": "#/247" }, { - "$ref": "#/242" + "$ref": "#/248" }, { - "$ref": "#/243" + "$ref": "#/249" } ] }, @@ -3245,10 +3287,10 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/245" + "$ref": "#/251" }, { - "$ref": "#/246" + "$ref": "#/252" } ] }, @@ -3268,13 +3310,13 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/248" + "$ref": "#/254" }, { - "$ref": "#/249" + "$ref": "#/255" }, { - "$ref": "#/250" + "$ref": "#/256" } ] }, @@ -3283,7 +3325,7 @@ "name": "AzureKeyVaultVolumePropertiesCertificates", "properties": {}, "additionalProperties": { - "$ref": "#/240" + "$ref": "#/246" } }, { @@ -3318,7 +3360,7 @@ "name": "AzureKeyVaultVolumePropertiesKeys", "properties": {}, "additionalProperties": { - "$ref": "#/253" + "$ref": "#/259" } }, { @@ -3334,7 +3376,7 @@ }, "encoding": { "type": { - "$ref": "#/259" + "$ref": "#/265" }, "flags": 0, "description": "Represents secret encodings" @@ -3371,13 +3413,13 @@ "$type": "UnionType", "elements": [ { - "$ref": "#/256" + "$ref": "#/262" }, { - "$ref": "#/257" + "$ref": "#/263" }, { - "$ref": "#/258" + "$ref": "#/264" } ] }, @@ -3386,7 +3428,7 @@ "name": "AzureKeyVaultVolumePropertiesSecrets", "properties": {}, "additionalProperties": { - "$ref": "#/255" + "$ref": "#/261" } }, { @@ -3406,7 +3448,7 @@ "name": "Applications.Core/volumes@2023-10-01-preview", "scopeType": 0, "body": { - "$ref": "#/229" + "$ref": "#/235" }, "flags": 0, "functions": {} diff --git a/hack/bicep-types-radius/generated/index.json b/hack/bicep-types-radius/generated/index.json index 9e9032ebcf..aa50617f14 100644 --- a/hack/bicep-types-radius/generated/index.json +++ b/hack/bicep-types-radius/generated/index.json @@ -16,10 +16,10 @@ "$ref": "applications/applications.core/2023-10-01-preview/types.json#/197" }, "Applications.Core/secretStores@2023-10-01-preview": { - "$ref": "applications/applications.core/2023-10-01-preview/types.json#/226" + "$ref": "applications/applications.core/2023-10-01-preview/types.json#/232" }, "Applications.Core/volumes@2023-10-01-preview": { - "$ref": "applications/applications.core/2023-10-01-preview/types.json#/263" + "$ref": "applications/applications.core/2023-10-01-preview/types.json#/269" }, "Applications.Dapr/pubSubBrokers@2023-10-01-preview": { "$ref": "applications/applications.dapr/2023-10-01-preview/types.json#/44" diff --git a/pkg/corerp/api/v20231001preview/secretstore_conversion.go b/pkg/corerp/api/v20231001preview/secretstore_conversion.go index cf1049f25b..24a3bed0e0 100644 --- a/pkg/corerp/api/v20231001preview/secretstore_conversion.go +++ b/pkg/corerp/api/v20231001preview/secretstore_conversion.go @@ -106,6 +106,12 @@ func toSecretStoreDataTypeDataModel(src *SecretStoreDataType) datamodel.SecretTy return datamodel.SecretTypeGeneric case SecretStoreDataTypeCertificate: return datamodel.SecretTypeCert + case SecretStoreDataTypeBasicAuthentication: + return datamodel.SecretTypeBasicAuthentication + case SecretStoreDataTypeAzureWorkloadIdentity: + return datamodel.SecretTypeAzureWorkloadIdentity + case SecretStoreDataTypeAwsIRSA: + return datamodel.SecretTypeAWSIRSA } return datamodel.SecretTypeGeneric @@ -117,6 +123,12 @@ func fromSecretStoreDataTypeDataModel(src datamodel.SecretType) *SecretStoreData return to.Ptr(SecretStoreDataTypeGeneric) case datamodel.SecretTypeCert: return to.Ptr(SecretStoreDataTypeCertificate) + case datamodel.SecretTypeBasicAuthentication: + return to.Ptr(SecretStoreDataTypeBasicAuthentication) + case datamodel.SecretTypeAzureWorkloadIdentity: + return to.Ptr(SecretStoreDataTypeAzureWorkloadIdentity) + case datamodel.SecretTypeAWSIRSA: + return to.Ptr(SecretStoreDataTypeAwsIRSA) } return nil } diff --git a/pkg/corerp/api/v20231001preview/zz_generated_constants.go b/pkg/corerp/api/v20231001preview/zz_generated_constants.go index 5e3909b5a8..5513e441df 100644 --- a/pkg/corerp/api/v20231001preview/zz_generated_constants.go +++ b/pkg/corerp/api/v20231001preview/zz_generated_constants.go @@ -329,6 +329,15 @@ func PossibleRestartPolicyValues() []RestartPolicy { type SecretStoreDataType string const ( + // SecretStoreDataTypeAwsIRSA - awsIRSA type is used to represent registry authentication using AWS IRSA(IAM Roles for Service +// accounts) and the secretstore resource is expected to have the keys 'roleARN'. + SecretStoreDataTypeAwsIRSA SecretStoreDataType = "awsIRSA" + // SecretStoreDataTypeAzureWorkloadIdentity - azureWorkloadIdentity type is used to represent registry authentication using +// azure federated identity and the secretstore resource is expected to have the keys 'clientId' and 'tenantId'. + SecretStoreDataTypeAzureWorkloadIdentity SecretStoreDataType = "azureWorkloadIdentity" + // SecretStoreDataTypeBasicAuthentication - basicAuthentication type is used to represent username and password based authentication +// and the secretstore resource is expected to have the keys 'username' and 'password'. + SecretStoreDataTypeBasicAuthentication SecretStoreDataType = "basicAuthentication" // SecretStoreDataTypeCertificate - Certificate secret data type SecretStoreDataTypeCertificate SecretStoreDataType = "certificate" // SecretStoreDataTypeGeneric - Generic secret data type @@ -338,6 +347,9 @@ const ( // PossibleSecretStoreDataTypeValues returns the possible values for the SecretStoreDataType const type. func PossibleSecretStoreDataTypeValues() []SecretStoreDataType { return []SecretStoreDataType{ + SecretStoreDataTypeAwsIRSA, + SecretStoreDataTypeAzureWorkloadIdentity, + SecretStoreDataTypeBasicAuthentication, SecretStoreDataTypeCertificate, SecretStoreDataTypeGeneric, } diff --git a/pkg/corerp/api/v20231001preview/zz_generated_models.go b/pkg/corerp/api/v20231001preview/zz_generated_models.go index 6b494e07bf..b96432971e 100644 --- a/pkg/corerp/api/v20231001preview/zz_generated_models.go +++ b/pkg/corerp/api/v20231001preview/zz_generated_models.go @@ -1312,7 +1312,7 @@ type Recipe struct { // RecipeConfigProperties - Configuration for Recipes. Defines how each type of Recipe should be configured and run. type RecipeConfigProperties struct { - // Environment variables injected during Terraform Recipe execution for the recipes in the environment. + // Environment variables injected during recipe execution for the recipes in the environment. Env map[string]*string // Environment variables containing sensitive information can be stored as secrets. The secrets are stored in Applications.Core/SecretStores diff --git a/pkg/corerp/datamodel/secretstore.go b/pkg/corerp/datamodel/secretstore.go index 5ec0993538..86f9bdc6df 100644 --- a/pkg/corerp/datamodel/secretstore.go +++ b/pkg/corerp/datamodel/secretstore.go @@ -45,6 +45,12 @@ const ( SecretTypeGeneric SecretType = "generic" // SecretTypeCert is the certificate secret type. SecretTypeCert SecretType = "certificate" + // SecretTypeBasicAuthentication is the basicAuthentication secret type. + SecretTypeBasicAuthentication SecretType = "basicAuthentication" + // SecretTypeAzureWorkloadIdentity is the azureWorkloadIdentity secret type. + SecretTypeAzureWorkloadIdentity SecretType = "azureWorkloadIdentity" + // SecretTypeAWSIRSA is the awsIRSA secret type. + SecretTypeAWSIRSA SecretType = "awsIRSA" ) // SecretStore represents secret store resource. diff --git a/pkg/corerp/frontend/controller/secretstores/kubernetes.go b/pkg/corerp/frontend/controller/secretstores/kubernetes.go index 3825a03a21..fff7c01bbe 100644 --- a/pkg/corerp/frontend/controller/secretstores/kubernetes.go +++ b/pkg/corerp/frontend/controller/secretstores/kubernetes.go @@ -46,6 +46,9 @@ func getOrDefaultType(t datamodel.SecretType) (datamodel.SecretType, error) { t = datamodel.SecretTypeGeneric case datamodel.SecretTypeCert: case datamodel.SecretTypeGeneric: + case datamodel.SecretTypeBasicAuthentication: + case datamodel.SecretTypeAzureWorkloadIdentity: + case datamodel.SecretTypeAWSIRSA: default: err = fmt.Errorf("'%s' is invalid secret type", t) } @@ -75,8 +78,15 @@ func getOrDefaultEncoding(t datamodel.SecretType, e datamodel.SecretValueEncodin return e, err } +// Define a map of required keys for each SecretType +var requiredKeys = map[datamodel.SecretType][]string{ + datamodel.SecretTypeBasicAuthentication: {RequiredUsername, RequiredPassword}, + datamodel.SecretTypeAzureWorkloadIdentity: {RequiredClientId, RequiredTenantId}, + datamodel.SecretTypeAWSIRSA: {RequiredRoleARN}, +} + // ValidateAndMutateRequest checks the type and encoding of the secret store, and ensures that the secret store data is -// valid. If any of these checks fail, a BadRequestResponse is returned. +// valid and required keys are present for the secret type. If any of these checks fail, a BadRequestResponse is returned. func ValidateAndMutateRequest(ctx context.Context, newResource *datamodel.SecretStore, oldResource *datamodel.SecretStore, options *controller.Options) (rest.Response, error) { var err error newResource.Properties.Type, err = getOrDefaultType(newResource.Properties.Type) @@ -116,6 +126,15 @@ func ValidateAndMutateRequest(ctx context.Context, newResource *datamodel.Secret } } + // Validate that required keys for the secret type are present in the secret data + if keys, ok := requiredKeys[newResource.Properties.Type]; ok { + for _, key := range keys { + if _, ok := newResource.Properties.Data[key]; !ok { + return rest.NewBadRequestResponse(fmt.Sprintf("$.properties.data must contain '%s' key for %s type.", key, newResource.Properties.Type)), nil + } + } + } + return nil, nil } diff --git a/pkg/corerp/frontend/controller/secretstores/kubernetes_test.go b/pkg/corerp/frontend/controller/secretstores/kubernetes_test.go index 2eda9c94ec..676107a91b 100644 --- a/pkg/corerp/frontend/controller/secretstores/kubernetes_test.go +++ b/pkg/corerp/frontend/controller/secretstores/kubernetes_test.go @@ -51,6 +51,11 @@ const ( testFileGenericValueGlobalScope = "secretstores_datamodel_global_scope.json" testFileGenericValueInvalidResource = "secretstores_datamodel_global_scope_invalid_resource.json" testFileGenericValueEmptyResource = "secretstores_datamodel_global_scope_empty_resource.json" + + testFileBasicAuthentication = "secretstores_datamodel_basicauth.json" + testFileBasicAuthenticationInvalid = "secretstores_datamodel_basicauth_invalid.json" + testFileAWSIRSA = "secretstores_datamodel_awsirsa.json" + testFileAzureWorkloadIdentity = "secretstores_datamodel_azwi.json" ) func TestGetNamespace(t *testing.T) { @@ -247,53 +252,109 @@ func TestGetOrDefaultEncoding(t *testing.T) { } func TestValidateAndMutateRequest(t *testing.T) { - t.Run("default type is generic", func(t *testing.T) { - newResource := testutil.MustGetTestData[datamodel.SecretStore](testFileCertValueFrom) - newResource.Properties.Type = "" - - resp, err := ValidateAndMutateRequest(context.TODO(), newResource, nil, nil) - require.NoError(t, err) - require.Nil(t, resp) - - // assert - require.Equal(t, datamodel.SecretTypeGeneric, newResource.Properties.Type) - }) - - t.Run("new resource, but referencing valueFrom", func(t *testing.T) { - newResource := testutil.MustGetTestData[datamodel.SecretStore](testFileCertValueFrom) - newResource.Properties.Resource = "" - resp, err := ValidateAndMutateRequest(context.TODO(), newResource, nil, nil) - require.NoError(t, err) - - // assert - r := resp.(*rest.BadRequestResponse) - require.True(t, r.Body.Error.Message == "$.properties.data[tls.crt].Value must be given to create the secret." || - r.Body.Error.Message == "$.properties.data[tls.key].Value must be given to create the secret.") - }) - - t.Run("update the existing resource - type not matched", func(t *testing.T) { - oldResource := testutil.MustGetTestData[datamodel.SecretStore](testFileCertValueFrom) - oldResource.Properties.Type = datamodel.SecretTypeGeneric - newResource := testutil.MustGetTestData[datamodel.SecretStore](testFileCertValueFrom) - resp, err := ValidateAndMutateRequest(context.TODO(), newResource, oldResource, nil) - require.NoError(t, err) - - // assert - r := resp.(*rest.BadRequestResponse) - require.Equal(t, "$.properties.type cannot change from 'generic' to 'certificate'.", r.Body.Error.Message) - }) - - t.Run("inherit resource id from existing resource", func(t *testing.T) { - oldResource := testutil.MustGetTestData[datamodel.SecretStore](testFileCertValueFrom) - newResource := testutil.MustGetTestData[datamodel.SecretStore](testFileCertValueFrom) - newResource.Properties.Resource = "" - resp, err := ValidateAndMutateRequest(context.TODO(), newResource, oldResource, nil) + tests := []struct { + name string + testFile string + oldResource *datamodel.SecretStore + modifyResource func(*datamodel.SecretStore, *datamodel.SecretStore) + assertions func(*testing.T, rest.Response, error, *datamodel.SecretStore, *datamodel.SecretStore) + }{ + { + name: "default type is generic", + testFile: testFileCertValueFrom, + modifyResource: func(newResource, oldResource *datamodel.SecretStore) { + newResource.Properties.Type = "" + }, + assertions: func(t *testing.T, resp rest.Response, err error, newResource, oldResource *datamodel.SecretStore) { + require.NoError(t, err) + require.Nil(t, resp) + require.Equal(t, datamodel.SecretTypeGeneric, newResource.Properties.Type) + }, + }, + { + name: "new resource, but referencing valueFrom", + testFile: testFileCertValueFrom, + modifyResource: func(newResource, oldResource *datamodel.SecretStore) { + newResource.Properties.Resource = "" + }, + assertions: func(t *testing.T, resp rest.Response, err error, newResource, oldResource *datamodel.SecretStore) { + require.NoError(t, err) + r := resp.(*rest.BadRequestResponse) + require.True(t, r.Body.Error.Message == "$.properties.data[tls.crt].Value must be given to create the secret." || + r.Body.Error.Message == "$.properties.data[tls.key].Value must be given to create the secret.") + }, + }, + { + name: "update the existing resource - type not matched", + testFile: testFileCertValueFrom, + oldResource: testutil.MustGetTestData[datamodel.SecretStore](testFileCertValueFrom), + modifyResource: func(newResource, oldResource *datamodel.SecretStore) { + oldResource.Properties.Type = datamodel.SecretTypeGeneric + }, + assertions: func(t *testing.T, resp rest.Response, err error, newResource, oldResource *datamodel.SecretStore) { + require.NoError(t, err) + r := resp.(*rest.BadRequestResponse) + require.Equal(t, "$.properties.type cannot change from 'generic' to 'certificate'.", r.Body.Error.Message) + }, + }, + { + name: "inherit resource id from existing resource", + testFile: testFileCertValueFrom, + oldResource: testutil.MustGetTestData[datamodel.SecretStore](testFileCertValueFrom), + modifyResource: func(newResource, oldResource *datamodel.SecretStore) { + newResource.Properties.Resource = "" + }, + assertions: func(t *testing.T, resp rest.Response, err error, newResource, oldResource *datamodel.SecretStore) { + require.NoError(t, err) + require.Nil(t, resp) + require.Equal(t, oldResource.Properties.Resource, newResource.Properties.Resource) + }, + }, + { + name: "new basicAuthentication resource", + testFile: testFileBasicAuthentication, + assertions: func(t *testing.T, resp rest.Response, err error, newResource, oldResource *datamodel.SecretStore) { + require.NoError(t, err) + require.Nil(t, resp) + }, + }, + { + name: "new awsIRSA resource", + testFile: testFileAWSIRSA, + assertions: func(t *testing.T, resp rest.Response, err error, newResource, oldResource *datamodel.SecretStore) { + require.NoError(t, err) + require.Nil(t, resp) + }, + }, + { + name: "new azureWorkloadIdentity resource", + testFile: testFileAzureWorkloadIdentity, + assertions: func(t *testing.T, resp rest.Response, err error, newResource, oldResource *datamodel.SecretStore) { + require.NoError(t, err) + require.Nil(t, resp) + }, + }, + { + name: "invalid basicAuthentication resource", + testFile: testFileBasicAuthenticationInvalid, + assertions: func(t *testing.T, resp rest.Response, err error, newResource, oldResource *datamodel.SecretStore) { + require.NoError(t, err) + r := resp.(*rest.BadRequestResponse) + require.True(t, r.Body.Error.Message == "$.properties.data must contain 'password' key for basicAuthentication type.") + }, + }, + } - // assert - require.NoError(t, err) - require.Nil(t, resp) - require.Equal(t, oldResource.Properties.Resource, newResource.Properties.Resource) - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newResource := testutil.MustGetTestData[datamodel.SecretStore](tt.testFile) + if tt.modifyResource != nil { + tt.modifyResource(newResource, tt.oldResource) + } + resp, err := ValidateAndMutateRequest(context.TODO(), newResource, tt.oldResource, nil) + tt.assertions(t, resp, err, newResource, tt.oldResource) + }) + } } func TestUpsertSecret(t *testing.T) { diff --git a/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_awsirsa.json b/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_awsirsa.json new file mode 100644 index 0000000000..eec09c3e9f --- /dev/null +++ b/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_awsirsa.json @@ -0,0 +1,29 @@ +{ + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/secretStores/secret0", + "name": "secret0", + "type": "applications.core/secretstores", + "location": "global", + "systemData": { + "createdAt": "2022-03-22T18:54:52.6857175Z", + "createdBy": "fake@hotmail.com", + "createdByType": "User", + "lastModifiedAt": "2022-03-22T18:57:52.6857175Z", + "lastModifiedBy": "fake@hotmail.com", + "lastModifiedByType": "User" + }, + "provisioningState": "Succeeded", + "properties": { + "application": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", + "type": "awsIRSA", + "data": { + "roleARN": { + "value": "test-role-arn" + } + } + }, + "tenantId": "00000000-0000-0000-0000-000000000000", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroup": "testGroup", + "createdApiVersion": "2023-10-01-preview", + "updatedApiVersion": "2023-10-01-preview" +} \ No newline at end of file diff --git a/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_azwi.json b/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_azwi.json new file mode 100644 index 0000000000..a05d157bfc --- /dev/null +++ b/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_azwi.json @@ -0,0 +1,32 @@ +{ + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/secretStores/secret0", + "name": "secret0", + "type": "applications.core/secretstores", + "location": "global", + "systemData": { + "createdAt": "2022-03-22T18:54:52.6857175Z", + "createdBy": "fake@hotmail.com", + "createdByType": "User", + "lastModifiedAt": "2022-03-22T18:57:52.6857175Z", + "lastModifiedBy": "fake@hotmail.com", + "lastModifiedByType": "User" + }, + "provisioningState": "Succeeded", + "properties": { + "application": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", + "type": "azureWorkloadIdentity", + "data": { + "clientId": { + "value": "test-client-Id" + }, + "tenantId": { + "value": "test-tenant-Id" + } + } + }, + "tenantId": "00000000-0000-0000-0000-000000000000", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroup": "testGroup", + "createdApiVersion": "2023-10-01-preview", + "updatedApiVersion": "2023-10-01-preview" +} \ No newline at end of file diff --git a/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_basicauth.json b/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_basicauth.json new file mode 100644 index 0000000000..4b4e3f1259 --- /dev/null +++ b/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_basicauth.json @@ -0,0 +1,32 @@ +{ + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/secretStores/secret0", + "name": "secret0", + "type": "applications.core/secretstores", + "location": "global", + "systemData": { + "createdAt": "2022-03-22T18:54:52.6857175Z", + "createdBy": "fake@hotmail.com", + "createdByType": "User", + "lastModifiedAt": "2022-03-22T18:57:52.6857175Z", + "lastModifiedBy": "fake@hotmail.com", + "lastModifiedByType": "User" + }, + "provisioningState": "Succeeded", + "properties": { + "application": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", + "type": "basicAuthentication", + "data": { + "username": { + "value": "uname123" + }, + "password": { + "value": "testpwd-dGxzLmNlcnQK" + } + } + }, + "tenantId": "00000000-0000-0000-0000-000000000000", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroup": "testGroup", + "createdApiVersion": "2023-10-01-preview", + "updatedApiVersion": "2023-10-01-preview" +} \ No newline at end of file diff --git a/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_basicauth_invalid.json b/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_basicauth_invalid.json new file mode 100644 index 0000000000..d0e7585a17 --- /dev/null +++ b/pkg/corerp/frontend/controller/secretstores/testdata/secretstores_datamodel_basicauth_invalid.json @@ -0,0 +1,29 @@ +{ + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/secretStores/secret0", + "name": "secret0", + "type": "applications.core/secretstores", + "location": "global", + "systemData": { + "createdAt": "2022-03-22T18:54:52.6857175Z", + "createdBy": "fake@hotmail.com", + "createdByType": "User", + "lastModifiedAt": "2022-03-22T18:57:52.6857175Z", + "lastModifiedBy": "fake@hotmail.com", + "lastModifiedByType": "User" + }, + "provisioningState": "Succeeded", + "properties": { + "application": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", + "type": "basicAuthentication", + "data": { + "username": { + "value": "uname123" + } + } + }, + "tenantId": "00000000-0000-0000-0000-000000000000", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroup": "testGroup", + "createdApiVersion": "2023-10-01-preview", + "updatedApiVersion": "2023-10-01-preview" +} \ No newline at end of file diff --git a/pkg/corerp/frontend/controller/secretstores/types.go b/pkg/corerp/frontend/controller/secretstores/types.go index 0e53242532..a4375f8420 100644 --- a/pkg/corerp/frontend/controller/secretstores/types.go +++ b/pkg/corerp/frontend/controller/secretstores/types.go @@ -19,4 +19,11 @@ package secretstores const ( // ResourceTypeName is the resource type name for secret stores. ResourceTypeName = "Applications.Core/secretStores" + + // The following are possible required keys in a SecretStore depending on it's SecretType + RequiredUsername = "username" + RequiredPassword = "password" + RequiredClientId = "clientId" + RequiredTenantId = "tenantId" + RequiredRoleARN = "roleARN" ) diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json index 62141141c6..dfbe7a8301 100644 --- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json +++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json @@ -4356,7 +4356,7 @@ }, "env": { "$ref": "#/definitions/EnvironmentVariables", - "description": "Environment variables injected during Terraform Recipe execution for the recipes in the environment." + "description": "Environment variables injected during recipe execution for the recipes in the environment." }, "envSecrets": { "type": "object", @@ -4660,7 +4660,10 @@ "description": "The type of SecretStore data", "enum": [ "generic", - "certificate" + "certificate", + "basicAuthentication", + "azureWorkloadIdentity", + "awsIRSA" ], "x-ms-enum": { "name": "SecretStoreDataType", @@ -4675,6 +4678,21 @@ "name": "certificate", "value": "certificate", "description": "Certificate secret data type" + }, + { + "name": "basicAuthentication", + "value": "basicAuthentication", + "description": "basicAuthentication type is used to represent username and password based authentication and the secretstore resource is expected to have the keys 'username' and 'password'." + }, + { + "name": "azureWorkloadIdentity", + "value": "azureWorkloadIdentity", + "description": "azureWorkloadIdentity type is used to represent registry authentication using azure federated identity and the secretstore resource is expected to have the keys 'clientId' and 'tenantId'." + }, + { + "name": "awsIRSA", + "value": "awsIRSA", + "description": "awsIRSA type is used to represent registry authentication using AWS IRSA(IAM Roles for Service accounts) and the secretstore resource is expected to have the keys 'roleARN'." } ] } diff --git a/typespec/Applications.Core/environments.tsp b/typespec/Applications.Core/environments.tsp index e1eea5e636..aac6eb9a91 100644 --- a/typespec/Applications.Core/environments.tsp +++ b/typespec/Applications.Core/environments.tsp @@ -79,7 +79,7 @@ model RecipeConfigProperties { @doc("Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment.") terraform?: TerraformConfigProperties; - @doc("Environment variables injected during Terraform Recipe execution for the recipes in the environment.") + @doc("Environment variables injected during recipe execution for the recipes in the environment.") env?: EnvironmentVariables; @doc("Environment variables containing sensitive information can be stored as secrets. The secrets are stored in Applications.Core/SecretStores resource.") diff --git a/typespec/Applications.Core/secretstores.tsp b/typespec/Applications.Core/secretstores.tsp index 41f35a2cab..f1e4e173dc 100644 --- a/typespec/Applications.Core/secretstores.tsp +++ b/typespec/Applications.Core/secretstores.tsp @@ -68,6 +68,15 @@ enum SecretStoreDataType { @doc("Certificate secret data type") certificate, + + @doc("basicAuthentication type is used to represent username and password based authentication and the secretstore resource is expected to have the keys 'username' and 'password'.") + basicAuthentication, + + @doc("azureWorkloadIdentity type is used to represent registry authentication using azure federated identity and the secretstore resource is expected to have the keys 'clientId' and 'tenantId'.") + azureWorkloadIdentity, + + @doc("awsIRSA type is used to represent registry authentication using AWS IRSA(IAM Roles for Service accounts) and the secretstore resource is expected to have the keys 'roleARN'.") + awsIRSA, } @doc("The type of SecretValue Encoding")