diff --git a/client/device/asa/asafixture_test.go b/client/device/asa/asafixture_test.go index 5c7beaf9..bd89c856 100644 --- a/client/device/asa/asafixture_test.go +++ b/client/device/asa/asafixture_test.go @@ -1,10 +1,17 @@ package asa_test import ( + "encoding/json" "fmt" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/asa" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/asa/asaconfig" + internalhttp "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/user" + "github.com/stretchr/testify/assert" "net/http" + "reflect" "testing" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" @@ -41,6 +48,47 @@ func configureDeviceCreateToRespondSuccessfully(createOutput device.CreateOutput ) } +func configureDeviceCreateToRespondSuccessfullyWithNewModel(t *testing.T, createOutput device.CreateOutput) { + httpmock.RegisterResponder( + http.MethodPost, + deviceCreatePath, + func(req *http.Request) (*http.Response, error) { + createInp, err := internalhttp.ReadRequestBody[device.CreateInput](req) + if err != nil { + return nil, err + } + expectedMetadata := &asa.Metadata{IsNewPolicyObjectModel: "true"} + expectedBytes, err := json.Marshal(expectedMetadata) + if err != nil { + return nil, err + } + actualBytes, err := json.Marshal(createInp.Metadata) + if err != nil { + return nil, err + } + expectedMetadataPayload := string(expectedBytes) + actualMetadataPayload := string(actualBytes) + assert.Equal(t, expectedMetadataPayload, actualMetadataPayload) + return httpmock.NewJsonResponse(http.StatusOK, createOutput) + }, + ) +} + +func configureDeviceCreateToRespondSuccessfullyWithoutNewModel(t *testing.T, createOutput device.CreateOutput) { + httpmock.RegisterResponder( + http.MethodPost, + deviceCreatePath, + func(req *http.Request) (*http.Response, error) { + createInp, err := internalhttp.ReadRequestBody[device.CreateInput](req) + if err != nil { + return nil, err + } + assert.True(t, reflect.TypeOf(createInp.Metadata).Kind() == reflect.Pointer) + return httpmock.NewJsonResponse(http.StatusOK, createOutput) + }, + ) +} + func configureDeviceCreateToRespondWithError() { httpmock.RegisterResponder( http.MethodPost, @@ -175,6 +223,22 @@ func configureConnectorReadToRespondWithError(connectorUid string) { ) } +func configureReadApiTokenInfoSuccessfully(tokenInfo user.GetTokenInfoOutput) { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadTokenInfo(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, tokenInfo), + ) +} + +func configureReadApiTokenInfoFailed() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadTokenInfo(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) +} + func assertDeviceCreateWasCalledOnce(t *testing.T) { internalTesting.AssertEndpointCalledTimes(http.MethodPost, deviceCreatePath, 1, t) } diff --git a/client/device/asa/create.go b/client/device/asa/create.go index 4944a8ea..1738e7e1 100644 --- a/client/device/asa/create.go +++ b/client/device/asa/create.go @@ -10,6 +10,8 @@ import ( "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/retry" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/devicetype" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/featureflag" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/user" "strings" ) @@ -36,6 +38,10 @@ type CreateOutput struct { ConnectorUid string `json:"larUid"` } +type Metadata struct { + IsNewPolicyObjectModel string `json:"isNewPolicyObjectModel"` // yes it is a string, but it should be either "true" or "false" :/ +} + type CreateError struct { Err error CreatedResourceId *string @@ -61,8 +67,23 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr client.Logger.Println("creating asa device") + // 1. create a general CDO device with device type ASA + // 1.1 check if we should use the new asa model + userInfo, err := user.GetTokenInfo(ctx, client, user.NewGetTokenInfoInput()) + if err != nil { + return nil, &CreateError{ + CreatedResourceId: nil, + Err: err, + } + } + // 1.2 set metadata according to whether we want to use new asa model + var metadata *Metadata = nil + if userInfo.HasFeatureFlagEnabled(featureflag.AsaConfigurationObjectMigration) { + metadata = &Metadata{IsNewPolicyObjectModel: "true"} + } + // 1.3 create the device deviceCreateOutp, err := device.Create(ctx, client, *device.NewCreateRequestInput( - createInp.Name, "ASA", createInp.ConnectorUid, createInp.ConnectorType, createInp.SocketAddress, false, createInp.IgnoreCertificate, + createInp.Name, "ASA", createInp.ConnectorUid, createInp.ConnectorType, createInp.SocketAddress, false, createInp.IgnoreCertificate, metadata, )) var createdResourceId *string = nil if deviceCreateOutp != nil { @@ -75,6 +96,7 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr } } + // 2. wait for creation to be done, by waiting until asa specific device state is done client.Logger.Println("reading specific device uid") asaReadSpecOutp, err := device.ReadSpecific(ctx, client, *device.NewReadSpecificInput( @@ -118,7 +140,7 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr } } - // encrypt credentials on prem connector + // 3. encrypt credentials for on prem connector (sdc) var publicKey *model.PublicKey if strings.EqualFold(deviceCreateOutp.ConnectorType, "SDC") { diff --git a/client/device/asa/create_test.go b/client/device/asa/create_test.go index 113caa80..6058a875 100644 --- a/client/device/asa/create_test.go +++ b/client/device/asa/create_test.go @@ -2,10 +2,14 @@ package asa_test import ( "context" + "fmt" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/asa/asaconfig" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/featureflag" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/statemachine/state" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/user/auth" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/user/auth/role" "github.com/stretchr/testify/assert" "testing" "time" @@ -45,6 +49,43 @@ func TestAsaCreate(t *testing.T) { Uid: asaSpecificDevice.SpecificUid, State: state.DONE, } + readApiTokenInfo_NoNewModelFeatureFlag := auth.Info{UserAuthentication: auth.Authentication{ + Authorities: []auth.Authority{ + {Authority: role.Admin}, + }, + Details: auth.Details{ + TenantUid: "11111111-1111-1111-1111-111111111111", + TenantName: "", + SseTenantUid: "", + TenantOrganizationName: "", + TenantDbFeatures: "{}", + TenantUserRoles: "", + TenantDatabaseName: "", + TenantPayType: "", + }, + Authenticated: false, + Principle: "", + Name: "", + }} + + readApiTokenInfo_NewModelFeatureFlag := auth.Info{UserAuthentication: auth.Authentication{ + Authorities: []auth.Authority{ + {Authority: role.Admin}, + }, + Details: auth.Details{ + TenantUid: "11111111-1111-1111-1111-111111111111", + TenantName: "", + SseTenantUid: "", + TenantOrganizationName: "", + TenantDbFeatures: fmt.Sprintf("{\"%s\":true}", featureflag.AsaConfigurationObjectMigration), + TenantUserRoles: "", + TenantDatabaseName: "", + TenantPayType: "", + }, + Authenticated: false, + Principle: "", + Name: "", + }} validConnector := connector.NewConnectorOutputBuilder(). WithName("CloudDeviceGateway"). @@ -71,6 +112,7 @@ func TestAsaCreate(t *testing.T) { }, setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoSuccessfully(readApiTokenInfo_NoNewModelFeatureFlag) configureDeviceCreateToRespondSuccessfully(asaDevice) configureDeviceReadSpecificToRespondSuccessfully(asaDevice.Uid, asaSpecificDevice) configureAsaConfigReadToRespondSuccessfully(asaSpecificDevice.SpecificUid, asaConfig) @@ -100,6 +142,81 @@ func TestAsaCreate(t *testing.T) { }, }, + { + testName: "returns error when invalid api token is given", + input: asa.CreateInput{ + Name: asaDevice.Name, + ConnectorType: asaDevice.ConnectorType, + SocketAddress: asaDevice.SocketAddress, + Username: "unittestuser", + Password: "not a real password", + IgnoreCertificate: false, + }, + + setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoFailed() + configureDeviceCreateToRespondSuccessfully(asaDevice) + configureDeviceReadSpecificToRespondSuccessfully(asaDevice.Uid, asaSpecificDevice) + configureAsaConfigReadToRespondSuccessfully(asaSpecificDevice.SpecificUid, asaConfig) + configureAsaConfigUpdateToRespondSuccessfully(asaConfig.Uid, asaconfig.UpdateOutput{Uid: asaConfig.Uid}) + }, + + assertFunc: func(output *asa.CreateOutput, err *asa.CreateError, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + + { + testName: "should call create device with new policy model metadata if feature flag for new model is enable", + input: asa.CreateInput{ + Name: asaDevice.Name, + ConnectorType: asaDevice.ConnectorType, + SocketAddress: asaDevice.SocketAddress, + Username: "unittestuser", + Password: "not a real password", + IgnoreCertificate: false, + }, + + setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoSuccessfully(readApiTokenInfo_NewModelFeatureFlag) + configureDeviceCreateToRespondSuccessfullyWithNewModel(t, asaDevice) + configureDeviceReadSpecificToRespondSuccessfully(asaDevice.Uid, asaSpecificDevice) + configureAsaConfigReadToRespondSuccessfully(asaSpecificDevice.SpecificUid, asaConfig) + configureAsaConfigUpdateToRespondSuccessfully(asaConfig.Uid, asaconfig.UpdateOutput{Uid: asaConfig.Uid}) + }, + + assertFunc: func(output *asa.CreateOutput, err *asa.CreateError, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + }, + }, + + { + testName: "should not call create device with new policy model metadata if feature flag for new model is not enabled", + input: asa.CreateInput{ + Name: asaDevice.Name, + ConnectorType: asaDevice.ConnectorType, + SocketAddress: asaDevice.SocketAddress, + Username: "unittestuser", + Password: "not a real password", + IgnoreCertificate: false, + }, + + setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoSuccessfully(readApiTokenInfo_NoNewModelFeatureFlag) + configureDeviceCreateToRespondSuccessfullyWithoutNewModel(t, asaDevice) + configureDeviceReadSpecificToRespondSuccessfully(asaDevice.Uid, asaSpecificDevice) + configureAsaConfigReadToRespondSuccessfully(asaSpecificDevice.SpecificUid, asaConfig) + configureAsaConfigUpdateToRespondSuccessfully(asaConfig.Uid, asaconfig.UpdateOutput{Uid: asaConfig.Uid}) + }, + + assertFunc: func(output *asa.CreateOutput, err *asa.CreateError, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + }, + }, + { testName: "successfully onboards ASA when using CDG after recovering from certificate error", @@ -113,6 +230,7 @@ func TestAsaCreate(t *testing.T) { }, setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoSuccessfully(readApiTokenInfo_NoNewModelFeatureFlag) configureDeviceCreateToRespondSuccessfully(asaDevice) configureDeviceReadSpecificToRespondSuccessfully(asaDevice.Uid, asaSpecificDevice) configureAsaConfigReadToRespondWithCalls(asaConfig.Uid, []httpmock.Responder{ @@ -163,6 +281,7 @@ func TestAsaCreate(t *testing.T) { }, setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoSuccessfully(readApiTokenInfo_NoNewModelFeatureFlag) configureDeviceCreateToRespondSuccessfully(asaDeviceUsingSdc) configureDeviceReadSpecificToRespondSuccessfully(asaDeviceUsingSdc.Uid, asaSpecificDevice) configureAsaConfigReadToRespondSuccessfully(asaSpecificDevice.SpecificUid, asaConfig) @@ -208,6 +327,7 @@ func TestAsaCreate(t *testing.T) { }, setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoSuccessfully(readApiTokenInfo_NoNewModelFeatureFlag) configureDeviceCreateToRespondSuccessfully(asaDeviceUsingSdc) configureDeviceReadSpecificToRespondSuccessfully(asaDeviceUsingSdc.Uid, asaSpecificDevice) configureAsaConfigReadToRespondWithCalls(asaConfig.Uid, []httpmock.Responder{ @@ -261,6 +381,7 @@ func TestAsaCreate(t *testing.T) { }, setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoSuccessfully(readApiTokenInfo_NoNewModelFeatureFlag) configureDeviceCreateToRespondWithError() configureDeviceReadSpecificToRespondSuccessfully(asaDeviceUsingSdc.Uid, asaSpecificDevice) configureAsaConfigReadToRespondWithCalls(asaConfig.Uid, []httpmock.Responder{ @@ -295,6 +416,7 @@ func TestAsaCreate(t *testing.T) { }, setupFunc: func(input asa.CreateInput) { + configureReadApiTokenInfoSuccessfully(readApiTokenInfo_NoNewModelFeatureFlag) configureDeviceCreateToRespondSuccessfully(asaDeviceUsingSdc) configureDeviceReadSpecificToRespondWithError(asaDeviceUsingSdc.Uid) configureAsaConfigReadToRespondWithCalls(asaConfig.Uid, []httpmock.Responder{ diff --git a/client/device/create.go b/client/device/create.go index f91f33cc..0ed3f582 100644 --- a/client/device/create.go +++ b/client/device/create.go @@ -8,19 +8,25 @@ import ( ) type CreateInput struct { - Name string `json:"name"` - DeviceType string `json:"deviceType"` - ConnectorUid string `json:"larUid,omitempty"` - ConnectorType string `json:"larType"` - SocketAddress string `json:"ipv4"` - Model bool `json:"model"` - - IgnoreCertificate bool `json:"ignoreCertificate"` + Name string `json:"name"` + DeviceType string `json:"deviceType"` + ConnectorUid string `json:"larUid,omitempty"` + ConnectorType string `json:"larType"` + SocketAddress string `json:"ipv4"` + Model bool `json:"model"` + IgnoreCertificate bool `json:"ignoreCertificate"` + Metadata *interface{} `json:"metadata,omitempty"` } type CreateOutput = ReadOutput -func NewCreateRequestInput(name, deviceType, connectorUid, connectorType, socketAddress string, model bool, ignoreCertificate bool) *CreateInput { +func NewCreateRequestInput(name, deviceType, connectorUid, connectorType, socketAddress string, model bool, ignoreCertificate bool, metadata interface{}) *CreateInput { + // convert interface{} to a pointer + var metadataPtr *interface{} = nil + if metadata != nil { + metadataPtr = &metadata + } + return &CreateInput{ Name: name, DeviceType: deviceType, @@ -29,6 +35,7 @@ func NewCreateRequestInput(name, deviceType, connectorUid, connectorType, socket SocketAddress: socketAddress, Model: model, IgnoreCertificate: ignoreCertificate, + Metadata: metadataPtr, } } diff --git a/client/device/genericssh/create.go b/client/device/genericssh/create.go index e3528562..cbf37aa0 100644 --- a/client/device/genericssh/create.go +++ b/client/device/genericssh/create.go @@ -28,7 +28,7 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr client.Logger.Println("creating generic ssh") - deviceInput := device.NewCreateRequestInput(createInp.Name, "GENERIC_SSH", createInp.ConnectorUid, createInp.ConnectorType, createInp.SocketAddress, false, false) + deviceInput := device.NewCreateRequestInput(createInp.Name, "GENERIC_SSH", createInp.ConnectorUid, createInp.ConnectorType, createInp.SocketAddress, false, false, nil) outp, err := device.Create(ctx, client, *deviceInput) if err != nil { return nil, err diff --git a/client/device/ios/create.go b/client/device/ios/create.go index 87e8bf10..c55f09a1 100644 --- a/client/device/ios/create.go +++ b/client/device/ios/create.go @@ -64,7 +64,7 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr client.Logger.Println("creating ios device") deviceCreateOutp, err := device.Create(ctx, client, *device.NewCreateRequestInput( - createInp.Name, "IOS", createInp.ConnectorUid, createInp.ConnectorType, createInp.SocketAddress, false, createInp.IgnoreCertificate, + createInp.Name, "IOS", createInp.ConnectorUid, createInp.ConnectorType, createInp.SocketAddress, false, createInp.IgnoreCertificate, nil, )) var createdResourceId *string = nil if deviceCreateOutp != nil { diff --git a/client/model/featureflag/featureflag.go b/client/model/featureflag/featureflag.go new file mode 100644 index 00000000..95f69fec --- /dev/null +++ b/client/model/featureflag/featureflag.go @@ -0,0 +1,13 @@ +package featureflag + +import "strings" + +type Type string + +const ( + AsaConfigurationObjectMigration Type = "asa_configuration_object_migration" +) + +func (t Type) String() string { + return strings.ToLower(string(t)) +} diff --git a/client/model/user/auth/info.go b/client/model/user/auth/info.go index 3d2e3cff..5054fcbc 100644 --- a/client/model/user/auth/info.go +++ b/client/model/user/auth/info.go @@ -1,6 +1,12 @@ package auth -import "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/user/auth/role" +import ( + "encoding/json" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/featureflag" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/user/auth/role" + "strings" +) type Info struct { UserAuthentication Authentication `json:"userAuthentication"` @@ -28,3 +34,26 @@ type Details struct { TenantDatabaseName string `json:"TenantDatabaseName"` TenantPayType string `json:"TenantPayType"` } + +func (info *Info) HasFeatureFlagEnabled(featureFlag featureflag.Type) bool { + featureMap := info.getFeatureMap() + enabled, ok := featureMap[featureFlag.String()] + return ok && enabled +} + +func (info *Info) getFeatureMap() map[string]bool { + featureMap := map[string]bool{} + err := json.Unmarshal([]byte(info.UserAuthentication.Details.TenantDbFeatures), &featureMap) + if err != nil { + // feature flag received is not a json + panic(fmt.Sprintf("feature flag received from authentication service is not in valid format: %s", info.UserAuthentication.Details.TenantDbFeatures)) + } + // convert feature flag keys to lowercase + // TODO: make use of lh-feature + normalizedFeatureMap := map[string]bool{} + for k, v := range featureMap { + normalizedFeatureMap[strings.ToLower(k)] = v + } + + return normalizedFeatureMap +}