From 1cc16e9b045f8bc05767d4be68c5b55361ba8667 Mon Sep 17 00:00:00 2001 From: erikostien-pingidentity <69643860+erikostien-pingidentity@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:03:50 -0600 Subject: [PATCH] PDI-1454: CLI v2 Command: Custom Request (#135) * PDI-1454: CLI v2 Command: Custom Request - Add Command Usage examples and Configuration format examples to Cobra usage messages where applicable. - Unite all Pingone and Pingfederate configuration options under 'service' parent, now used by both 'platform export' and 'request' commands. - Add 'authentication' parent in service configuration to allow the user to directly specify which authentication method to use. This removes the flag exclusivity requirement on the 'platform export' command, as well as removes authentication "hierarchy" in pingfederate go-client authentication logic. - Refactor MultiService custom map type to ExportServices []string type. - Add new command 'request' to send custom requests to PingIdentity services. Currently, only PingOne is supported. - Add multiple additional custom types to support Ints, authentication types, HTTP methods, and request services. - In the configuration file, support Yaml array lists for scopes, CA cert files, and Export services. - Use PingOne region codes instead of deprecated PingOne Regions. - Various testing fixes and additions. --- .../workflows/code-analysis-lint-test.yaml | 3 +- cmd/config/add_profile.go | 19 +- cmd/config/config.go | 19 +- cmd/config/delete_profile.go | 17 +- cmd/config/get.go | 19 +- cmd/config/get_test.go | 4 +- cmd/config/list_profiles.go | 7 +- cmd/config/set.go | 17 +- cmd/config/set_active_profile.go | 17 +- cmd/config/set_test.go | 4 +- cmd/config/unset.go | 17 +- cmd/config/unset_test.go | 4 +- cmd/config/view_profile.go | 17 +- cmd/platform/export.go | 159 ++++--- cmd/platform/export_test.go | 132 +++--- cmd/request/request.go | 64 +++ cmd/request/request_test.go | 107 +++++ cmd/root.go | 13 + internal/commands/config/get_internal_test.go | 6 +- internal/commands/config/set_internal.go | 58 ++- internal/commands/config/set_internal_test.go | 12 +- .../commands/config/unset_internal_test.go | 6 +- internal/commands/platform/export_internal.go | 158 ++++--- .../commands/platform/export_internal_test.go | 36 +- internal/commands/request/request_internal.go | 294 ++++++++++++ .../commands/request/request_internal_test.go | 202 ++++++++ internal/configuration/config/config.go | 12 - internal/configuration/configuration.go | 7 + internal/configuration/configuration_test.go | 8 +- internal/configuration/options/options.go | 130 ++++-- internal/configuration/platform/export.go | 437 ++---------------- internal/configuration/request/request.go | 115 +++++ internal/configuration/root/root.go | 4 +- .../configuration/services/pingfederate.go | 312 +++++++++++++ internal/configuration/services/pingone.go | 129 ++++++ internal/connector/common/common_utils.go | 2 +- internal/connector/exportable.go | 4 - internal/customtypes/export_format.go | 13 +- internal/customtypes/export_format_test.go | 9 +- internal/customtypes/export_services.go | 106 +++++ internal/customtypes/export_services_test.go | 95 ++++ internal/customtypes/http_method.go | 68 +++ internal/customtypes/http_method_test.go | 49 ++ internal/customtypes/int.go | 39 ++ internal/customtypes/int_test.go | 49 ++ internal/customtypes/multi_service.go | 133 ------ internal/customtypes/multi_service_test.go | 108 ----- internal/customtypes/output_format.go | 8 +- .../customtypes/pingfederate_auth_type.go | 59 +++ .../pingfederate_auth_type_test.go | 47 ++ internal/customtypes/pingone_auth_type.go | 51 ++ .../customtypes/pingone_auth_type_test.go | 47 ++ internal/customtypes/pingone_region.go | 56 --- internal/customtypes/pingone_region_code.go | 73 +++ ...on_test.go => pingone_region_code_test.go} | 22 +- internal/customtypes/request_services.go | 51 ++ internal/customtypes/request_services_test.go | 47 ++ internal/output/output.go | 4 +- internal/profiles/validate.go | 125 ++++- internal/profiles/viper.go | 20 +- internal/testing/testutils/utils.go | 24 +- .../testutils_terraform/terraform_utils.go | 3 +- .../testing/testutils_viper/viper_utils.go | 77 +-- 63 files changed, 2772 insertions(+), 1182 deletions(-) create mode 100644 cmd/request/request.go create mode 100644 cmd/request/request_test.go create mode 100644 internal/commands/request/request_internal.go create mode 100644 internal/commands/request/request_internal_test.go create mode 100644 internal/configuration/request/request.go create mode 100644 internal/configuration/services/pingfederate.go create mode 100644 internal/configuration/services/pingone.go create mode 100644 internal/customtypes/export_services.go create mode 100644 internal/customtypes/export_services_test.go create mode 100644 internal/customtypes/http_method.go create mode 100644 internal/customtypes/http_method_test.go create mode 100644 internal/customtypes/int.go create mode 100644 internal/customtypes/int_test.go delete mode 100644 internal/customtypes/multi_service.go delete mode 100644 internal/customtypes/multi_service_test.go create mode 100644 internal/customtypes/pingfederate_auth_type.go create mode 100644 internal/customtypes/pingfederate_auth_type_test.go create mode 100644 internal/customtypes/pingone_auth_type.go create mode 100644 internal/customtypes/pingone_auth_type_test.go delete mode 100644 internal/customtypes/pingone_region.go create mode 100644 internal/customtypes/pingone_region_code.go rename internal/customtypes/{pingone_region_test.go => pingone_region_code_test.go} (58%) create mode 100644 internal/customtypes/request_services.go create mode 100644 internal/customtypes/request_services_test.go diff --git a/.github/workflows/code-analysis-lint-test.yaml b/.github/workflows/code-analysis-lint-test.yaml index bfa4d1c..5bdeb81 100644 --- a/.github/workflows/code-analysis-lint-test.yaml +++ b/.github/workflows/code-analysis-lint-test.yaml @@ -126,8 +126,9 @@ jobs: PING_IDENTITY_CONFIG: ${{ secrets.PING_IDENTITY_CONFIG }} PINGCTL_PINGONE_WORKER_CLIENT_ID: ${{ secrets.PINGCTL_PINGONE_WORKER_CLIENT_ID }} PINGCTL_PINGONE_WORKER_CLIENT_SECRET: ${{ secrets.PINGCTL_PINGONE_WORKER_CLIENT_SECRET }} - PINGCTL_PINGONE_REGION: ${{ secrets.PINGCTL_PINGONE_REGION }} + PINGCTL_PINGONE_REGION_CODE: ${{ secrets.PINGCTL_PINGONE_REGION_CODE }} PINGCTL_PINGONE_WORKER_ENVIRONMENT_ID: ${{ secrets.PINGCTL_PINGONE_WORKER_ENVIRONMENT_ID }} + PINGCTL_PINGONE_EXPORT_ENVIRONMENT_ID: ${{ secrets.PINGCTL_PINGONE_EXPORT_ENVIRONMENT_ID }} PINGONE_CLIENT_ID: ${{ secrets.PINGONE_CLIENT_ID }} PINGONE_CLIENT_SECRET: ${{ secrets.PINGONE_CLIENT_SECRET }} PINGONE_ENVIRONMENT_ID: ${{ secrets.PINGONE_ENVIRONMENT_ID }} diff --git a/cmd/config/add_profile.go b/cmd/config/add_profile.go index 5469637..d6b1836 100644 --- a/cmd/config/add_profile.go +++ b/cmd/config/add_profile.go @@ -10,17 +10,22 @@ import ( "github.com/spf13/cobra" ) +const ( + addProfilecommandExamples = `Command Usage Examples: +pingctl config add-profile +pingctl config add-profile --name myprofile --description "My Profile desc" +pingctl config add-profile --set-active=true` +) + func NewConfigAddProfileCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(0), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config add-profile -pingctl config add-profile --name myprofile --description "My Profile desc" -pingctl config add-profile --set-active=true`, - Long: `Add a new configuration profile to pingctl.`, - RunE: configAddProfileRunE, - Short: "Add a new configuration profile to pingctl.", - Use: "add-profile [flags]", + Example: addProfilecommandExamples, + Long: `Add a new configuration profile to pingctl.`, + RunE: configAddProfileRunE, + Short: "Add a new configuration profile to pingctl.", + Use: "add-profile [flags]", } cmd.Flags().AddFlag(options.ConfigAddProfileNameOption.Flag) diff --git a/cmd/config/config.go b/cmd/config/config.go index 4c91d19..19aed0b 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -10,17 +10,22 @@ import ( "github.com/spf13/cobra" ) +const ( + configCommandExamples = `Command Usage Examples: +pingctl config +pingctl config --profile myprofile +pingctl config --name myprofile --description "My Profile"` +) + func NewConfigCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(0), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config -pingctl config --profile myprofile -pingctl config --name myprofile --description "My Profile"`, - Long: `Update an existing configuration profile's name and description. See subcommands for more profile configuration management options.`, - RunE: configRunE, - Short: "Update an existing configuration profile's name and description. See subcommands for more profile configuration management options.", - Use: "config [flags]", + Example: configCommandExamples, + Long: `Update an existing configuration profile's name and description. See subcommands for more profile configuration management options.`, + RunE: configRunE, + Short: "Update an existing configuration profile's name and description. See subcommands for more profile configuration management options.", + Use: "config [flags]", } // Add subcommands diff --git a/cmd/config/delete_profile.go b/cmd/config/delete_profile.go index 781ac22..150cd4e 100644 --- a/cmd/config/delete_profile.go +++ b/cmd/config/delete_profile.go @@ -10,16 +10,21 @@ import ( "github.com/spf13/cobra" ) +const ( + deleteProfileCommandExamples = `Command Usage Examples: +pingctl config delete-profile +pingctl config delete-profile --profile myprofile` +) + func NewConfigDeleteProfileCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(0), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config delete-profile -pingctl config delete-profile --profile myprofile`, - Long: `Delete a configuration profile from pingctl.`, - RunE: configDeleteProfileRunE, - Short: "Delete a configuration profile from pingctl.", - Use: "delete-profile [flags]", + Example: deleteProfileCommandExamples, + Long: `Delete a configuration profile from pingctl.`, + RunE: configDeleteProfileRunE, + Short: "Delete a configuration profile from pingctl.", + Use: "delete-profile [flags]", } cmd.Flags().AddFlag(options.ConfigDeleteProfileOption.Flag) diff --git a/cmd/config/get.go b/cmd/config/get.go index 2d23603..396bd04 100644 --- a/cmd/config/get.go +++ b/cmd/config/get.go @@ -8,17 +8,22 @@ import ( "github.com/spf13/cobra" ) +const ( + configGetCommandExamples = `Command Usage Examples: +pingctl config get pingone +pingctl config get --profile myProfile color +pingctl config get service.pingone.authentication.worker.environmentID` +) + func NewConfigGetCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(1), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config get pingone -pingctl config get --profile myProfile pingctl.color -pingctl config get pingone.export.environmentID`, - Long: `Get pingctl configuration settings.`, - RunE: configGetRunE, - Short: "Get pingctl configuration settings.", - Use: "get [flags] key", + Example: configGetCommandExamples, + Long: `Get pingctl configuration settings.`, + RunE: configGetRunE, + Short: "Get pingctl configuration settings.", + Use: "get [flags] key", } cmd.Flags().AddFlag(options.ConfigGetProfileOption.Flag) diff --git a/cmd/config/get_test.go b/cmd/config/get_test.go index d1519e8..e5dcd4d 100644 --- a/cmd/config/get_test.go +++ b/cmd/config/get_test.go @@ -23,13 +23,13 @@ func TestConfigGetCmd_TooManyArgs(t *testing.T) { // Test Config Get Command Executes when provided a full key func TestConfigGetCmd_FullKey(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "get", options.PlatformExportPingoneWorkerClientIDOption.ViperKey) + err := testutils_cobra.ExecutePingctl(t, "config", "get", options.PingoneAuthenticationWorkerClientIDOption.ViperKey) testutils.CheckExpectedError(t, err, nil) } // Test Config Get Command Executes when provided a partial key func TestConfigGetCmd_PartialKey(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "get", "export.pingone") + err := testutils_cobra.ExecutePingctl(t, "config", "get", "service.pingone") testutils.CheckExpectedError(t, err, nil) } diff --git a/cmd/config/list_profiles.go b/cmd/config/list_profiles.go index 996b4ae..0339edd 100644 --- a/cmd/config/list_profiles.go +++ b/cmd/config/list_profiles.go @@ -7,11 +7,16 @@ import ( "github.com/spf13/cobra" ) +const ( + listProfilesCommandExamples = `Command Usage Examples: +pingctl config list-profiles` +) + func NewConfigListProfilesCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(0), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config list-profiles`, + Example: listProfilesCommandExamples, Long: `List all configuration profiles from pingctl.`, RunE: configListProfilesRunE, Short: "List all configuration profiles from pingctl.", diff --git a/cmd/config/set.go b/cmd/config/set.go index a999ccc..b7a9b2f 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -8,16 +8,21 @@ import ( "github.com/spf13/cobra" ) +const ( + configSetCommandExamples = `Command Usage Examples: +pingctl config set color=true +pingctl config set --profile myProfile service.pingone.regionCode=AP` +) + func NewConfigSetCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(1), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config set pingctl.color=true -pingctl config set --profile myProfile pingone.region=AsiaPacific`, - Long: `Set pingctl configuration settings.`, - RunE: configSetRunE, - Short: "Set pingctl configuration settings.", - Use: "set [flags] key=value", + Example: configSetCommandExamples, + Long: `Set pingctl configuration settings.`, + RunE: configSetRunE, + Short: "Set pingctl configuration settings.", + Use: "set [flags] key=value", } cmd.Flags().AddFlag(options.ConfigSetProfileOption.Flag) diff --git a/cmd/config/set_active_profile.go b/cmd/config/set_active_profile.go index b36b49a..7bc979f 100644 --- a/cmd/config/set_active_profile.go +++ b/cmd/config/set_active_profile.go @@ -10,16 +10,21 @@ import ( "github.com/spf13/cobra" ) +const ( + setActiveProfileCommandExamples = `Command Usage Examples: +pingctl config set-active-profile +pingctl config set-active-profile --profile myprofile` +) + func NewConfigSetActiveProfileCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(0), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config set-active-profile -pingctl config set-active-profile --profile myprofile`, - Long: `Set a configuration profile as the in-use profile for pingctl.`, - RunE: configSetActiveProfileRunE, - Short: "Set a configuration profile as the in-use profile for pingctl.", - Use: "set-active-profile [flags]", + Example: setActiveProfileCommandExamples, + Long: `Set a configuration profile as the in-use profile for pingctl.`, + RunE: configSetActiveProfileRunE, + Short: "Set a configuration profile as the in-use profile for pingctl.", + Use: "set-active-profile [flags]", } cmd.Flags().AddFlag(options.ConfigSetActiveProfileOption.Flag) diff --git a/cmd/config/set_test.go b/cmd/config/set_test.go index 629947f..3108b3d 100644 --- a/cmd/config/set_test.go +++ b/cmd/config/set_test.go @@ -46,14 +46,14 @@ func TestConfigSetCmd_InvalidValueType(t *testing.T) { // Test Config Set Command Fails when no value is provided func TestConfigSetCmd_NoValueProvided(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key 'pingctl\.color' is empty\. Use 'pingctl config unset pingctl\.color' to unset the key$` + expectedErrorPattern := `^failed to set configuration: value for key '.*' is empty\. Use 'pingctl config unset .*' to unset the key$` err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=", options.RootColorOption.ViperKey)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Config Set Command for key 'pingone.worker.clientId' updates viper configuration func TestConfigSetCmd_CheckViperConfig(t *testing.T) { - viperKey := options.PlatformExportPingoneWorkerClientIDOption.ViperKey + viperKey := options.PingoneAuthenticationWorkerClientIDOption.ViperKey viperNewUUID := "12345678-1234-1234-1234-123456789012" err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=%s", viperKey, viperNewUUID)) diff --git a/cmd/config/unset.go b/cmd/config/unset.go index d4bc593..0ac70c6 100644 --- a/cmd/config/unset.go +++ b/cmd/config/unset.go @@ -8,16 +8,21 @@ import ( "github.com/spf13/cobra" ) +const ( + configUnsetCommandExamples = `Command Usage Examples: +pingctl config unset color +pingctl config unset --profile myProfile service.pingone.regionCode` +) + func NewConfigUnsetCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(1), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config unset pingctl.color -pingctl config unset --profile myProfile pingone.region`, - Long: `Unset pingctl configuration settings.`, - RunE: configUnsetRunE, - Short: "Unset pingctl configuration settings.", - Use: "unset [flags] key", + Example: configUnsetCommandExamples, + Long: `Unset pingctl configuration settings.`, + RunE: configUnsetRunE, + Short: "Unset pingctl configuration settings.", + Use: "unset [flags] key", } cmd.Flags().AddFlag(options.ConfigUnsetProfileOption.Flag) diff --git a/cmd/config/unset_test.go b/cmd/config/unset_test.go index a7aaa6b..87ae82b 100644 --- a/cmd/config/unset_test.go +++ b/cmd/config/unset_test.go @@ -39,8 +39,8 @@ func TestConfigUnsetCmd_InvalidKey(t *testing.T) { // Test Config Unset Command for key 'pingone.worker.clientId' updates viper configuration func TestConfigUnsetCmd_CheckViperConfig(t *testing.T) { - viperKey := options.PlatformExportPingoneWorkerClientIDOption.ViperKey - viperOldValue := os.Getenv(options.PlatformExportPingoneWorkerClientIDOption.EnvVar) + viperKey := options.PingoneAuthenticationWorkerClientIDOption.ViperKey + viperOldValue := os.Getenv(options.PingoneAuthenticationWorkerClientIDOption.EnvVar) err := testutils_cobra.ExecutePingctl(t, "config", "unset", viperKey) testutils.CheckExpectedError(t, err, nil) diff --git a/cmd/config/view_profile.go b/cmd/config/view_profile.go index b0a4c98..cf81af4 100644 --- a/cmd/config/view_profile.go +++ b/cmd/config/view_profile.go @@ -8,16 +8,21 @@ import ( "github.com/spf13/cobra" ) +const ( + viewProfileCommandExamples = `Command Usage Examples: +pingctl config view-profile +pingctl config view-profile --profile myprofile` +) + func NewConfigViewProfileCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(0), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config view-profile -pingctl config view-profile --profile myprofile`, - Long: `View a configuration profile from pingctl.`, - RunE: configViewProfileRunE, - Short: "View a configuration profile from pingctl.", - Use: "view-profile [flags]", + Example: viewProfileCommandExamples, + Long: `View a configuration profile from pingctl.`, + RunE: configViewProfileRunE, + Short: "View a configuration profile from pingctl.", + Use: "view-profile [flags]", } cmd.Flags().AddFlag(options.ConfigViewProfileOption.Flag) diff --git a/cmd/platform/export.go b/cmd/platform/export.go index 7340a6a..f59eac2 100644 --- a/cmd/platform/export.go +++ b/cmd/platform/export.go @@ -1,6 +1,8 @@ package platform import ( + "fmt" + "github.com/pingidentity/pingctl/cmd/common" platform_internal "github.com/pingidentity/pingctl/internal/commands/platform" "github.com/pingidentity/pingctl/internal/configuration/options" @@ -8,23 +10,70 @@ import ( "github.com/spf13/cobra" ) -func NewExportCommand() *cobra.Command { - cmd := &cobra.Command{ - Args: common.ExactArgs(0), - DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl platform export +const ( + commandExamples = `Command Usage Examples: +pingctl platform export pingctl platform export --output-directory dir --overwrite pingctl platform export --export-format HCL -pingctl platform export --service pingone-platform --service pingone-sso -pingctl platform export --service pingone-platform --pingone-client-environment-id envID --pingone-worker-client-id clientID --pingone-worker-client-secret clientSecret --pingone-region region +pingctl platform export --services pingone-platform,pingone-sso +pingctl platform export --services pingone-platform --pingone-client-environment-id envID --pingone-worker-client-id clientID --pingone-worker-client-secret clientSecret --pingone-region-code regionCode pingctl platform export --service pingfederate --pingfederate-username user --pingfederate-password password pingctl platform export --service pingfederate --pingfederate-client-id clientID --pingfederate-client-secret clientSecret --pingfederate-token-url tokenURL pingctl platform export --service pingfederate --pingfederate-access-token accessToken -pingctl platform export --service pingfederate --x-bypass-external-validation=false --ca-certificate-pem-files "/path/to/cert.pem,/path/to/cert2.pem" --insecure-trust-all-tls=false`, - Long: `Export configuration-as-code packages for the Ping Platform.`, - Short: "Export configuration-as-code packages for the Ping Platform.", - RunE: exportRunE, - Use: "export [flags]", +pingctl platform export --service pingfederate --x-bypass-external-validation=false --ca-certificate-pem-files "/path/to/cert.pem,/path/to/cert2.pem" --insecure-trust-all-tls=false` + + profileConfigurationFormat = `Profile Configuration Format: +export: + format: + services: + - + - + outputDirectory: + overwrite: + pingone: + environmentID: +service: + pingfederate: + httpsHost: + adminAPIPath: + x-bypass-external-validation: + ca-certificate-pem-files: + - + - + insecure-trust-all-tls: + authentication: + type: + basicAuth: + username: + password: + accessTokenAuth: + accessToken: + clientCredentialsAuth: + clientID: + clientSecret: + tokenURL: + scopes: + - + - + pingone: + regionCode: + authentication: + type: + worker: + clientID: + clientSecret: + environmentID: ` +) + +func NewExportCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: fmt.Sprintf("%s\n\n%s", commandExamples, profileConfigurationFormat), + Long: `Export configuration-as-code packages for the Ping Platform.`, + Short: "Export configuration-as-code packages for the Ping Platform.", + RunE: exportRunE, + Use: "export [flags]", } initGeneralExportFlags(cmd) @@ -33,7 +82,6 @@ pingctl platform export --service pingfederate --x-bypass-external-validation=fa initPingFederateBasicAuthFlags(cmd) initPingFederateAccessTokenFlags(cmd) initPingFederateClientCredentialsFlags(cmd) - markPingFederateFlagsExclusive(cmd) return cmd } @@ -51,81 +99,62 @@ func initGeneralExportFlags(cmd *cobra.Command) { cmd.Flags().AddFlag(options.PlatformExportServiceOption.Flag) cmd.Flags().AddFlag(options.PlatformExportOutputDirectoryOption.Flag) cmd.Flags().AddFlag(options.PlatformExportOverwriteOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingoneEnvironmentIDOption.Flag) } func initPingOneExportFlags(cmd *cobra.Command) { - cmd.Flags().AddFlag(options.PlatformExportPingoneWorkerEnvironmentIDOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingoneExportEnvironmentIDOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingoneWorkerClientIDOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingoneWorkerClientSecretOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingoneRegionOption.Flag) + cmd.Flags().AddFlag(options.PingoneAuthenticationWorkerEnvironmentIDOption.Flag) + cmd.Flags().AddFlag(options.PingoneAuthenticationWorkerClientIDOption.Flag) + cmd.Flags().AddFlag(options.PingoneAuthenticationWorkerClientSecretOption.Flag) + cmd.Flags().AddFlag(options.PingoneRegionCodeOption.Flag) + cmd.Flags().AddFlag(options.PingoneAuthenticationTypeOption.Flag) cmd.MarkFlagsRequiredTogether( - options.PlatformExportPingoneWorkerEnvironmentIDOption.CobraParamName, - options.PlatformExportPingoneWorkerClientIDOption.CobraParamName, - options.PlatformExportPingoneWorkerClientSecretOption.CobraParamName, - options.PlatformExportPingoneRegionOption.CobraParamName) + options.PingoneAuthenticationWorkerEnvironmentIDOption.CobraParamName, + options.PingoneAuthenticationWorkerClientIDOption.CobraParamName, + options.PingoneAuthenticationWorkerClientSecretOption.CobraParamName, + options.PingoneRegionCodeOption.CobraParamName, + ) + } func initPingFederateGeneralFlags(cmd *cobra.Command) { - cmd.Flags().AddFlag(options.PlatformExportPingfederateHTTPSHostOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingfederateAdminAPIPathOption.Flag) + cmd.Flags().AddFlag(options.PingfederateHTTPSHostOption.Flag) + cmd.Flags().AddFlag(options.PingfederateAdminAPIPathOption.Flag) cmd.MarkFlagsRequiredTogether( - options.PlatformExportPingfederateHTTPSHostOption.CobraParamName, - options.PlatformExportPingfederateAdminAPIPathOption.CobraParamName) + options.PingfederateHTTPSHostOption.CobraParamName, + options.PingfederateAdminAPIPathOption.CobraParamName) - cmd.Flags().AddFlag(options.PlatformExportPingfederateXBypassExternalValidationHeaderOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingfederateCACertificatePemFilesOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingfederateInsecureTrustAllTLSOption.Flag) + cmd.Flags().AddFlag(options.PingfederateXBypassExternalValidationHeaderOption.Flag) + cmd.Flags().AddFlag(options.PingfederateCACertificatePemFilesOption.Flag) + cmd.Flags().AddFlag(options.PingfederateInsecureTrustAllTLSOption.Flag) + cmd.Flags().AddFlag(options.PingfederateAuthenticationTypeOption.Flag) } func initPingFederateBasicAuthFlags(cmd *cobra.Command) { - cmd.Flags().AddFlag(options.PlatformExportPingfederateUsernameOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingfederatePasswordOption.Flag) + cmd.Flags().AddFlag(options.PingfederateBasicAuthUsernameOption.Flag) + cmd.Flags().AddFlag(options.PingfederateBasicAuthPasswordOption.Flag) cmd.MarkFlagsRequiredTogether( - options.PlatformExportPingfederateUsernameOption.CobraParamName, - options.PlatformExportPingfederatePasswordOption.CobraParamName) + options.PingfederateBasicAuthUsernameOption.CobraParamName, + options.PingfederateBasicAuthPasswordOption.CobraParamName, + ) } func initPingFederateAccessTokenFlags(cmd *cobra.Command) { - cmd.Flags().AddFlag(options.PlatformExportPingfederateAccessTokenOption.Flag) + cmd.Flags().AddFlag(options.PingfederateAccessTokenAuthAccessTokenOption.Flag) } func initPingFederateClientCredentialsFlags(cmd *cobra.Command) { - cmd.Flags().AddFlag(options.PlatformExportPingfederateClientIDOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingfederateClientSecretOption.Flag) - cmd.Flags().AddFlag(options.PlatformExportPingfederateTokenURLOption.Flag) + cmd.Flags().AddFlag(options.PingfederateClientCredentialsAuthClientIDOption.Flag) + cmd.Flags().AddFlag(options.PingfederateClientCredentialsAuthClientSecretOption.Flag) + cmd.Flags().AddFlag(options.PingfederateClientCredentialsAuthTokenURLOption.Flag) cmd.MarkFlagsRequiredTogether( - options.PlatformExportPingfederateClientIDOption.CobraParamName, - options.PlatformExportPingfederateClientSecretOption.CobraParamName, - options.PlatformExportPingfederateTokenURLOption.CobraParamName) - - cmd.Flags().AddFlag(options.PlatformExportPingfederateScopesOption.Flag) -} + options.PingfederateClientCredentialsAuthClientIDOption.CobraParamName, + options.PingfederateClientCredentialsAuthClientSecretOption.CobraParamName, + options.PingfederateClientCredentialsAuthTokenURLOption.CobraParamName) -func markPingFederateFlagsExclusive(cmd *cobra.Command) { - // The username flag cannot be used with the access token or client credentials authentication methods - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateAccessTokenOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateClientIDOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateClientSecretOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateTokenURLOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateScopesOption.CobraParamName) - - // The password flag cannot be used with the access token or client credentials authentication methods - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateAccessTokenOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateClientIDOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateClientSecretOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateTokenURLOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateScopesOption.CobraParamName) - - // The access token flag cannot be used with the client credentials authentication method - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateAccessTokenOption.CobraParamName, options.PlatformExportPingfederateClientIDOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateAccessTokenOption.CobraParamName, options.PlatformExportPingfederateClientSecretOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateAccessTokenOption.CobraParamName, options.PlatformExportPingfederateTokenURLOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateAccessTokenOption.CobraParamName, options.PlatformExportPingfederateScopesOption.CobraParamName) - - // Client credential flag exclusivity is already defined above. + cmd.Flags().AddFlag(options.PingfederateClientCredentialsAuthScopesOption.Flag) } diff --git a/cmd/platform/export_test.go b/cmd/platform/export_test.go index 14a6345..d53015f 100644 --- a/cmd/platform/export_test.go +++ b/cmd/platform/export_test.go @@ -45,7 +45,7 @@ func TestPlatformExportCmd_HelpFlag(t *testing.T) { testutils.CheckExpectedError(t, err, nil) } -// Test Platform Export Command --service flag +// Test Platform Export Command --services flag func TestPlatformExportCmd_ServiceFlag(t *testing.T) { outputDir := t.TempDir() @@ -56,29 +56,29 @@ func TestPlatformExportCmd_ServiceFlag(t *testing.T) { testutils.CheckExpectedError(t, err, nil) } -// Test Platform Export Command --service flag with invalid service +// Test Platform Export Command --services flag with invalid service func TestPlatformExportCmd_ServiceFlagInvalidService(t *testing.T) { - expectedErrorPattern := `^invalid argument "invalid" for "-s, --services" flag: unrecognized service 'invalid'\. Must be one of: [a-z-\s,]+$` + expectedErrorPattern := `^invalid argument ".*" for "-s, --services" flag: failed to set ExportServices: Invalid service: .*\. Allowed services: .*$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--services", "invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test Platform Export Command --export-format flag +// Test Platform Export Command --format flag func TestPlatformExportCmd_ExportFormatFlag(t *testing.T) { outputDir := t.TempDir() err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--export-format", "HCL", + "--format", "HCL", "--overwrite", "true", "--services", "pingone-protect") testutils.CheckExpectedError(t, err, nil) } -// Test Platform Export Command --export-format flag with invalid format +// Test Platform Export Command --format flag with invalid format func TestPlatformExportCmd_ExportFormatFlagInvalidFormat(t *testing.T) { - expectedErrorPattern := `^invalid argument "invalid" for "-e, --export-format" flag: unrecognized export format 'invalid'\. Must be one of: [A-Z]+$` - err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--export-format", "invalid") + expectedErrorPattern := `^invalid argument ".*" for "-f, --format" flag: unrecognized export format '.*'\. Must be one of: .*$` + err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--format", "invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -158,18 +158,18 @@ func TestPlatformExportCmd_PingOneWorkerEnvironmentIdFlag(t *testing.T) { "--output-directory", outputDir, "--overwrite", "true", "--services", "pingone-protect", - "--pingone-worker-environment-id", os.Getenv(options.PlatformExportPingoneWorkerEnvironmentIDOption.EnvVar), - "--pingone-worker-client-id", os.Getenv(options.PlatformExportPingoneWorkerClientIDOption.EnvVar), - "--pingone-worker-client-secret", os.Getenv(options.PlatformExportPingoneWorkerClientSecretOption.EnvVar), - "--pingone-region", os.Getenv(options.PlatformExportPingoneRegionOption.EnvVar)) + "--pingone-worker-environment-id", os.Getenv(options.PingoneAuthenticationWorkerEnvironmentIDOption.EnvVar), + "--pingone-worker-client-id", os.Getenv(options.PingoneAuthenticationWorkerClientIDOption.EnvVar), + "--pingone-worker-client-secret", os.Getenv(options.PingoneAuthenticationWorkerClientSecretOption.EnvVar), + "--pingone-region-code", os.Getenv(options.PingoneRegionCodeOption.EnvVar)) testutils.CheckExpectedError(t, err, nil) } // Test Platform Export Command fails when not provided required pingone flags together func TestPlatformExportCmd_PingOneWorkerEnvironmentIdFlagRequiredTogether(t *testing.T) { - expectedErrorPattern := `^if any flags in the group \[pingone-worker-environment-id pingone-worker-client-id pingone-worker-client-secret pingone-region] are set they must all be set; missing \[pingone-region pingone-worker-client-id pingone-worker-client-secret]$` + expectedErrorPattern := `^if any flags in the group \[pingone-worker-environment-id pingone-worker-client-id pingone-worker-client-secret pingone-region-code] are set they must all be set; missing \[pingone-region-code pingone-worker-client-id pingone-worker-client-secret]$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--pingone-worker-environment-id", os.Getenv(options.PlatformExportPingoneWorkerEnvironmentIDOption.EnvVar)) + "--pingone-worker-environment-id", os.Getenv(options.PingoneAuthenticationWorkerEnvironmentIDOption.EnvVar)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -181,8 +181,10 @@ func TestPlatformExportCmd_PingFederateBasicAuthFlags(t *testing.T) { "--output-directory", outputDir, "--overwrite", "true", "--services", "pingfederate", - "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PingfederateBasicAuthUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PingfederateBasicAuthPasswordOption.EnvVar), + "--pingfederate-authentication-type", "basicAuth", + ) testutils.CheckExpectedError(t, err, nil) } @@ -204,7 +206,9 @@ func TestPlatformExportCmd_PingFederateBasicAuthFlagsInvalid(t *testing.T) { "--overwrite", "true", "--services", "pingfederate", "--pingfederate-username", "Administrator", - "--pingfederate-password", "invalid") + "--pingfederate-password", "invalid", + "--pingfederate-authentication-type", "basicAuth", + ) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -216,10 +220,12 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlags(t *testing.T) "--output-directory", outputDir, "--overwrite", "true", "--services", "pingfederate", - "--pingfederate-client-id", os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), - "--pingfederate-client-secret", os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), - "--pingfederate-scopes", os.Getenv(options.PlatformExportPingfederateScopesOption.EnvVar), - "--pingfederate-token-url", os.Getenv(options.PlatformExportPingfederateTokenURLOption.EnvVar)) + "--pingfederate-client-id", os.Getenv(options.PingfederateClientCredentialsAuthClientIDOption.EnvVar), + "--pingfederate-client-secret", os.Getenv(options.PingfederateClientCredentialsAuthClientSecretOption.EnvVar), + "--pingfederate-scopes", os.Getenv(options.PingfederateClientCredentialsAuthScopesOption.EnvVar), + "--pingfederate-token-url", os.Getenv(options.PingfederateClientCredentialsAuthTokenURLOption.EnvVar), + "--pingfederate-authentication-type", "clientCredentialsAuth", + ) testutils.CheckExpectedError(t, err, nil) } @@ -242,7 +248,9 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsInvalid(t *test "--services", "pingfederate", "--pingfederate-client-id", "test", "--pingfederate-client-secret", "invalid", - "--pingfederate-token-url", "https://localhost:9031/as/token.oauth2") + "--pingfederate-token-url", "https://localhost:9031/as/token.oauth2", + "--pingfederate-authentication-type", "clientCredentialsAuth", + ) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -255,49 +263,11 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsInvalidTokenURL "--output-directory", outputDir, "--overwrite", "true", "--services", "pingfederate", - "--pingfederate-client-id", os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), - "--pingfederate-client-secret", os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), - "--pingfederate-token-url", "https://localhost:9031/as/invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export command fails when basic auth flags are provided with client credentials auth flags -func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsWithUsername(t *testing.T) { - expectedErrorPattern := `^if any flags in the group \[.*\] are set none of the others can be; \[.*\] were all set$` - err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--pingfederate-client-id", os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), - "--pingfederate-client-secret", os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), - "--pingfederate-token-url", os.Getenv(options.PlatformExportPingfederateTokenURLOption.EnvVar), - "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) - - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export command fails when access token flags are provided with client credentials auth flags -func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsWithAccessToken(t *testing.T) { - expectedErrorPattern := `^if any flags in the group \[.*\] are set none of the others can be; \[.*\] were all set$` - err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--pingfederate-client-id", os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), - "--pingfederate-client-secret", os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), - "--pingfederate-token-url", os.Getenv(options.PlatformExportPingfederateTokenURLOption.EnvVar), - "--pingfederate-access-token", "token") - - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Platform Export command fails with invalid basic auth flags while there is valid client credentials in config. -// This is because cobra/viper model prioritizes flags over config values. -func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsWithInvalidBasicAuth(t *testing.T) { - outputDir := t.TempDir() - - expectedErrorPattern := `^failed to export 'pingfederate' service: failed to export resource .*\. err: .* Request for resource '.*' was not successful\.\s+Response Code: 401 Unauthorized\s+Response Body: {{"resultId":"invalid_credentials","message":"The credentials you provided were not recognized\."}}\s+Error: 401 Unauthorized$` - err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), - "--pingfederate-password", "invalid", - "--output-directory", outputDir, - "--services", "pingfederate", - "--overwrite", "true") + "--pingfederate-client-id", os.Getenv(options.PingfederateClientCredentialsAuthClientIDOption.EnvVar), + "--pingfederate-client-secret", os.Getenv(options.PingfederateClientCredentialsAuthClientSecretOption.EnvVar), + "--pingfederate-token-url", "https://localhost:9031/as/invalid", + "--pingfederate-authentication-type", "clientCredentialsAuth", + ) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -310,8 +280,10 @@ func TestPlatformExportCmd_PingFederateXBypassHeaderFlag(t *testing.T) { "--overwrite", "true", "--services", "pingfederate", "--pingfederate-x-bypass-external-validation-header=true", - "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PingfederateBasicAuthUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PingfederateBasicAuthPasswordOption.EnvVar), + "--pingfederate-authentication-type", "basicAuth", + ) testutils.CheckExpectedError(t, err, nil) } @@ -324,8 +296,10 @@ func TestPlatformExportCmd_PingFederateTrustAllTLSFlag(t *testing.T) { "--overwrite", "true", "--services", "pingfederate", "--pingfederate-insecure-trust-all-tls=true", - "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PingfederateBasicAuthUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PingfederateBasicAuthPasswordOption.EnvVar), + "--pingfederate-authentication-type", "basicAuth", + ) testutils.CheckExpectedError(t, err, nil) } @@ -333,14 +307,16 @@ func TestPlatformExportCmd_PingFederateTrustAllTLSFlag(t *testing.T) { func TestPlatformExportCmd_PingFederateTrustAllTLSFlagFalse(t *testing.T) { outputDir := t.TempDir() - expectedErrorPattern := `^failed to export 'pingfederate' service: failed to export resource .*. err: .* Request for resource '.*' was not successful. Response is nil. Error: Get "https.*": tls: failed to verify certificate: x509: certificate signed by unknown authority$` + expectedErrorPattern := `^failed to export '.*' service: failed to export resource .*\. err: .* Request for resource '.*' was not successful\. Response is nil\. Error: Get "https.*": tls: failed to verify certificate: x509: certificate signed by unknown authority$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, "--overwrite", "true", "--services", "pingfederate", "--pingfederate-insecure-trust-all-tls=false", - "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PingfederateBasicAuthUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PingfederateBasicAuthPasswordOption.EnvVar), + "--pingfederate-authentication-type", "basicAuth", + ) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -354,8 +330,10 @@ func TestPlatformExportCmd_PingFederateCaCertificatePemFiles(t *testing.T) { "--services", "pingfederate", "--pingfederate-insecure-trust-all-tls=false", "--pingfederate-ca-certificate-pem-files", "testdata/ssl-server-crt.pem", - "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PingfederateBasicAuthUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PingfederateBasicAuthPasswordOption.EnvVar), + "--pingfederate-authentication-type", "basicAuth", + ) testutils.CheckExpectedError(t, err, nil) } @@ -365,7 +343,9 @@ func TestPlatformExportCmd_PingFederateCaCertificatePemFilesInvalid(t *testing.T err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--services", "pingfederate", "--pingfederate-ca-certificate-pem-files", "invalid/crt.pem", - "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PingfederateBasicAuthUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PingfederateBasicAuthPasswordOption.EnvVar), + "--pingfederate-authentication-type", "basicAuth", + ) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/cmd/request/request.go b/cmd/request/request.go new file mode 100644 index 0000000..f3fc41a --- /dev/null +++ b/cmd/request/request.go @@ -0,0 +1,64 @@ +package request + +import ( + "fmt" + + "github.com/pingidentity/pingctl/cmd/common" + request_internal "github.com/pingidentity/pingctl/internal/commands/request" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/logger" + "github.com/spf13/cobra" +) + +const ( + commandExamples = `Command Usage Examples: +pingctl request --service pingone environments +pingctl request --service pingone --http-method GET environments/{{environmentID}} +pingctl request --service pingone --http-method POST --data {{raw-data}} environments +pingctl request --service pingone --http-method POST --data @{{filepath}} environments +pingctl request --service pingone --http-method DELETE environments/{{environmentID}}` + + profileConfigurationFormat = `Profile Configuration Format: +request: + data: @ OR + http-method: + service: +service: + pingone: + regionCode: + authentication: + type: + worker: + clientID: + clientSecret: + environmentID: ` +) + +func NewRequestCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(1), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: fmt.Sprintf("%s\n\n%s", commandExamples, profileConfigurationFormat), + Long: `Send a custom request to a Ping Service.`, + RunE: requestRunE, + Short: "Send a custom request to a Ping Service.", + Use: "request [flags] API_URI", + } + + cmd.Flags().AddFlag(options.RequestHTTPMethodOption.Flag) + cmd.Flags().AddFlag(options.RequestServiceOption.Flag) + cmd.Flags().AddFlag(options.RequestDataOption.Flag) + + return cmd +} + +func requestRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("Request Subcommand Called.") + + if err := request_internal.RunInternalRequest(args[0]); err != nil { + return err + } + + return nil +} diff --git a/cmd/request/request_test.go b/cmd/request/request_test.go new file mode 100644 index 0000000..c644f63 --- /dev/null +++ b/cmd/request/request_test.go @@ -0,0 +1,107 @@ +package request_test + +import ( + "encoding/json" + "fmt" + "io" + "os" + "regexp" + "testing" + + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" +) + +// Test Request Command Executes without issue +func TestRequestCmd_Execute(t *testing.T) { + originalStdout := os.Stdout + pipeReader, pipeWriter, err := os.Pipe() + if err != nil { + t.Fatalf("Failed to create pipe: %v", err) + } + defer pipeReader.Close() + os.Stdout = pipeWriter + + err = testutils_cobra.ExecutePingctl(t, "request", + "--service", "pingone", + "--http-method", "GET", + "environments", + ) + testutils.CheckExpectedError(t, err, nil) + + os.Stdout = originalStdout + pipeWriter.Close() + + pipeReaderOut, err := io.ReadAll(pipeReader) + if err != nil { + t.Fatalf("Failed to read from pipe: %v", err) + } + + // Capture response json body + captureGroupName := "BodyJSON" + re := regexp.MustCompile(fmt.Sprintf(`(?s)^.*response: (?P<%s>\{.*\}).*status: .*$`, captureGroupName)) + matchData := re.FindSubmatch(pipeReaderOut) + + for index, name := range re.SubexpNames() { + if name == captureGroupName { + bodyJSON := matchData[index] + + // Check for valid JSON + if !json.Valid(bodyJSON) { + t.Errorf("Invalid JSON: %s", bodyJSON) + } + } + } +} + +// Test Request Command fails when provided too many arguments +func TestRequestCmd_Execute_TooManyArguments(t *testing.T) { + expectedErrorPattern := `accepts 1 arg\(s\), received 2` + err := testutils_cobra.ExecutePingctl(t, "request", "arg1", "arg2") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Request Command fails when provided invalid flag +func TestRequestCmd_Execute_InvalidFlag(t *testing.T) { + expectedErrorPattern := `unknown flag: --invalid` + err := testutils_cobra.ExecutePingctl(t, "request", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Request Command --help, -h flag +func TestRequestCmd_Execute_Help(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "request", "--help") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingctl(t, "request", "-h") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Request Command with Invalid Service +func TestRequestCmd_Execute_InvalidService(t *testing.T) { + expectedErrorPattern := `^invalid argument ".*" for "-s, --service" flag: unrecognized Request Service: '.*'. Must be one of: .*$` + err := testutils_cobra.ExecutePingctl(t, "request", + "--service", "invalid-service", + "--http-method", "GET", + "environments", + ) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Request Command with Invalid HTTP Method +func TestRequestCmd_Execute_InvalidHTTPMethod(t *testing.T) { + expectedErrorPattern := `^invalid argument ".*" for "-m, --http-method" flag: unrecognized HTTP Method: '.*'. Must be one of: .*$` + err := testutils_cobra.ExecutePingctl(t, "request", + "--service", "pingone", + "--http-method", "INVALID", + "environments", + ) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Request Command with Missing Required Service Flag +func TestRequestCmd_Execute_MissingRequiredServiceFlag(t *testing.T) { + expectedErrorPattern := `failed to send custom request: service is required` + err := testutils_cobra.ExecutePingctl(t, "request", "environments") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/cmd/root.go b/cmd/root.go index b695a1f..bda3e1f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( "github.com/pingidentity/pingctl/cmd/config" "github.com/pingidentity/pingctl/cmd/feedback" "github.com/pingidentity/pingctl/cmd/platform" + "github.com/pingidentity/pingctl/cmd/request" "github.com/pingidentity/pingctl/internal/configuration" "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/logger" @@ -17,6 +18,16 @@ import ( "github.com/spf13/viper" ) +const ( + ConfigurationFileFormat = `Configuration File Format: +activeProfile: + +: + color: + outputFormat: + ...` +) + func init() { l := logger.Get() @@ -30,6 +41,7 @@ func init() { // rootCmd represents the base command when called without any subcommands func NewRootCommand() *cobra.Command { cmd := &cobra.Command{ + Example: ConfigurationFileFormat, Long: `A CLI tool for managing Ping Identity products.`, Short: "A CLI tool for managing Ping Identity products.", SilenceErrors: true, // Upon error in RunE method, let output package in main.go handle error output @@ -42,6 +54,7 @@ func NewRootCommand() *cobra.Command { config.NewConfigCommand(), feedback.NewFeedbackCommand(), platform.NewPlatformCommand(), + request.NewRequestCommand(), ) cmd.PersistentFlags().AddFlag(options.RootConfigOption.Flag) diff --git a/internal/commands/config/get_internal_test.go b/internal/commands/config/get_internal_test.go index 82e2e09..21fbbd9 100644 --- a/internal/commands/config/get_internal_test.go +++ b/internal/commands/config/get_internal_test.go @@ -13,7 +13,7 @@ import ( func Test_RunInternalConfigGet(t *testing.T) { testutils_viper.InitVipers(t) - err := RunInternalConfigGet("pingctl") + err := RunInternalConfigGet("service") if err != nil { t.Errorf("RunInternalConfigGet returned error: %v", err) } @@ -39,7 +39,7 @@ func Test_RunInternalConfigGet_DifferentProfile(t *testing.T) { options.ConfigGetProfileOption.Flag.Changed = true options.ConfigGetProfileOption.CobraParamValue = &profileName - err := RunInternalConfigGet("pingctl") + err := RunInternalConfigGet("service") if err != nil { t.Errorf("RunInternalConfigGet returned error: %v", err) } @@ -57,6 +57,6 @@ func Test_RunInternalConfigGet_InvalidProfileName(t *testing.T) { options.ConfigGetProfileOption.CobraParamValue = &profileName expectedErrorPattern := `^failed to get configuration: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigGet("pingctl") + err := RunInternalConfigGet("service") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/config/set_internal.go b/internal/commands/config/set_internal.go index d7cd028..f481da0 100644 --- a/internal/commands/config/set_internal.go +++ b/internal/commands/config/set_internal.go @@ -106,53 +106,83 @@ func parseKeyValuePair(kvPair string) (string, string, error) { func setValue(profileViper *viper.Viper, vKey, vValue string, valueType options.OptionType) (err error) { switch valueType { case options.ENUM_BOOL: - var bool customtypes.Bool + bool := new(customtypes.Bool) if err = bool.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a boolean. Allowed [true, false]: %v", vKey, err) } profileViper.Set(vKey, bool) case options.ENUM_EXPORT_FORMAT: - var exportFormat customtypes.ExportFormat + exportFormat := new(customtypes.ExportFormat) if err = exportFormat.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid export format. Allowed [%s]: %v", vKey, strings.Join(customtypes.ExportFormatValidValues(), ", "), err) } profileViper.Set(vKey, exportFormat) - case options.ENUM_MULTI_SERVICE: - var multiService customtypes.MultiService - if err = multiService.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid multi-service. Allowed [%s]: %v", vKey, strings.Join(customtypes.MultiServiceValidValues(), ", "), err) + case options.ENUM_EXPORT_SERVICES: + exportServices := new(customtypes.ExportServices) + if err = exportServices.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be valid export service(s). Allowed [%s]: %v", vKey, strings.Join(customtypes.ExportServicesValidValues(), ", "), err) } - profileViper.Set(vKey, multiService) + profileViper.Set(vKey, exportServices) case options.ENUM_OUTPUT_FORMAT: - var outputFormat customtypes.OutputFormat + outputFormat := new(customtypes.OutputFormat) if err = outputFormat.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid output format. Allowed [%s]: %v", vKey, strings.Join(customtypes.OutputFormatValidValues(), ", "), err) } profileViper.Set(vKey, outputFormat) - case options.ENUM_PINGONE_REGION: - var region customtypes.PingOneRegion + case options.ENUM_PINGONE_REGION_CODE: + region := new(customtypes.PingoneRegionCode) if err = region.Set(vValue); err != nil { - return fmt.Errorf("value for key '%s' must be a valid PingOne region. Allowed [%s]: %v", vKey, strings.Join(customtypes.PingOneRegionValidValues(), ", "), err) + return fmt.Errorf("value for key '%s' must be a valid Pingone Region Code. Allowed [%s]: %v", vKey, strings.Join(customtypes.PingoneRegionCodeValidValues(), ", "), err) } profileViper.Set(vKey, region) case options.ENUM_STRING: - var str customtypes.String + str := new(customtypes.String) if err = str.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a string: %v", vKey, err) } profileViper.Set(vKey, str) case options.ENUM_STRING_SLICE: - var strSlice customtypes.StringSlice + strSlice := new(customtypes.StringSlice) if err = strSlice.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a string slice: %v", vKey, err) } profileViper.Set(vKey, strSlice) case options.ENUM_UUID: - var uuid customtypes.UUID + uuid := new(customtypes.UUID) if err = uuid.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid UUID: %v", vKey, err) } profileViper.Set(vKey, uuid) + case options.ENUM_PINGONE_AUTH_TYPE: + authType := new(customtypes.PingoneAuthenticationType) + if err = authType.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid Pingone Authentication Type. Allowed [%s]: %v", vKey, strings.Join(customtypes.PingoneAuthenticationTypeValidValues(), ", "), err) + } + profileViper.Set(vKey, authType) + case options.ENUM_PINGFEDERATE_AUTH_TYPE: + authType := new(customtypes.PingfederateAuthenticationType) + if err = authType.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid Pingfederate Authentication Type. Allowed [%s]: %v", vKey, strings.Join(customtypes.PingfederateAuthenticationTypeValidValues(), ", "), err) + } + profileViper.Set(vKey, authType) + case options.ENUM_INT: + intValue := new(customtypes.Int) + if err = intValue.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be an integer: %v", vKey, err) + } + profileViper.Set(vKey, intValue) + case options.ENUM_REQUEST_HTTP_METHOD: + httpMethod := new(customtypes.HTTPMethod) + if err = httpMethod.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid HTTP method. Allowed [%s]: %v", vKey, strings.Join(customtypes.HTTPMethodValidValues(), ", "), err) + } + profileViper.Set(vKey, httpMethod) + case options.ENUM_REQUEST_SERVICE: + service := new(customtypes.RequestService) + if err = service.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid request service. Allowed [%s]: %v", vKey, strings.Join(customtypes.RequestServiceValidValues(), ", "), err) + } + profileViper.Set(vKey, service) default: return fmt.Errorf("failed to set configuration: variable type for key '%s' is not recognized", vKey) } diff --git a/internal/commands/config/set_internal_test.go b/internal/commands/config/set_internal_test.go index ac07ddf..d1b13e7 100644 --- a/internal/commands/config/set_internal_test.go +++ b/internal/commands/config/set_internal_test.go @@ -13,7 +13,7 @@ import ( func Test_RunInternalConfigSet(t *testing.T) { testutils_viper.InitVipers(t) - err := RunInternalConfigSet("pingctl.color=true") + err := RunInternalConfigSet("color=true") if err != nil { t.Errorf("RunInternalConfigSet returned error: %v", err) } @@ -33,7 +33,7 @@ func Test_RunInternalConfigSet_InvalidValue(t *testing.T) { testutils_viper.InitVipers(t) expectedErrorPattern := `^failed to set configuration: value for key '.*' must be a boolean. Allowed .*: strconv.ParseBool: parsing ".*": invalid syntax$` - err := RunInternalConfigSet("pingctl.color=invalid") + err := RunInternalConfigSet("color=invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -49,7 +49,7 @@ func Test_RunInternalConfigSet_NonExistentProfileName(t *testing.T) { options.ConfigSetProfileOption.CobraParamValue = &profileName expectedErrorPattern := `^failed to set configuration: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigSet("pingctl.color=true") + err := RunInternalConfigSet("color=true") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -64,7 +64,7 @@ func Test_RunInternalConfigSet_DifferentProfile(t *testing.T) { options.ConfigSetProfileOption.Flag.Changed = true options.ConfigSetProfileOption.CobraParamValue = &profileName - err := RunInternalConfigSet("pingctl.color=true") + err := RunInternalConfigSet("color=true") if err != nil { t.Errorf("RunInternalConfigSet returned error: %v", err) } @@ -82,7 +82,7 @@ func Test_RunInternalConfigSet_InvalidProfileName(t *testing.T) { options.ConfigSetProfileOption.CobraParamValue = &profileName expectedErrorPattern := `^failed to set configuration: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` - err := RunInternalConfigSet("pingctl.color=true") + err := RunInternalConfigSet("color=true") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -91,7 +91,7 @@ func Test_RunInternalConfigSet_NoValue(t *testing.T) { testutils_viper.InitVipers(t) expectedErrorPattern := `^failed to set configuration: value for key '.*' is empty. Use 'pingctl config unset .*' to unset the key$` - err := RunInternalConfigSet("pingctl.color=") + err := RunInternalConfigSet("color=") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/config/unset_internal_test.go b/internal/commands/config/unset_internal_test.go index 0d05fb0..8446e76 100644 --- a/internal/commands/config/unset_internal_test.go +++ b/internal/commands/config/unset_internal_test.go @@ -13,7 +13,7 @@ import ( func Test_RunInternalConfigUnset(t *testing.T) { testutils_viper.InitVipers(t) - err := RunInternalConfigUnset("pingctl.color") + err := RunInternalConfigUnset("color") if err != nil { t.Errorf("RunInternalConfigUnset returned error: %v", err) } @@ -39,7 +39,7 @@ func Test_RunInternalConfigUnset_DifferentProfile(t *testing.T) { options.ConfigUnsetProfileOption.Flag.Changed = true options.ConfigUnsetProfileOption.CobraParamValue = &profileName - err := RunInternalConfigUnset("pingctl.color") + err := RunInternalConfigUnset("color") if err != nil { t.Errorf("RunInternalConfigUnset returned error: %v", err) } @@ -57,6 +57,6 @@ func Test_RunInternalConfigUnset_InvalidProfileName(t *testing.T) { options.ConfigUnsetProfileOption.CobraParamValue = &profileName expectedErrorPattern := `^failed to unset configuration: invalid profile name: '.*' profile does not exist$` - err := RunInternalConfigUnset("pingctl.color") + err := RunInternalConfigUnset("color") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go index a758794..508877c 100644 --- a/internal/commands/platform/export_internal.go +++ b/internal/commands/platform/export_internal.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + "github.com/patrickcping/pingone-go-sdk-v2/management" pingoneGoClient "github.com/patrickcping/pingone-go-sdk-v2/pingone" "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/connector" @@ -47,7 +48,7 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) { if err != nil { return err } - multiService, err := profiles.GetOptionValue(options.PlatformExportServiceOption) + exportServices, err := profiles.GetOptionValue(options.PlatformExportServiceOption) if err != nil { return err } @@ -60,18 +61,18 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) { return err } - ms := customtypes.NewMultiService() - if err = ms.Set(multiService); err != nil { + es := new(customtypes.ExportServices) + if err = es.Set(exportServices); err != nil { return err } - if ms.ContainsPingOneService() { + if es.ContainsPingOneService() { if err = initPingOneServices(ctx, commandVersion); err != nil { return err } } - if ms.ContainsPingFederateService() { + if es.ContainsPingFederateService() { if err = initPingFederateServices(ctx, commandVersion); err != nil { return err } @@ -85,7 +86,7 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) { return err } - exportableConnectors := getExportableConnectors(ms) + exportableConnectors := getExportableConnectors(es) if err := exportConnectors(exportableConnectors, exportFormat, outputDir, overwriteExportBool); err != nil { return err @@ -104,40 +105,11 @@ func initPingFederateServices(ctx context.Context, pingctlVersion string) (err e return fmt.Errorf("failed to initialize PingFederate services. context is nil") } - // Get all the PingFederate configuration values - pfClientID, err := profiles.GetOptionValue(options.PlatformExportPingfederateClientIDOption) + pfInsecureTrustAllTLS, err := profiles.GetOptionValue(options.PingfederateInsecureTrustAllTLSOption) if err != nil { return err } - pfClientSecret, err := profiles.GetOptionValue(options.PlatformExportPingfederateClientSecretOption) - if err != nil { - return err - } - pfTokenUrl, err := profiles.GetOptionValue(options.PlatformExportPingfederateTokenURLOption) - if err != nil { - return err - } - pfScopes, err := profiles.GetOptionValue(options.PlatformExportPingfederateScopesOption) - if err != nil { - return err - } - pfAccessToken, err := profiles.GetOptionValue(options.PlatformExportPingfederateAccessTokenOption) - if err != nil { - return err - } - pfUsername, err := profiles.GetOptionValue(options.PlatformExportPingfederateUsernameOption) - if err != nil { - return err - } - pfPassword, err := profiles.GetOptionValue(options.PlatformExportPingfederatePasswordOption) - if err != nil { - return err - } - pfInsecureTrustAllTLS, err := profiles.GetOptionValue(options.PlatformExportPingfederateInsecureTrustAllTLSOption) - if err != nil { - return err - } - caCertPemFiles, err := profiles.GetOptionValue(options.PlatformExportPingfederateCACertificatePemFilesOption) + caCertPemFiles, err := profiles.GetOptionValue(options.PingfederateCACertificatePemFilesOption) if err != nil { return err } @@ -175,15 +147,64 @@ func initPingFederateServices(ctx context.Context, pingctlVersion string) (err e return err } + // Create context based on pingfederate authentication type + authType, err := profiles.GetOptionValue(options.PingfederateAuthenticationTypeOption) + if err != nil { + return err + } + switch { - case options.PlatformExportPingfederateUsernameOption.Flag.Changed && options.PlatformExportPingfederatePasswordOption.Flag.Changed: + case strings.EqualFold(authType, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC): + pfUsername, err := profiles.GetOptionValue(options.PingfederateBasicAuthUsernameOption) + if err != nil { + return err + } + pfPassword, err := profiles.GetOptionValue(options.PingfederateBasicAuthPasswordOption) + if err != nil { + return err + } + + if pfUsername == "" || pfPassword == "" { + return fmt.Errorf("failed to initialize PingFederate services. Basic authentication username or password is empty") + } + pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextBasicAuth, pingfederateGoClient.BasicAuth{ UserName: pfUsername, Password: pfPassword, }) - case options.PlatformExportPingfederateAccessTokenOption.Flag.Changed: + case strings.EqualFold(authType, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN): + pfAccessToken, err := profiles.GetOptionValue(options.PingfederateAccessTokenAuthAccessTokenOption) + if err != nil { + return err + } + + if pfAccessToken == "" { + return fmt.Errorf("failed to initialize PingFederate services. Access token is empty") + } + pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextAccessToken, pfAccessToken) - case pfClientID != "" && pfClientSecret != "" && pfTokenUrl != "": + case strings.EqualFold(authType, customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS): + pfClientID, err := profiles.GetOptionValue(options.PingfederateClientCredentialsAuthClientIDOption) + if err != nil { + return err + } + pfClientSecret, err := profiles.GetOptionValue(options.PingfederateClientCredentialsAuthClientSecretOption) + if err != nil { + return err + } + pfTokenUrl, err := profiles.GetOptionValue(options.PingfederateClientCredentialsAuthTokenURLOption) + if err != nil { + return err + } + pfScopes, err := profiles.GetOptionValue(options.PingfederateClientCredentialsAuthScopesOption) + if err != nil { + return err + } + + if pfClientID == "" || pfClientSecret == "" || pfTokenUrl == "" { + return fmt.Errorf("failed to initialize PingFederate services. Client ID, Client Secret, or Token URL is empty") + } + pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextOAuth2, pingfederateGoClient.OAuthValues{ Transport: tr, TokenUrl: pfTokenUrl, @@ -191,15 +212,8 @@ func initPingFederateServices(ctx context.Context, pingctlVersion string) (err e ClientSecret: pfClientSecret, Scopes: strings.Split(pfScopes, ","), }) - case pfAccessToken != "": - pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextAccessToken, pfAccessToken) - case pfUsername != "" && pfPassword != "": - pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextBasicAuth, pingfederateGoClient.BasicAuth{ - UserName: pfUsername, - Password: pfPassword, - }) default: - return fmt.Errorf(`failed to initialize PingFederate API client. none of the following sets of authentication configuration values are set: OAuth2 client credentials (client ID, client secret, token URL), Access token, or Basic Authentication credentials (username, password). configure these properties via parameter flags, environment variables, or the tool's configuration file (default: $HOME/.pingctl/config.yaml)`) + return fmt.Errorf("failed to initialize PingFederate services. unrecognized authentication type '%s'", authType) } return nil @@ -231,15 +245,15 @@ func initPingFederateApiClient(tr *http.Transport, pingctlVersion string) (err e return fmt.Errorf("failed to initialize pingfederate API client. http transport is nil") } - httpsHost, err := profiles.GetOptionValue(options.PlatformExportPingfederateHTTPSHostOption) + httpsHost, err := profiles.GetOptionValue(options.PingfederateHTTPSHostOption) if err != nil { return err } - adminApiPath, err := profiles.GetOptionValue(options.PlatformExportPingfederateAdminAPIPathOption) + adminApiPath, err := profiles.GetOptionValue(options.PingfederateAdminAPIPathOption) if err != nil { return err } - xBypassExternalValidationHeader, err := profiles.GetOptionValue(options.PlatformExportPingfederateXBypassExternalValidationHeaderOption) + xBypassExternalValidationHeader, err := profiles.GetOptionValue(options.PingfederateXBypassExternalValidationHeaderOption) if err != nil { return err } @@ -256,7 +270,7 @@ func initPingFederateApiClient(tr *http.Transport, pingctlVersion string) (err e userAgent := fmt.Sprintf("pingctl/%s", pingctlVersion) if v := strings.TrimSpace(os.Getenv("PINGCTL_PINGFEDERATE_APPEND_USER_AGENT")); v != "" { - userAgent += fmt.Sprintf(" %s", v) + userAgent = fmt.Sprintf("%s %s", userAgent, v) } pfClientConfig := pingfederateGoClient.NewConfiguration() @@ -284,38 +298,40 @@ func initPingOneApiClient(ctx context.Context, pingctlVersion string) (err error return fmt.Errorf("failed to initialize pingone API client. context is nil") } - pingoneApiClientId, err = profiles.GetOptionValue(options.PlatformExportPingoneWorkerClientIDOption) + pingoneApiClientId, err = profiles.GetOptionValue(options.PingoneAuthenticationWorkerClientIDOption) if err != nil { return err } - clientSecret, err := profiles.GetOptionValue(options.PlatformExportPingoneWorkerClientSecretOption) + clientSecret, err := profiles.GetOptionValue(options.PingoneAuthenticationWorkerClientSecretOption) if err != nil { return err } - environmentID, err := profiles.GetOptionValue(options.PlatformExportPingoneWorkerEnvironmentIDOption) + environmentID, err := profiles.GetOptionValue(options.PingoneAuthenticationWorkerEnvironmentIDOption) if err != nil { return err } - region, err := profiles.GetOptionValue(options.PlatformExportPingoneRegionOption) + regionCode, err := profiles.GetOptionValue(options.PingoneRegionCodeOption) if err != nil { return err } - if pingoneApiClientId == "" || clientSecret == "" || environmentID == "" || region == "" { - return fmt.Errorf(`failed to initialize pingone API client. one of worker client ID, worker client secret, pingone region, and/or worker environment ID is empty. configure these properties via parameter flags, environment variables, or the tool's configuration file (default: $HOME/.pingctl/config.yaml)`) + if pingoneApiClientId == "" || clientSecret == "" || environmentID == "" || regionCode == "" { + return fmt.Errorf(`failed to initialize pingone API client. one of worker client ID, worker client secret, pingone region code, and/or worker environment ID is empty. configure these properties via parameter flags, environment variables, or the tool's configuration file (default: $HOME/.pingctl/config.yaml)`) } userAgent := fmt.Sprintf("pingctl/%s", pingctlVersion) if v := strings.TrimSpace(os.Getenv("PINGCTL_PINGONE_APPEND_USER_AGENT")); v != "" { - userAgent += fmt.Sprintf(" %s", v) + userAgent = fmt.Sprintf("%s %s", userAgent, v) } + enumRegionCode := management.EnumRegionCode(regionCode) + apiConfig := &pingoneGoClient.Config{ ClientID: &pingoneApiClientId, ClientSecret: &clientSecret, EnvironmentID: &environmentID, - Region: region, + RegionCode: &enumRegionCode, UserAgentSuffix: &userAgent, } @@ -336,7 +352,7 @@ worker client ID - %s worker environment ID - %s pingone region - %s %s` - return fmt.Errorf(initFailErrFormatMessage, err.Error(), pingoneApiClientId, environmentID, region, clientSecretErrorMessage) + return fmt.Errorf(initFailErrFormatMessage, err.Error(), pingoneApiClientId, environmentID, regionCode, clientSecretErrorMessage) } return nil @@ -384,13 +400,13 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (err erro } func getPingOneExportEnvID() (err error) { - pingoneExportEnvID, err = profiles.GetOptionValue(options.PlatformExportPingoneExportEnvironmentIDOption) + pingoneExportEnvID, err = profiles.GetOptionValue(options.PlatformExportPingoneEnvironmentIDOption) if err != nil { return err } if pingoneExportEnvID == "" { - pingoneExportEnvID, err = profiles.GetOptionValue(options.PlatformExportPingoneWorkerEnvironmentIDOption) + pingoneExportEnvID, err = profiles.GetOptionValue(options.PingoneAuthenticationWorkerEnvironmentIDOption) if err != nil { return err } @@ -432,25 +448,25 @@ func validatePingOneExportEnvID(ctx context.Context) (err error) { return nil } -func getExportableConnectors(multiService *customtypes.MultiService) (exportableConnectors *[]connector.Exportable) { +func getExportableConnectors(exportServices *customtypes.ExportServices) (exportableConnectors *[]connector.Exportable) { // Using the --service parameter(s) provided by user, build list of connectors to export connectors := []connector.Exportable{} - if multiService == nil { + if exportServices == nil { return &connectors } - for _, service := range multiService.GetServices() { + for _, service := range exportServices.GetServices() { switch service { - case customtypes.ENUM_SERVICE_PINGONE_PLATFORM: + case customtypes.ENUM_EXPORT_SERVICE_PINGONE_PLATFORM: connectors = append(connectors, platform.PlatformConnector(pingoneContext, pingoneApiClient, &pingoneApiClientId, pingoneExportEnvID)) - case customtypes.ENUM_SERVICE_PINGONE_SSO: + case customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO: connectors = append(connectors, sso.SSOConnector(pingoneContext, pingoneApiClient, &pingoneApiClientId, pingoneExportEnvID)) - case customtypes.ENUM_SERVICE_PINGONE_MFA: + case customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA: connectors = append(connectors, mfa.MFAConnector(pingoneContext, pingoneApiClient, &pingoneApiClientId, pingoneExportEnvID)) - case customtypes.ENUM_SERVICE_PINGONE_PROTECT: + case customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT: connectors = append(connectors, protect.ProtectConnector(pingoneContext, pingoneApiClient, &pingoneApiClientId, pingoneExportEnvID)) - case customtypes.ENUM_SERVICE_PINGFEDERATE: + case customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE: connectors = append(connectors, pingfederate.PFConnector(pingfederateContext, pingfederateApiClient)) // default: // This unrecognized service condition is handled by cobra with the custom type MultiService diff --git a/internal/commands/platform/export_internal_test.go b/internal/commands/platform/export_internal_test.go index 521bbfb..06122bd 100644 --- a/internal/commands/platform/export_internal_test.go +++ b/internal/commands/platform/export_internal_test.go @@ -240,11 +240,15 @@ func TestValidatePingOneExportEnvIDNilContext(t *testing.T) { func TestGetExportableConnectors(t *testing.T) { testutils_viper.InitVipers(t) - ms := customtypes.NewMultiService() + es := new(customtypes.ExportServices) + err := es.Set(customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) + if err != nil { + t.Fatalf("ms.Set() error = %v", err) + } - expectedConnectors := len(ms.GetServices()) + expectedConnectors := len(es.GetServices()) - exportableConnectors := getExportableConnectors(ms) + exportableConnectors := getExportableConnectors(es) if len(*exportableConnectors) == 0 { t.Errorf("getExportableConnectors() exportableConnectors = %v, want non-empty", exportableConnectors) } @@ -273,21 +277,21 @@ func TestExportConnectors(t *testing.T) { t.Fatalf("initPingOneServices() error = %v", err) } - ms := customtypes.NewMultiService() - err = ms.Set(customtypes.ENUM_SERVICE_PINGONE_PROTECT) + es := new(customtypes.ExportServices) + err = es.Set(customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) if err != nil { t.Fatalf("ms.Set() error = %v", err) } - exportableConnectors := getExportableConnectors(ms) + exportableConnectors := getExportableConnectors(es) - err = exportConnectors(exportableConnectors, connector.ENUMEXPORTFORMAT_HCL, t.TempDir(), true) + err = exportConnectors(exportableConnectors, customtypes.ENUM_EXPORT_FORMAT_HCL, t.TempDir(), true) testutils.CheckExpectedError(t, err, nil) } // Test exportConnectors function with nil exportable connectors func TestExportConnectorsNilExportableConnectors(t *testing.T) { - err := exportConnectors(nil, connector.ENUMEXPORTFORMAT_HCL, t.TempDir(), true) + err := exportConnectors(nil, customtypes.ENUM_EXPORT_FORMAT_HCL, t.TempDir(), true) expectedErrorPattern := `^failed to export services\. exportable connectors list is nil$` testutils.CheckExpectedError(t, err, &expectedErrorPattern) @@ -297,7 +301,7 @@ func TestExportConnectorsNilExportableConnectors(t *testing.T) { func TestExportConnectorsEmptyExportableConnectors(t *testing.T) { exportableConnectors := &[]connector.Exportable{} - err := exportConnectors(exportableConnectors, connector.ENUMEXPORTFORMAT_HCL, t.TempDir(), true) + err := exportConnectors(exportableConnectors, customtypes.ENUM_EXPORT_FORMAT_HCL, t.TempDir(), true) testutils.CheckExpectedError(t, err, nil) } @@ -310,13 +314,13 @@ func TestExportConnectorsInvalidExportFormat(t *testing.T) { t.Fatalf("initPingOneServices() error = %v", err) } - ms := customtypes.NewMultiService() - err = ms.Set(customtypes.ENUM_SERVICE_PINGONE_PROTECT) + es := new(customtypes.ExportServices) + err = es.Set(customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) if err != nil { t.Fatalf("ms.Set() error = %v", err) } - exportableConnectors := getExportableConnectors(ms) + exportableConnectors := getExportableConnectors(es) err = exportConnectors(exportableConnectors, "invalid", t.TempDir(), true) @@ -333,15 +337,15 @@ func TestExportConnectorsInvalidOutputDir(t *testing.T) { t.Fatalf("initPingOneServices() error = %v", err) } - ms := customtypes.NewMultiService() - err = ms.Set(customtypes.ENUM_SERVICE_PINGONE_PROTECT) + es := new(customtypes.ExportServices) + err = es.Set(customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT) if err != nil { t.Fatalf("ms.Set() error = %v", err) } - exportableConnectors := getExportableConnectors(ms) + exportableConnectors := getExportableConnectors(es) - err = exportConnectors(exportableConnectors, connector.ENUMEXPORTFORMAT_HCL, "/invalid", true) + err = exportConnectors(exportableConnectors, customtypes.ENUM_EXPORT_FORMAT_HCL, "/invalid", true) expectedErrorPattern := `^failed to export '.*' service: failed to create export file ".*". err: open .*: no such file or directory$` testutils.CheckExpectedError(t, err, &expectedErrorPattern) diff --git a/internal/commands/request/request_internal.go b/internal/commands/request/request_internal.go new file mode 100644 index 0000000..8dbe9be --- /dev/null +++ b/internal/commands/request/request_internal.go @@ -0,0 +1,294 @@ +package request_internal + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/output" + "github.com/pingidentity/pingctl/internal/profiles" +) + +type PingoneAuthResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` +} + +func RunInternalRequest(uri string) (err error) { + service, err := profiles.GetOptionValue(options.RequestServiceOption) + if err != nil { + return fmt.Errorf("failed to send custom request: %v", err) + } + + if service == "" { + return fmt.Errorf("failed to send custom request: service is required") + } + + switch service { + case customtypes.ENUM_REQUEST_SERVICE_PINGONE: + err = runInternalPingOneRequest(uri) + if err != nil { + return fmt.Errorf("failed to send custom request: %v", err) + } + default: + return fmt.Errorf("failed to send custom request: unrecognized service '%s'", service) + } + + return nil +} + +func runInternalPingOneRequest(uri string) (err error) { + accessToken, err := pingoneAccessToken() + if err != nil { + return err + } + + topLevelDomain, err := getTopLevelDomain() + if err != nil { + return err + } + + apiURL := fmt.Sprintf("https://api.pingone.%s/v1/%s", topLevelDomain, uri) + + httpMethod, err := profiles.GetOptionValue(options.RequestHTTPMethodOption) + if err != nil { + return err + } + + if httpMethod == "" { + return fmt.Errorf("http method is required") + } + + data, err := getData() + if err != nil { + return err + } + + payload := strings.NewReader(data) + + client := &http.Client{} + req, err := http.NewRequest(httpMethod, apiURL, payload) + if err != nil { + return err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Add("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + if res.StatusCode < 200 || res.StatusCode >= 300 { + output.Print(output.Opts{ + Message: "Custom request failed.", + Result: output.ENUM_RESULT_FAILURE, + Fields: map[string]any{ + "response": string(body), + "status": res.StatusCode, + }, + }) + } else { + output.Print(output.Opts{ + Message: "Custom request successful.", + Result: output.ENUM_RESULT_SUCCESS, + Fields: map[string]any{ + "response": string(body), + "status": res.StatusCode, + }, + }) + } + + return nil +} + +func getTopLevelDomain() (topLevelDomain string, err error) { + pingoneRegionCode, err := profiles.GetOptionValue(options.PingoneRegionCodeOption) + if err != nil { + return "", err + } + + if pingoneRegionCode == "" { + return "", fmt.Errorf("PingOne region code is required") + } + + switch pingoneRegionCode { + case customtypes.ENUM_PINGONE_REGION_CODE_AP: + topLevelDomain = customtypes.ENUM_PINGONE_TLD_AP + case customtypes.ENUM_PINGONE_REGION_CODE_AU: + topLevelDomain = customtypes.ENUM_PINGONE_TLD_AU + case customtypes.ENUM_PINGONE_REGION_CODE_CA: + topLevelDomain = customtypes.ENUM_PINGONE_TLD_CA + case customtypes.ENUM_PINGONE_REGION_CODE_EU: + topLevelDomain = customtypes.ENUM_PINGONE_TLD_EU + case customtypes.ENUM_PINGONE_REGION_CODE_NA: + topLevelDomain = customtypes.ENUM_PINGONE_TLD_NA + default: + return "", fmt.Errorf("unrecognized Pingone region code: '%s'", pingoneRegionCode) + } + + return topLevelDomain, nil +} + +func pingoneAccessToken() (accessToken string, err error) { + // Check if existing access token is available + accessToken, err = profiles.GetOptionValue(options.RequestAccessTokenOption) + if err != nil { + return "", err + } + + if accessToken != "" { + accessTokenExpiry, err := profiles.GetOptionValue(options.RequestAccessTokenExpiryOption) + if err != nil { + return "", err + } + + if accessTokenExpiry == "" { + accessTokenExpiry = "0" + } + + // convert expiry string to int + tokenExpiryInt, err := strconv.ParseInt(accessTokenExpiry, 10, 64) + if err != nil { + return "", err + } + + // Get current Unix epoch time in seconds + currentEpochSeconds := time.Now().Unix() + + // Return access token if it is still valid + if currentEpochSeconds < tokenExpiryInt { + return accessToken, nil + } + } + + output.Print(output.Opts{ + Message: "PingOne Access token does not exist or is expired, requesting a new token...", + Result: output.ENUM_RESULT_NOACTION_WARN, + }) + + // If no valid access token is available, login and get a new one + return pingoneAuth() +} + +func pingoneAuth() (accessToken string, err error) { + topLevelDomain, err := getTopLevelDomain() + if err != nil { + return "", err + } + + workerEnvId, err := profiles.GetOptionValue(options.PingoneAuthenticationWorkerEnvironmentIDOption) + if err != nil { + return "", err + } + + if workerEnvId == "" { + return "", fmt.Errorf("PingOne worker environment ID is required") + } + + authURL := fmt.Sprintf("https://auth.pingone.%s/%s/as/token", topLevelDomain, workerEnvId) + + clientId, err := profiles.GetOptionValue(options.PingoneAuthenticationWorkerClientIDOption) + if err != nil { + return "", err + } + clientSecret, err := profiles.GetOptionValue(options.PingoneAuthenticationWorkerClientSecretOption) + if err != nil { + return "", err + } + + if clientId == "" || clientSecret == "" { + return "", fmt.Errorf("PingOne client ID and secret are required") + } + + basicAuthBase64 := base64.StdEncoding.EncodeToString([]byte(clientId + ":" + clientSecret)) + + payload := strings.NewReader("grant_type=client_credentials") + + client := &http.Client{} + req, err := http.NewRequest(customtypes.ENUM_HTTP_METHOD_POST, authURL, payload) + if err != nil { + return "", err + } + + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuthBase64)) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err := client.Do(req) + if err != nil { + return "", err + } + + defer res.Body.Close() + responseBodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return "", fmt.Errorf("failed to authenticate with PingOne: Response Status %s: Response Body %s", res.Status, string(responseBodyBytes)) + } + + pingoneAuthResponse := new(PingoneAuthResponse) + err = json.Unmarshal(responseBodyBytes, pingoneAuthResponse) + if err != nil { + return "", err + } + + // Store access token and expiry + profileViper := profiles.GetMainConfig().ActiveProfile().ViperInstance() + profileViper.Set(options.RequestAccessTokenOption.ViperKey, pingoneAuthResponse.AccessToken) + + currentTime := time.Now().Unix() + tokenExpiry := currentTime + pingoneAuthResponse.ExpiresIn + profileViper.Set(options.RequestAccessTokenExpiryOption.ViperKey, tokenExpiry) + + err = profiles.GetMainConfig().SaveProfile(profiles.GetMainConfig().ActiveProfile().Name(), profileViper) + if err != nil { + return "", err + } + + return pingoneAuthResponse.AccessToken, nil +} + +func getData() (data string, err error) { + data, err = profiles.GetOptionValue(options.RequestDataOption) + if err != nil { + return "", err + } + + if data == "" { + return "", nil + } + + // if data string first character is '@', read from file + if strings.HasPrefix(data, "@") { + filePath := strings.TrimPrefix(data, "@") + + contents, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + + data = string(contents) + } + + return data, nil +} diff --git a/internal/commands/request/request_internal_test.go b/internal/commands/request/request_internal_test.go new file mode 100644 index 0000000..720b944 --- /dev/null +++ b/internal/commands/request/request_internal_test.go @@ -0,0 +1,202 @@ +package request_internal + +import ( + "os" + "testing" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" +) + +// Test RunInternalRequest function +func Test_RunInternalRequest(t *testing.T) { + testutils_viper.InitVipers(t) + + t.Setenv(options.RequestServiceOption.EnvVar, "pingone") + + err := RunInternalRequest("environments") + testutils.CheckExpectedError(t, err, nil) +} + +// Test RunInternalRequest function with empty service +func Test_RunInternalRequest_EmptyService(t *testing.T) { + testutils_viper.InitVipers(t) + + os.Unsetenv(options.RequestServiceOption.EnvVar) + + err := RunInternalRequest("environments") + expectedErrorPattern := "failed to send custom request: service is required" + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalRequest function with unrecognized service +func Test_RunInternalRequest_UnrecognizedService(t *testing.T) { + testutils_viper.InitVipers(t) + + t.Setenv(options.RequestServiceOption.EnvVar, "invalid-service") + + err := RunInternalRequest("environments") + expectedErrorPattern := "failed to send custom request: unrecognized service 'invalid-service'" + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalRequest function with valid service but invalid URI +// This should not error, but rather print a failure message with Body and status of response +func Test_RunInternalRequest_ValidService_InvalidURI(t *testing.T) { + testutils_viper.InitVipers(t) + + t.Setenv(options.RequestServiceOption.EnvVar, "pingone") + + err := RunInternalRequest("invalid-uri") + testutils.CheckExpectedError(t, err, nil) +} + +// Test runInternalPingOneRequest function +func Test_runInternalPingOneRequest(t *testing.T) { + testutils_viper.InitVipers(t) + + err := runInternalPingOneRequest("environments") + testutils.CheckExpectedError(t, err, nil) +} + +// Test runInternalPingOneRequest function with invalid URI +// This should not error, but rather print a failure message with Body and status of response +func Test_runInternalPingOneRequest_InvalidURI(t *testing.T) { + testutils_viper.InitVipers(t) + + err := runInternalPingOneRequest("invalid-uri") + testutils.CheckExpectedError(t, err, nil) +} + +// Test getTopLevelDomain function +func Test_getTopLevelDomain(t *testing.T) { + testutils_viper.InitVipers(t) + + t.Setenv(options.PingoneRegionCodeOption.EnvVar, customtypes.ENUM_PINGONE_REGION_CODE_CA) + + domain, err := getTopLevelDomain() + testutils.CheckExpectedError(t, err, nil) + + expectedDomain := customtypes.ENUM_PINGONE_TLD_CA + if domain != expectedDomain { + t.Errorf("expected %s, got %s", expectedDomain, domain) + } +} + +// Test getTopLevelDomain function with invalid region code +func Test_getTopLevelDomain_InvalidRegionCode(t *testing.T) { + testutils_viper.InitVipers(t) + + t.Setenv(options.PingoneRegionCodeOption.EnvVar, "invalid-region") + + _, err := getTopLevelDomain() + expectedErrorPattern := "unrecognized Pingone region code: 'invalid-region'" + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test pingoneAccessToken function +func Test_pingoneAccessToken(t *testing.T) { + testutils_viper.InitVipers(t) + + firstToken, err := pingoneAccessToken() + testutils.CheckExpectedError(t, err, nil) + + // Run the function again to test caching + secondToken, err := pingoneAccessToken() + testutils.CheckExpectedError(t, err, nil) + + if firstToken != secondToken { + t.Errorf("expected access token to be cached, got different tokens: %s and %s", firstToken, secondToken) + } +} + +// Test pingoneAuth function +func Test_pingoneAuth(t *testing.T) { + testutils_viper.InitVipers(t) + + firstToken, err := pingoneAuth() + testutils.CheckExpectedError(t, err, nil) + + // Check token was cached + secondToken, err := pingoneAccessToken() + testutils.CheckExpectedError(t, err, nil) + + if firstToken != secondToken { + t.Errorf("expected access token to be cached, got different tokens: %s and %s", firstToken, secondToken) + } +} + +// Test pingoneAuth function with invalid credentials +func Test_pingoneAuth_InvalidCredentials(t *testing.T) { + testutils_viper.InitVipers(t) + + t.Setenv(options.PingoneAuthenticationWorkerClientIDOption.EnvVar, "invalid") + + _, err := pingoneAuth() + expectedErrorPattern := `(?s)^failed to authenticate with PingOne: Response Status 401 Unauthorized: Response Body .*$` + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test getData function +func Test_getData(t *testing.T) { + testutils_viper.InitVipers(t) + + expectedData := "{data: 'json'}" + t.Setenv(options.RequestDataOption.EnvVar, expectedData) + + data, err := getData() + testutils.CheckExpectedError(t, err, nil) + + if data != expectedData { + t.Errorf("expected %s, got %s", expectedData, data) + } +} + +// Test getData function with empty data +func Test_getData_EmptyData(t *testing.T) { + testutils_viper.InitVipers(t) + + t.Setenv(options.RequestDataOption.EnvVar, "") + + data, err := getData() + testutils.CheckExpectedError(t, err, nil) + + if data != "" { + t.Errorf("expected empty data, got %s", data) + } +} + +// Test getData function with file input +func Test_getData_FileInput(t *testing.T) { + testutils_viper.InitVipers(t) + + expectedData := "{data: 'json from file'}" + testDir := t.TempDir() + testFile := testDir + "/test.json" + err := os.WriteFile(testFile, []byte(expectedData), 0600) + if err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + t.Setenv(options.RequestDataOption.EnvVar, "@"+testFile) + + data, err := getData() + testutils.CheckExpectedError(t, err, nil) + + if data != expectedData { + t.Errorf("expected %s, got %s", expectedData, data) + } +} + +// Test getData function with non-existent file input +func Test_getData_NonExistentFileInput(t *testing.T) { + testutils_viper.InitVipers(t) + + t.Setenv(options.RequestDataOption.EnvVar, "@non_existent_file.json") + + _, err := getData() + expectedErrorPattern := `^open .*: no such file or directory$` + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/internal/configuration/config/config.go b/internal/configuration/config/config.go index ec2a8ce..5a94ce4 100644 --- a/internal/configuration/config/config.go +++ b/internal/configuration/config/config.go @@ -10,18 +10,6 @@ func InitConfigOptions() { initConfigProfileOption() initConfigNameOption() initConfigDescriptionOption() - - // initDeleteProfileOption() - - // initViewProfileOption() - - // initSetActiveProfileOption() - - // initGetProfileOption() - - // initSetProfileOption() - - // initUnsetProfileOption() } func initConfigProfileOption() { diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index edf5ddc..33584d3 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -9,7 +9,9 @@ import ( "github.com/pingidentity/pingctl/internal/configuration/options" configuration_platform "github.com/pingidentity/pingctl/internal/configuration/platform" configuration_profiles "github.com/pingidentity/pingctl/internal/configuration/profiles" + configuration_request "github.com/pingidentity/pingctl/internal/configuration/request" configuration_root "github.com/pingidentity/pingctl/internal/configuration/root" + configuration_services "github.com/pingidentity/pingctl/internal/configuration/services" ) func ViperKeys() (keys []string) { @@ -93,4 +95,9 @@ func InitAllOptions() { configuration_profiles.InitProfilesOptions() configuration_root.InitRootOptions() + + configuration_request.InitRequestOptions() + + configuration_services.InitPingfederateServiceOptions() + configuration_services.InitPingoneServiceOptions() } diff --git a/internal/configuration/configuration_test.go b/internal/configuration/configuration_test.go index 537d4f7..0f22a1c 100644 --- a/internal/configuration/configuration_test.go +++ b/internal/configuration/configuration_test.go @@ -12,7 +12,7 @@ import ( func Test_ValidateViperKey(t *testing.T) { testutils_viper.InitVipers(t) - err := configuration.ValidateViperKey("pingctl.color") + err := configuration.ValidateViperKey("color") if err != nil { t.Errorf("ValidateViperKey returned error: %v", err) } @@ -40,7 +40,7 @@ func Test_ValidateViperKey_EmptyKey(t *testing.T) { func Test_ValidateParentViperKey(t *testing.T) { testutils_viper.InitVipers(t) - err := configuration.ValidateParentViperKey("pingctl") + err := configuration.ValidateParentViperKey("service") if err != nil { t.Errorf("ValidateParentViperKey returned error: %v", err) } @@ -68,12 +68,12 @@ func Test_ValidateParentViperKey_EmptyKey(t *testing.T) { func Test_OptionFromViperKey(t *testing.T) { testutils_viper.InitVipers(t) - opt, err := configuration.OptionFromViperKey("pingctl.color") + opt, err := configuration.OptionFromViperKey("color") if err != nil { t.Errorf("OptionFromViperKey returned error: %v", err) } - if opt.ViperKey != "pingctl.color" { + if opt.ViperKey != "color" { t.Errorf("OptionFromViperKey returned invalid option: %v", opt) } } diff --git a/internal/configuration/options/options.go b/internal/configuration/options/options.go index 9e98652..fe55e3d 100644 --- a/internal/configuration/options/options.go +++ b/internal/configuration/options/options.go @@ -6,14 +6,19 @@ type OptionType string // OptionType enums const ( - ENUM_BOOL OptionType = "ENUM_BOOL" - ENUM_EXPORT_FORMAT OptionType = "ENUM_EXPORT_FORMAT" - ENUM_UUID OptionType = "ENUM_UUID" - ENUM_MULTI_SERVICE OptionType = "ENUM_MULTI_SERVICE" - ENUM_OUTPUT_FORMAT OptionType = "ENUM_OUTPUT_FORMAT" - ENUM_PINGONE_REGION OptionType = "ENUM_PINGONE_REGION" - ENUM_STRING OptionType = "ENUM_STRING" - ENUM_STRING_SLICE OptionType = "ENUM_STRING_SLICE" + ENUM_BOOL OptionType = "ENUM_BOOL" + ENUM_EXPORT_FORMAT OptionType = "ENUM_EXPORT_FORMAT" + ENUM_INT OptionType = "ENUM_INT" + ENUM_EXPORT_SERVICES OptionType = "ENUM_EXPORT_SERVICES" + ENUM_OUTPUT_FORMAT OptionType = "ENUM_OUTPUT_FORMAT" + ENUM_PINGFEDERATE_AUTH_TYPE OptionType = "ENUM_PINGFEDERATE_AUTH_TYPE" + ENUM_PINGONE_AUTH_TYPE OptionType = "ENUM_PINGONE_AUTH_TYPE" + ENUM_PINGONE_REGION_CODE OptionType = "ENUM_PINGONE_REGION_CODE" + ENUM_REQUEST_HTTP_METHOD OptionType = "ENUM_REQUEST_HTTP_METHOD" + ENUM_REQUEST_SERVICE OptionType = "ENUM_REQUEST_SERVICE" + ENUM_STRING OptionType = "ENUM_STRING" + ENUM_STRING_SLICE OptionType = "ENUM_STRING_SLICE" + ENUM_UUID OptionType = "ENUM_UUID" ) type Option struct { @@ -28,27 +33,31 @@ type Option struct { func Options() []Option { return []Option{ + PingoneAuthenticationTypeOption, + PingoneAuthenticationWorkerClientIDOption, + PingoneAuthenticationWorkerClientSecretOption, + PingoneAuthenticationWorkerEnvironmentIDOption, + PingoneRegionCodeOption, + PlatformExportExportFormatOption, PlatformExportServiceOption, PlatformExportOutputDirectoryOption, PlatformExportOverwriteOption, - PlatformExportPingoneWorkerEnvironmentIDOption, - PlatformExportPingoneExportEnvironmentIDOption, - PlatformExportPingoneWorkerClientIDOption, - PlatformExportPingoneWorkerClientSecretOption, - PlatformExportPingoneRegionOption, - PlatformExportPingfederateHTTPSHostOption, - PlatformExportPingfederateAdminAPIPathOption, - PlatformExportPingfederateXBypassExternalValidationHeaderOption, - PlatformExportPingfederateCACertificatePemFilesOption, - PlatformExportPingfederateInsecureTrustAllTLSOption, - PlatformExportPingfederateUsernameOption, - PlatformExportPingfederatePasswordOption, - PlatformExportPingfederateAccessTokenOption, - PlatformExportPingfederateClientIDOption, - PlatformExportPingfederateClientSecretOption, - PlatformExportPingfederateTokenURLOption, - PlatformExportPingfederateScopesOption, + PlatformExportPingoneEnvironmentIDOption, + + PingfederateHTTPSHostOption, + PingfederateAdminAPIPathOption, + PingfederateXBypassExternalValidationHeaderOption, + PingfederateCACertificatePemFilesOption, + PingfederateInsecureTrustAllTLSOption, + PingfederateBasicAuthUsernameOption, + PingfederateBasicAuthPasswordOption, + PingfederateAccessTokenAuthAccessTokenOption, + PingfederateClientCredentialsAuthClientIDOption, + PingfederateClientCredentialsAuthClientSecretOption, + PingfederateClientCredentialsAuthTokenURLOption, + PingfederateClientCredentialsAuthScopesOption, + PingfederateAuthenticationTypeOption, RootActiveProfileOption, RootColorOption, @@ -69,9 +78,41 @@ func Options() []Option { ConfigGetProfileOption, ConfigSetProfileOption, ConfigUnsetProfileOption, + + RequestDataOption, + RequestHTTPMethodOption, + RequestServiceOption, + RequestAccessTokenOption, + RequestAccessTokenExpiryOption, } } +// pingone service options +var ( + PingoneAuthenticationTypeOption Option + PingoneAuthenticationWorkerClientIDOption Option + PingoneAuthenticationWorkerClientSecretOption Option + PingoneAuthenticationWorkerEnvironmentIDOption Option + PingoneRegionCodeOption Option +) + +// pingfederate service options +var ( + PingfederateHTTPSHostOption Option + PingfederateAdminAPIPathOption Option + PingfederateXBypassExternalValidationHeaderOption Option + PingfederateCACertificatePemFilesOption Option + PingfederateInsecureTrustAllTLSOption Option + PingfederateBasicAuthUsernameOption Option + PingfederateBasicAuthPasswordOption Option + PingfederateAccessTokenAuthAccessTokenOption Option + PingfederateClientCredentialsAuthClientIDOption Option + PingfederateClientCredentialsAuthClientSecretOption Option + PingfederateClientCredentialsAuthTokenURLOption Option + PingfederateClientCredentialsAuthScopesOption Option + PingfederateAuthenticationTypeOption Option +) + // 'pingctl config' command options var ( ConfigProfileOption Option @@ -97,29 +138,11 @@ var ( // 'pingctl platform export' command options var ( - PlatformExportExportFormatOption Option - PlatformExportServiceOption Option - PlatformExportOutputDirectoryOption Option - PlatformExportOverwriteOption Option - - PlatformExportPingoneWorkerEnvironmentIDOption Option - PlatformExportPingoneExportEnvironmentIDOption Option - PlatformExportPingoneWorkerClientIDOption Option - PlatformExportPingoneWorkerClientSecretOption Option - PlatformExportPingoneRegionOption Option - - PlatformExportPingfederateHTTPSHostOption Option - PlatformExportPingfederateAdminAPIPathOption Option - PlatformExportPingfederateXBypassExternalValidationHeaderOption Option - PlatformExportPingfederateCACertificatePemFilesOption Option - PlatformExportPingfederateInsecureTrustAllTLSOption Option - PlatformExportPingfederateUsernameOption Option - PlatformExportPingfederatePasswordOption Option - PlatformExportPingfederateAccessTokenOption Option - PlatformExportPingfederateClientIDOption Option - PlatformExportPingfederateClientSecretOption Option - PlatformExportPingfederateTokenURLOption Option - PlatformExportPingfederateScopesOption Option + PlatformExportExportFormatOption Option + PlatformExportServiceOption Option + PlatformExportOutputDirectoryOption Option + PlatformExportOverwriteOption Option + PlatformExportPingoneEnvironmentIDOption Option ) // Generic viper profile options @@ -127,10 +150,19 @@ var ( ProfileDescriptionOption Option ) -// Options +// Root Command Options var ( RootActiveProfileOption Option RootColorOption Option RootConfigOption Option RootOutputFormatOption Option ) + +// 'pingctl request' command options +var ( + RequestDataOption Option + RequestHTTPMethodOption Option + RequestServiceOption Option + RequestAccessTokenOption Option + RequestAccessTokenExpiryOption Option +) diff --git a/internal/configuration/platform/export.go b/internal/configuration/platform/export.go index 41c3e73..fb5a873 100644 --- a/internal/configuration/platform/export.go +++ b/internal/configuration/platform/export.go @@ -6,42 +6,23 @@ import ( "strings" "github.com/pingidentity/pingctl/internal/configuration/options" - "github.com/pingidentity/pingctl/internal/connector" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/logger" "github.com/spf13/pflag" ) func InitPlatformExportOptions() { - initExportFormatOption() + initFormatOption() initServicesOption() initOutputDirectoryOption() initOverwriteOption() - - initPingOneWorkerEnvironmentIDOption() - initPingOneExportEnvironmentIDOption() - initPingOneWorkerClientIDOption() - initPingOneWorkerClientSecretOption() - initPingOneRegionOption() - - initPingFederateHTTPSHostOption() - initPingFederateAdminAPIPathOption() - initPingFederateXBypassExternalValidationHeaderOption() - initPingFederateCACertificatePemFilesOption() - initPingFederateInsecureTrustAllTLSOption() - initPingFederateUsernameOption() - initPingFederatePasswordOption() - initPingFederateAccessTokenOption() - initPingFederateClientIDOption() - initPingFederateClientSecretOption() - initPingFederateTokenURLOption() - initPingFederateScopesOption() + initPingOneEnvironmentIDOption() } -func initExportFormatOption() { - cobraParamName := "export-format" +func initFormatOption() { + cobraParamName := "format" cobraValue := new(customtypes.ExportFormat) - defaultValue := customtypes.ExportFormat(connector.ENUMEXPORTFORMAT_HCL) + defaultValue := customtypes.ExportFormat(customtypes.ENUM_EXPORT_FORMAT_HCL) envVar := "PINGCTL_EXPORT_FORMAT" options.PlatformExportExportFormatOption = options.Option{ @@ -51,35 +32,35 @@ func initExportFormatOption() { EnvVar: envVar, Flag: &pflag.Flag{ Name: cobraParamName, - Shorthand: "e", + Shorthand: "f", Usage: fmt.Sprintf("Specifies export format\nAllowed: [%s]. Also configurable via environment variable %s", strings.Join(customtypes.ExportFormatValidValues(), ", "), envVar), Value: cobraValue, - DefValue: connector.ENUMEXPORTFORMAT_HCL, + DefValue: customtypes.ENUM_EXPORT_FORMAT_HCL, }, Type: options.ENUM_STRING, - ViperKey: "export.exportFormat", + ViperKey: "export.format", } } func initServicesOption() { cobraParamName := "services" - cobraValue := new(customtypes.MultiService) - defaultValue := customtypes.NewMultiService() + cobraValue := new(customtypes.ExportServices) + defaultValue := customtypes.ExportServices(customtypes.ExportServicesValidValues()) envVar := "PINGCTL_EXPORT_SERVICES" options.PlatformExportServiceOption = options.Option{ CobraParamName: cobraParamName, CobraParamValue: cobraValue, - DefaultValue: defaultValue, + DefaultValue: &defaultValue, EnvVar: envVar, Flag: &pflag.Flag{ Name: cobraParamName, Shorthand: "s", - Usage: fmt.Sprintf("Specifies service(s) to export. Accepts comma-separated string to delimit multiple services. Allowed: [%s]. Also configurable via environment variable %s", strings.Join(customtypes.MultiServiceValidValues(), ", "), envVar), + Usage: fmt.Sprintf("Specifies service(s) to export. Accepts comma-separated string to delimit multiple services. Allowed: [%s]. Also configurable via environment variable %s", strings.Join(customtypes.ExportServicesValidValues(), ", "), envVar), Value: cobraValue, - DefValue: strings.Join(customtypes.MultiServiceValidValues(), ", "), + DefValue: strings.Join(customtypes.ExportServicesValidValues(), ", "), }, - Type: options.ENUM_MULTI_SERVICE, + Type: options.ENUM_EXPORT_SERVICES, ViperKey: "export.services", } } @@ -129,395 +110,43 @@ func initOverwriteOption() { } } -func initPingOneWorkerEnvironmentIDOption() { - cobraParamName := "pingone-worker-environment-id" - cobraValue := new(customtypes.UUID) - defaultValue := customtypes.UUID("") - envVar := "PINGCTL_PINGONE_WORKER_ENVIRONMENT_ID" - - options.PlatformExportPingoneWorkerEnvironmentIDOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The ID of the PingOne environment that contains the worker client used to authenticate. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_UUID, - ViperKey: "export.pingone.worker.environmentID", +func getDefaultExportDir() (defaultExportDir *customtypes.String) { + l := logger.Get() + pwd, err := os.Getwd() + if err != nil { + l.Err(err).Msg("Failed to determine current working directory") + return nil } -} -func initPingOneExportEnvironmentIDOption() { - cobraParamName := "pingone-export-environment-id" - cobraValue := new(customtypes.UUID) - defaultValue := customtypes.UUID("") - envVar := "PING_CTL_PINGONE_EXPORT_ENVIRONMENT_ID" + defaultExportDir = new(customtypes.String) - options.PlatformExportPingoneExportEnvironmentIDOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The ID of the PingOne environment to export. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_UUID, - ViperKey: "export.pingone.export.environmentID", + err = defaultExportDir.Set(fmt.Sprintf("%s/export", pwd)) + if err != nil { + l.Err(err).Msg("Failed to set default export directory") + return nil } + + return defaultExportDir } -func initPingOneWorkerClientIDOption() { - cobraParamName := "pingone-worker-client-id" +func initPingOneEnvironmentIDOption() { + cobraParamName := "pingone-export-environment-id" cobraValue := new(customtypes.UUID) defaultValue := customtypes.UUID("") - envVar := "PINGCTL_PINGONE_WORKER_CLIENT_ID" + envVar := "PINGCTL_PINGONE_EXPORT_ENVIRONMENT_ID" - options.PlatformExportPingoneWorkerClientIDOption = options.Option{ + options.PlatformExportPingoneEnvironmentIDOption = options.Option{ CobraParamName: cobraParamName, CobraParamValue: cobraValue, DefaultValue: &defaultValue, EnvVar: envVar, Flag: &pflag.Flag{ Name: cobraParamName, - Usage: fmt.Sprintf("The ID of the PingOne worker client used to authenticate. Also configurable via environment variable %s", envVar), + Usage: fmt.Sprintf("The ID of the Pingone environment to export. Also configurable via environment variable %s", envVar), Value: cobraValue, DefValue: "", }, Type: options.ENUM_UUID, - ViperKey: "export.pingone.worker.clientID", - } -} - -func initPingOneWorkerClientSecretOption() { - cobraParamName := "pingone-worker-client-secret" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("") - envVar := "PINGCTL_PINGONE_WORKER_CLIENT_SECRET" - - options.PlatformExportPingoneWorkerClientSecretOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingOne worker client secret used to authenticate. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingone.worker.clientSecret", - } -} - -func initPingOneRegionOption() { - cobraParamName := "pingone-region" - cobraValue := new(customtypes.PingOneRegion) - defaultValue := customtypes.PingOneRegion("") - envVar := "PINGCTL_PINGONE_REGION" - - options.PlatformExportPingoneRegionOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The region of the PingOne service(s). Allowed: %s. Also configurable via environment variable %s", strings.Join(customtypes.PingOneRegionValidValues(), ", "), envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingone.region", - } -} - -func initPingFederateHTTPSHostOption() { - cobraParamName := "pingfederate-https-host" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("") - envVar := "PINGCTL_PINGFEDERATE_HTTPS_HOST" - - options.PlatformExportPingfederateHTTPSHostOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate HTTPS host used to communicate with PingFederate's API. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingfederate.httpsHost", - } -} - -func initPingFederateAdminAPIPathOption() { - cobraParamName := "pingfederate-admin-api-path" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("/pf-admin-api/v1") - envVar := "PINGCTL_PINGFEDERATE_ADMIN_API_PATH" - - options.PlatformExportPingfederateAdminAPIPathOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate API URL path used to communicate with PingFederate's API. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "/pf-admin-api/v1", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingfederate.adminAPIPath", - } -} - -func initPingFederateXBypassExternalValidationHeaderOption() { - cobraParamName := "pingfederate-x-bypass-external-validation-header" - cobraValue := new(customtypes.Bool) - defaultValue := customtypes.Bool(false) - envVar := "PINGCTL_PINGFEDERATE_X_BYPASS_EXTERNAL_VALIDATION_HEADER" - - options.PlatformExportPingfederateXBypassExternalValidationHeaderOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("Header value in request for PingFederate. PingFederate's connection tests will be bypassed when set to true. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "false", - }, - Type: options.ENUM_BOOL, - ViperKey: "export.pingfederate.xBypassExternalValidationHeader", - } -} - -func initPingFederateCACertificatePemFilesOption() { - cobraParamName := "pingfederate-ca-certificate-pem-files" - cobraValue := new(customtypes.StringSlice) - defaultValue := customtypes.StringSlice{} - envVar := "PINGCTL_PINGFEDERATE_CA_CERTIFICATE_PEM_FILES" - - options.PlatformExportPingfederateCACertificatePemFilesOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("Paths to files containing PEM-encoded certificates to be trusted as root CAs when connecting to the PingFederate server over HTTPS. Accepts comma-separated string to delimit multiple PEM files. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "[]", - }, - Type: options.ENUM_STRING_SLICE, - ViperKey: "export.pingfederate.caCertificatePemFiles", - } -} - -func initPingFederateInsecureTrustAllTLSOption() { - cobraParamName := "pingfederate-insecure-trust-all-tls" - cobraValue := new(customtypes.Bool) - defaultValue := customtypes.Bool(false) - envVar := "PINGCTL_PINGFEDERATE_INSECURE_TRUST_ALL_TLS" - - options.PlatformExportPingfederateInsecureTrustAllTLSOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("Set to true to trust any certificate when connecting to the PingFederate server. This is insecure and should not be enabled outside of testing. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "false", - }, - Type: options.ENUM_BOOL, - ViperKey: "export.pingfederate.insecureTrustAllTLS", - } -} - -func initPingFederateUsernameOption() { - cobraParamName := "pingfederate-username" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("") - envVar := "PINGCTL_PINGFEDERATE_USERNAME" - - options.PlatformExportPingfederateUsernameOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate username used to authenticate. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingfederate.basicAuth.username", + ViperKey: "export.pingone.environmentID", } } - -func initPingFederatePasswordOption() { - cobraParamName := "pingfederate-password" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("") - envVar := "PINGCTL_PINGFEDERATE_PASSWORD" - - options.PlatformExportPingfederatePasswordOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate password used to authenticate. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingfederate.basicAuth.password", - } -} - -func initPingFederateAccessTokenOption() { - cobraParamName := "pingfederate-access-token" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("") - envVar := "PINGCTL_PINGFEDERATE_ACCESS_TOKEN" - - options.PlatformExportPingfederateAccessTokenOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate access token used to authenticate. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingfederate.accessTokenAuth.accessToken", - } -} - -func initPingFederateClientIDOption() { - cobraParamName := "pingfederate-client-id" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("") - envVar := "PINGCTL_PINGFEDERATE_CLIENT_ID" - - options.PlatformExportPingfederateClientIDOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate OAuth client ID used to authenticate. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingfederate.clientCredentialsAuth.clientID", - } -} - -func initPingFederateClientSecretOption() { - cobraParamName := "pingfederate-client-secret" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("") - envVar := "PINGCTL_PINGFEDERATE_CLIENT_SECRET" - - options.PlatformExportPingfederateClientSecretOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate OAuth client secret used to authenticate. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingfederate.clientCredentialsAuth.clientSecret", - } -} - -func initPingFederateTokenURLOption() { - cobraParamName := "pingfederate-token-url" - cobraValue := new(customtypes.String) - defaultValue := customtypes.String("") - envVar := "PINGCTL_PINGFEDERATE_TOKEN_URL" - - options.PlatformExportPingfederateTokenURLOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate OAuth token URL used to authenticate. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "", - }, - Type: options.ENUM_STRING, - ViperKey: "export.pingfederate.clientCredentialsAuth.tokenURL", - } -} - -func initPingFederateScopesOption() { - cobraParamName := "pingfederate-scopes" - cobraValue := new(customtypes.StringSlice) - defaultValue := customtypes.StringSlice{} - envVar := "PINGCTL_PINGFEDERATE_SCOPES" - - options.PlatformExportPingfederateScopesOption = options.Option{ - CobraParamName: cobraParamName, - CobraParamValue: cobraValue, - DefaultValue: &defaultValue, - EnvVar: envVar, - Flag: &pflag.Flag{ - Name: cobraParamName, - Usage: fmt.Sprintf("The PingFederate OAuth scopes used to authenticate. Accepts comma-separated string to delimit multiple scopes. Also configurable via environment variable %s", envVar), - Value: cobraValue, - DefValue: "[]", - }, - Type: options.ENUM_STRING_SLICE, - ViperKey: "export.pingfederate.clientCredentialsAuth.scopes", - } -} - -func getDefaultExportDir() (defaultExportDir *customtypes.String) { - l := logger.Get() - pwd, err := os.Getwd() - if err != nil { - l.Err(err).Msg("Failed to determine current working directory") - return nil - } - - defaultExportDir = new(customtypes.String) - - err = defaultExportDir.Set(fmt.Sprintf("%s/export", pwd)) - if err != nil { - l.Err(err).Msg("Failed to set default export directory") - return nil - } - - return defaultExportDir -} diff --git a/internal/configuration/request/request.go b/internal/configuration/request/request.go new file mode 100644 index 0000000..1c3fafd --- /dev/null +++ b/internal/configuration/request/request.go @@ -0,0 +1,115 @@ +package configuration_request + +import ( + "fmt" + "strings" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitRequestOptions() { + initDataOption() + initHTTPMethodOption() + initServiceOption() + initAccessTokenOption() + initAccessTokenExpiryOption() + +} + +func initDataOption() { + cobraParamName := "data" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_REQUEST_DATA" + + options.RequestDataOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The data to send in the request. Use prefix '@' to specify data filepath instead of raw data. Also configurable via environment variable %s.", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} + +func initHTTPMethodOption() { + cobraParamName := "http-method" + cobraValue := new(customtypes.HTTPMethod) + defaultValue := customtypes.HTTPMethod(customtypes.ENUM_HTTP_METHOD_GET) + envVar := "PINGCTL_REQUEST_HTTP_METHOD" + + options.RequestHTTPMethodOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "m", + Usage: fmt.Sprintf("The HTTP method to use for the request. Allowed: %s. Also configurable via environment variable %s.", strings.Join(customtypes.HTTPMethodValidValues(), ", "), envVar), + Value: cobraValue, + DefValue: customtypes.ENUM_HTTP_METHOD_GET, + }, + Type: options.ENUM_REQUEST_HTTP_METHOD, + ViperKey: "request.httpMethod", + } +} + +func initServiceOption() { + cobraParamName := "service" + cobraValue := new(customtypes.RequestService) + defaultValue := customtypes.RequestService("") + envVar := "PINGCTL_REQUEST_SERVICE" + + options.RequestServiceOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "s", + Usage: fmt.Sprintf("The service to send a custom request. Allowed: %s. Also configurable via environment variable %s.", strings.Join(customtypes.RequestServiceValidValues(), ", "), envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_REQUEST_SERVICE, + ViperKey: "request.service", + } +} + +func initAccessTokenOption() { + defaultValue := customtypes.String("") + + options.RequestAccessTokenOption = options.Option{ + CobraParamName: "", // No cobra param name + CobraParamValue: nil, // No cobra param value + DefaultValue: &defaultValue, // No default value + EnvVar: "", // No environment variable + Flag: nil, + Type: options.ENUM_STRING, + ViperKey: "request.accessToken", + } +} + +func initAccessTokenExpiryOption() { + defaultValue := customtypes.Int(0) + + options.RequestAccessTokenExpiryOption = options.Option{ + CobraParamName: "", // No cobra param name + CobraParamValue: nil, // No cobra param value + DefaultValue: &defaultValue, // No default value + EnvVar: "", // No environment variable + Flag: nil, // No flag + Type: options.ENUM_INT, + ViperKey: "request.accessTokenExpiry", + } +} diff --git a/internal/configuration/root/root.go b/internal/configuration/root/root.go index 3164a05..d815816 100644 --- a/internal/configuration/root/root.go +++ b/internal/configuration/root/root.go @@ -57,7 +57,7 @@ func initColorOption() { DefValue: "true", }, Type: options.ENUM_BOOL, - ViperKey: "pingctl.color", + ViperKey: "color", } } @@ -101,7 +101,7 @@ func initOutputFormatOption() { DefValue: customtypes.ENUM_OUTPUT_FORMAT_TEXT, }, Type: options.ENUM_OUTPUT_FORMAT, - ViperKey: "pingctl.outputFormat", + ViperKey: "outputFormat", } } diff --git a/internal/configuration/services/pingfederate.go b/internal/configuration/services/pingfederate.go new file mode 100644 index 0000000..8128ef6 --- /dev/null +++ b/internal/configuration/services/pingfederate.go @@ -0,0 +1,312 @@ +package configuration_services + +import ( + "fmt" + "strings" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitPingfederateServiceOptions() { + initHTTPSHostOption() + initAdminAPIPathOption() + initXBypassExternalValidationHeaderOption() + initCACertificatePemFilesOption() + initInsecureTrustAllTLSOption() + initUsernameOption() + initPasswordOption() + initAccessTokenOption() + initClientIDOption() + initClientSecretOption() + initTokenURLOption() + initScopesOption() + initPingfederateAuthenticationTypeOption() +} + +func initHTTPSHostOption() { + cobraParamName := "pingfederate-https-host" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_HTTPS_HOST" + + options.PingfederateHTTPSHostOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate HTTPS host used to communicate with PingFederate's API. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingfederate.httpsHost", + } +} + +func initAdminAPIPathOption() { + cobraParamName := "pingfederate-admin-api-path" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("/pf-admin-api/v1") + envVar := "PINGCTL_PINGFEDERATE_ADMIN_API_PATH" + + options.PingfederateAdminAPIPathOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate API URL path used to communicate with PingFederate's API. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "/pf-admin-api/v1", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingfederate.adminAPIPath", + } +} + +func initXBypassExternalValidationHeaderOption() { + cobraParamName := "pingfederate-x-bypass-external-validation-header" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(false) + envVar := "PINGCTL_PINGFEDERATE_X_BYPASS_EXTERNAL_VALIDATION_HEADER" + + options.PingfederateXBypassExternalValidationHeaderOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("Header value in request for PingFederate. PingFederate's connection tests will be bypassed when set to true. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "false", + }, + Type: options.ENUM_BOOL, + ViperKey: "service.pingfederate.xBypassExternalValidationHeader", + } +} + +func initCACertificatePemFilesOption() { + cobraParamName := "pingfederate-ca-certificate-pem-files" + cobraValue := new(customtypes.StringSlice) + defaultValue := customtypes.StringSlice{} + envVar := "PINGCTL_PINGFEDERATE_CA_CERTIFICATE_PEM_FILES" + + options.PingfederateCACertificatePemFilesOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("Paths to files containing PEM-encoded certificates to be trusted as root CAs when connecting to the PingFederate server over HTTPS. Accepts comma-separated string to delimit multiple PEM files. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "[]", + }, + Type: options.ENUM_STRING_SLICE, + ViperKey: "service.pingfederate.caCertificatePemFiles", + } +} + +func initInsecureTrustAllTLSOption() { + cobraParamName := "pingfederate-insecure-trust-all-tls" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(false) + envVar := "PINGCTL_PINGFEDERATE_INSECURE_TRUST_ALL_TLS" + + options.PingfederateInsecureTrustAllTLSOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("Set to true to trust any certificate when connecting to the PingFederate server. This is insecure and should not be enabled outside of testing. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "false", + }, + Type: options.ENUM_BOOL, + ViperKey: "service.pingfederate.insecureTrustAllTLS", + } +} + +func initUsernameOption() { + cobraParamName := "pingfederate-username" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_USERNAME" + + options.PingfederateBasicAuthUsernameOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate username used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingfederate.authentication.basicAuth.username", + } +} + +func initPasswordOption() { + cobraParamName := "pingfederate-password" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_PASSWORD" + + options.PingfederateBasicAuthPasswordOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate password used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingfederate.authentication.basicAuth.password", + } +} + +func initAccessTokenOption() { + cobraParamName := "pingfederate-access-token" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_ACCESS_TOKEN" + + options.PingfederateAccessTokenAuthAccessTokenOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate access token used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingfederate.authentication.accessTokenAuth.accessToken", + } +} + +func initClientIDOption() { + cobraParamName := "pingfederate-client-id" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_CLIENT_ID" + + options.PingfederateClientCredentialsAuthClientIDOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate OAuth client ID used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingfederate.authentication.clientCredentialsAuth.clientID", + } +} + +func initClientSecretOption() { + cobraParamName := "pingfederate-client-secret" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_CLIENT_SECRET" + + options.PingfederateClientCredentialsAuthClientSecretOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate OAuth client secret used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingfederate.authentication.clientCredentialsAuth.clientSecret", + } +} + +func initTokenURLOption() { + cobraParamName := "pingfederate-token-url" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_TOKEN_URL" + + options.PingfederateClientCredentialsAuthTokenURLOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate OAuth token URL used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingfederate.authentication.clientCredentialsAuth.tokenURL", + } +} + +func initScopesOption() { + cobraParamName := "pingfederate-scopes" + cobraValue := new(customtypes.StringSlice) + defaultValue := customtypes.StringSlice{} + envVar := "PINGCTL_PINGFEDERATE_SCOPES" + + options.PingfederateClientCredentialsAuthScopesOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate OAuth scopes used to authenticate. Accepts comma-separated string to delimit multiple scopes. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "[]", + }, + Type: options.ENUM_STRING_SLICE, + ViperKey: "service.pingfederate.authentication.clientCredentialsAuth.scopes", + } +} + +func initPingfederateAuthenticationTypeOption() { + cobraParamName := "pingfederate-authentication-type" + cobraValue := new(customtypes.PingfederateAuthenticationType) + defaultValue := customtypes.PingfederateAuthenticationType("") + envVar := "PINGCTL_PINGFEDERATE_AUTHENTICATION_TYPE" + + options.PingfederateAuthenticationTypeOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The authentication type to use with the PingFederate service. Allowed: %s. Also configurable via environment variable %s", strings.Join(customtypes.PingfederateAuthenticationTypeValidValues(), ", "), envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_PINGFEDERATE_AUTH_TYPE, + ViperKey: "service.pingfederate.authentication.type", + } +} diff --git a/internal/configuration/services/pingone.go b/internal/configuration/services/pingone.go new file mode 100644 index 0000000..a8708f5 --- /dev/null +++ b/internal/configuration/services/pingone.go @@ -0,0 +1,129 @@ +package configuration_services + +import ( + "fmt" + "strings" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitPingoneServiceOptions() { + initPingoneAuthenticationTypeOption() + initAuthenticationWorkerClientIDOption() + initAuthenticationWorkerClientSecretOption() + initAuthenticationWorkerEnvironmentIDOption() + initRegionCodeOption() + +} + +func initAuthenticationWorkerClientIDOption() { + cobraParamName := "pingone-worker-client-id" + cobraValue := new(customtypes.UUID) + defaultValue := customtypes.UUID("") + envVar := "PINGCTL_PINGONE_WORKER_CLIENT_ID" + + options.PingoneAuthenticationWorkerClientIDOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The Pingone worker client ID used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_UUID, + ViperKey: "service.pingone.authentication.worker.clientID", + } +} + +func initAuthenticationWorkerClientSecretOption() { + cobraParamName := "pingone-worker-client-secret" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGONE_WORKER_CLIENT_SECRET" + + options.PingoneAuthenticationWorkerClientSecretOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The Pingone worker client secret used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "service.pingone.authentication.worker.clientSecret", + } +} + +func initAuthenticationWorkerEnvironmentIDOption() { + cobraParamName := "pingone-worker-environment-id" + cobraValue := new(customtypes.UUID) + defaultValue := customtypes.UUID("") + envVar := "PINGCTL_PINGONE_WORKER_ENVIRONMENT_ID" + + options.PingoneAuthenticationWorkerEnvironmentIDOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The ID of the Pingone environment that contains the worker client used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_UUID, + ViperKey: "service.pingone.authentication.worker.environmentID", + } +} + +func initPingoneAuthenticationTypeOption() { + cobraParamName := "pingone-authentication-type" + cobraValue := new(customtypes.PingoneAuthenticationType) + defaultValue := customtypes.PingoneAuthenticationType("") + envVar := "PINGCTL_PINGONE_AUTHENTICATION_TYPE" + + options.PingoneAuthenticationTypeOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The authentication type to use with the Pingone service. Allowed: %s. Also configurable via environment variable %s", strings.Join(customtypes.PingoneAuthenticationTypeValidValues(), ", "), envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_PINGONE_AUTH_TYPE, + ViperKey: "service.pingone.authentication.type", + } +} + +func initRegionCodeOption() { + cobraParamName := "pingone-region-code" + cobraValue := new(customtypes.PingoneRegionCode) + defaultValue := customtypes.PingoneRegionCode("") + envVar := "PINGCTL_PINGONE_REGION_CODE" + + options.PingoneRegionCodeOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The region code of the Pingone service. Allowed: %s. Also configurable via environment variable %s", strings.Join(customtypes.PingoneRegionCodeValidValues(), ", "), envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_PINGONE_REGION_CODE, + ViperKey: "service.pingone.regionCode", + } +} diff --git a/internal/connector/common/common_utils.go b/internal/connector/common/common_utils.go index 39fe668..8c221f6 100644 --- a/internal/connector/common/common_utils.go +++ b/internal/connector/common/common_utils.go @@ -56,7 +56,7 @@ func WriteFiles(exportableResources []connector.ExportableResource, format, outp importBlock.Sanitize() switch format { - case connector.ENUMEXPORTFORMAT_HCL: + case customtypes.ENUM_EXPORT_FORMAT_HCL: err := hclImportBlockTemplate.Execute(outputFile, importBlock) if err != nil { return fmt.Errorf("failed to write import block template to file %q. err: %s", outputFilePath, err.Error()) diff --git a/internal/connector/exportable.go b/internal/connector/exportable.go index 18b8f5e..4e4dba0 100644 --- a/internal/connector/exportable.go +++ b/internal/connector/exportable.go @@ -4,10 +4,6 @@ import ( _ "embed" ) -const ( - ENUMEXPORTFORMAT_HCL = "HCL" -) - // Embed import block template needed for export generation // //go:embed templates/hcl_import_block.template diff --git a/internal/customtypes/export_format.go b/internal/customtypes/export_format.go index a744d8c..ce25685 100644 --- a/internal/customtypes/export_format.go +++ b/internal/customtypes/export_format.go @@ -5,10 +5,13 @@ import ( "slices" "strings" - "github.com/pingidentity/pingctl/internal/connector" "github.com/spf13/pflag" ) +const ( + ENUM_EXPORT_FORMAT_HCL string = "HCL" +) + type ExportFormat string // Verify that the custom type satisfies the pflag.Value interface @@ -21,9 +24,9 @@ func (ef *ExportFormat) Set(format string) error { return fmt.Errorf("failed to set Export Format value: %s. Export Format is nil", format) } - switch format { - case connector.ENUMEXPORTFORMAT_HCL: - *ef = ExportFormat(format) + switch { + case strings.EqualFold(format, ENUM_EXPORT_FORMAT_HCL): + *ef = ExportFormat(ENUM_EXPORT_FORMAT_HCL) default: return fmt.Errorf("unrecognized export format '%s'. Must be one of: %s", format, strings.Join(ExportFormatValidValues(), ", ")) } @@ -40,7 +43,7 @@ func (ef ExportFormat) String() string { func ExportFormatValidValues() []string { exportFormats := []string{ - connector.ENUMEXPORTFORMAT_HCL, + ENUM_EXPORT_FORMAT_HCL, } slices.Sort(exportFormats) diff --git a/internal/customtypes/export_format_test.go b/internal/customtypes/export_format_test.go index ac7d496..296beef 100644 --- a/internal/customtypes/export_format_test.go +++ b/internal/customtypes/export_format_test.go @@ -3,7 +3,6 @@ package customtypes_test import ( "testing" - "github.com/pingidentity/pingctl/internal/connector" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/testing/testutils" ) @@ -13,7 +12,7 @@ func Test_ExportFormat_Set(t *testing.T) { // Create a new ExportFormat exportFormat := new(customtypes.ExportFormat) - err := exportFormat.Set(connector.ENUMEXPORTFORMAT_HCL) + err := exportFormat.Set(customtypes.ENUM_EXPORT_FORMAT_HCL) if err != nil { t.Errorf("Set returned error: %v", err) } @@ -35,7 +34,7 @@ func Test_ExportFormat_Set_InvalidValue(t *testing.T) { func Test_ExportFormat_Set_Nil(t *testing.T) { var exportFormat *customtypes.ExportFormat - val := connector.ENUMEXPORTFORMAT_HCL + val := customtypes.ENUM_EXPORT_FORMAT_HCL expectedErrorPattern := `^failed to set Export Format value: .* Export Format is nil$` err := exportFormat.Set(val) @@ -44,9 +43,9 @@ func Test_ExportFormat_Set_Nil(t *testing.T) { // Test String function func Test_ExportFormat_String(t *testing.T) { - exportFormat := customtypes.ExportFormat(connector.ENUMEXPORTFORMAT_HCL) + exportFormat := customtypes.ExportFormat(customtypes.ENUM_EXPORT_FORMAT_HCL) - expected := connector.ENUMEXPORTFORMAT_HCL + expected := customtypes.ENUM_EXPORT_FORMAT_HCL actual := exportFormat.String() if actual != expected { t.Errorf("String returned: %s, expected: %s", actual, expected) diff --git a/internal/customtypes/export_services.go b/internal/customtypes/export_services.go new file mode 100644 index 0000000..41f90d6 --- /dev/null +++ b/internal/customtypes/export_services.go @@ -0,0 +1,106 @@ +package customtypes + +import ( + "fmt" + "slices" + "strings" + + "github.com/spf13/pflag" +) + +const ( + ENUM_EXPORT_SERVICE_PINGONE_PLATFORM string = "pingone-platform" + ENUM_EXPORT_SERVICE_PINGONE_SSO string = "pingone-sso" + ENUM_EXPORT_SERVICE_PINGONE_MFA string = "pingone-mfa" + ENUM_EXPORT_SERVICE_PINGONE_PROTECT string = "pingone-protect" + ENUM_EXPORT_SERVICE_PINGFEDERATE string = "pingfederate" +) + +type ExportServices []string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*ExportServices)(nil) + +// Implement pflag.Value interface for custom type in cobra MultiService parameter +func (es ExportServices) GetServices() []string { + return []string(es) +} + +func (es *ExportServices) Set(services string) error { + if es == nil { + return fmt.Errorf("failed to set ExportServices value: %s. ExportServices is nil", services) + } + + validServices := ExportServicesValidValues() + serviceList := strings.Split(services, ",") + + for i, service := range serviceList { + if !slices.ContainsFunc(validServices, func(validService string) bool { + if strings.EqualFold(validService, service) { + serviceList[i] = validService + return true + } + return false + }) { + return fmt.Errorf("failed to set ExportServices: Invalid service: %s. Allowed services: %s", service, strings.Join(validServices, ", ")) + } + } + + slices.Sort(serviceList) + + *es = ExportServices(serviceList) + return nil +} + +func (es ExportServices) ContainsPingOneService() bool { + if es == nil { + return false + } + + pingoneServices := []string{ + ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, + ENUM_EXPORT_SERVICE_PINGONE_SSO, + ENUM_EXPORT_SERVICE_PINGONE_MFA, + ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + } + + for _, service := range es { + if slices.ContainsFunc(pingoneServices, func(s string) bool { + return strings.EqualFold(s, service) + }) { + return true + } + } + + return false +} + +func (es ExportServices) ContainsPingFederateService() bool { + if es == nil { + return false + } + + return slices.Contains(es, ENUM_EXPORT_SERVICE_PINGFEDERATE) +} + +func (es ExportServices) Type() string { + return "[]string" +} + +func (es ExportServices) String() string { + return strings.Join(es, ",") +} + +func ExportServicesValidValues() []string { + allServices := []string{ + ENUM_EXPORT_SERVICE_PINGFEDERATE, + ENUM_EXPORT_SERVICE_PINGONE_PLATFORM, + ENUM_EXPORT_SERVICE_PINGONE_SSO, + ENUM_EXPORT_SERVICE_PINGONE_MFA, + ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + } + + slices.Sort(allServices) + + return allServices +} diff --git a/internal/customtypes/export_services_test.go b/internal/customtypes/export_services_test.go new file mode 100644 index 0000000..4a81600 --- /dev/null +++ b/internal/customtypes/export_services_test.go @@ -0,0 +1,95 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test ExportServices Set function +func Test_ExportServices_Set(t *testing.T) { + es := new(customtypes.ExportServices) + + service := customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA + err := es.Set(service) + if err != nil { + t.Errorf("Set returned error: %v", err) + } + + services := es.GetServices() + if len(services) != 1 { + t.Errorf("GetServices returned: %v, expected: %v", services, service) + } + + if services[0] != service { + t.Errorf("GetServices returned: %v, expected: %v", services, service) + } +} + +// Test ExportServices Set function with invalid value +func Test_ExportServices_Set_InvalidValue(t *testing.T) { + es := new(customtypes.ExportServices) + + invalidValue := "invalid" + expectedErrorPattern := `^failed to set ExportServices: Invalid service: .*\. Allowed services: .*$` + err := es.Set(invalidValue) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test ExportServices Set function with nil +func Test_ExportServices_Set_Nil(t *testing.T) { + var es *customtypes.ExportServices + + service := customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA + expectedErrorPattern := `^failed to set ExportServices value: .* ExportServices is nil$` + err := es.Set(service) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test ExportServices ContainsPingOneService function +func Test_ExportServices_ContainsPingOneService(t *testing.T) { + es := new(customtypes.ExportServices) + + service := customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA + err := es.Set(service) + if err != nil { + t.Errorf("Set returned error: %v", err) + } + + if !es.ContainsPingOneService() { + t.Errorf("ContainsPingOneService returned false, expected true") + } +} + +// Test ExportServices ContainsPingFederateService function +func Test_ExportServices_ContainsPingFederateService(t *testing.T) { + es := new(customtypes.ExportServices) + + service := customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE + err := es.Set(service) + if err != nil { + t.Errorf("Set returned error: %v", err) + } + + if !es.ContainsPingFederateService() { + t.Errorf("ContainsPingFederateService returned false, expected true") + } +} + +// Test ExportServices String function +func Test_ExportServices_String(t *testing.T) { + es := new(customtypes.ExportServices) + + service := customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA + err := es.Set(service) + if err != nil { + t.Errorf("Set returned error: %v", err) + } + + expected := service + actual := es.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} diff --git a/internal/customtypes/http_method.go b/internal/customtypes/http_method.go new file mode 100644 index 0000000..5656d3a --- /dev/null +++ b/internal/customtypes/http_method.go @@ -0,0 +1,68 @@ +package customtypes + +import ( + "fmt" + "slices" + "strings" + + "github.com/spf13/pflag" +) + +const ( + ENUM_HTTP_METHOD_GET string = "GET" + ENUM_HTTP_METHOD_POST string = "POST" + ENUM_HTTP_METHOD_PUT string = "PUT" + ENUM_HTTP_METHOD_DELETE string = "DELETE" + ENUM_HTTP_METHOD_PATCH string = "PATCH" +) + +type HTTPMethod string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*HTTPMethod)(nil) + +// Implement pflag.Value interface for custom type in cobra pingctl-output parameter + +func (hm *HTTPMethod) Set(httpMethod string) error { + if hm == nil { + return fmt.Errorf("failed to set HTTP Method value: %s. HTTPMethod is nil", httpMethod) + } + + switch { + case strings.EqualFold(httpMethod, ENUM_HTTP_METHOD_GET): + *hm = HTTPMethod(ENUM_HTTP_METHOD_GET) + case strings.EqualFold(httpMethod, ENUM_HTTP_METHOD_POST): + *hm = HTTPMethod(ENUM_HTTP_METHOD_POST) + case strings.EqualFold(httpMethod, ENUM_HTTP_METHOD_PUT): + *hm = HTTPMethod(ENUM_HTTP_METHOD_PUT) + case strings.EqualFold(httpMethod, ENUM_HTTP_METHOD_DELETE): + *hm = HTTPMethod(ENUM_HTTP_METHOD_DELETE) + case strings.EqualFold(httpMethod, ENUM_HTTP_METHOD_PATCH): + *hm = HTTPMethod(ENUM_HTTP_METHOD_PATCH) + default: + return fmt.Errorf("unrecognized HTTP Method: '%s'. Must be one of: %s", httpMethod, strings.Join(HTTPMethodValidValues(), ", ")) + } + return nil +} + +func (hm HTTPMethod) Type() string { + return "string" +} + +func (hm HTTPMethod) String() string { + return string(hm) +} + +func HTTPMethodValidValues() []string { + methods := []string{ + ENUM_HTTP_METHOD_GET, + ENUM_HTTP_METHOD_POST, + ENUM_HTTP_METHOD_PUT, + ENUM_HTTP_METHOD_DELETE, + ENUM_HTTP_METHOD_PATCH, + } + + slices.Sort(methods) + + return methods +} diff --git a/internal/customtypes/http_method_test.go b/internal/customtypes/http_method_test.go new file mode 100644 index 0000000..c8212fb --- /dev/null +++ b/internal/customtypes/http_method_test.go @@ -0,0 +1,49 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test HTTP Method Set function +func Test_HTTPMethod_Set(t *testing.T) { + // Create a new HTTPMethod + httpMethod := new(customtypes.HTTPMethod) + + err := httpMethod.Set(customtypes.ENUM_HTTP_METHOD_GET) + if err != nil { + t.Errorf("Set returned error: %v", err) + } +} + +// Test Set function fails with invalid value +func Test_HTTPMethod_Set_InvalidValue(t *testing.T) { + httpMethod := new(customtypes.HTTPMethod) + + invalidValue := "invalid" + expectedErrorPattern := `^unrecognized HTTP Method: '.*'. Must be one of: .*$` + err := httpMethod.Set(invalidValue) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Set function fails with nil +func Test_HTTPMethod_Set_Nil(t *testing.T) { + var httpMethod *customtypes.HTTPMethod + + expectedErrorPattern := `^failed to set HTTP Method value: .*\. HTTPMethod is nil$` + err := httpMethod.Set(customtypes.ENUM_HTTP_METHOD_GET) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test String function +func Test_HTTPMethod_String(t *testing.T) { + httpMethod := customtypes.HTTPMethod(customtypes.ENUM_HTTP_METHOD_GET) + + expected := customtypes.ENUM_HTTP_METHOD_GET + actual := httpMethod.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} diff --git a/internal/customtypes/int.go b/internal/customtypes/int.go new file mode 100644 index 0000000..413cbb4 --- /dev/null +++ b/internal/customtypes/int.go @@ -0,0 +1,39 @@ +package customtypes + +import ( + "fmt" + "strconv" + + "github.com/spf13/pflag" +) + +type Int int64 + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*Int)(nil) + +func (i *Int) Set(val string) error { + if i == nil { + return fmt.Errorf("failed to set Int value: %s. Int is nil", val) + } + + parsedInt, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return err + } + *i = Int(parsedInt) + + return nil +} + +func (i Int) Type() string { + return "int64" +} + +func (i Int) String() string { + return strconv.FormatInt(int64(i), 10) +} + +func (i Int) Int64() int64 { + return int64(i) +} diff --git a/internal/customtypes/int_test.go b/internal/customtypes/int_test.go new file mode 100644 index 0000000..8c5f375 --- /dev/null +++ b/internal/customtypes/int_test.go @@ -0,0 +1,49 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test Int Set function +func Test_Int_Set(t *testing.T) { + i := new(customtypes.Int) + + err := i.Set("42") + if err != nil { + t.Errorf("Set returned error: %v", err) + } +} + +// Test Set function fails with invalid value +func Test_Int_Set_InvalidValue(t *testing.T) { + i := new(customtypes.Int) + + invalidValue := "invalid" + expectedErrorPattern := `^strconv.ParseInt: parsing ".*": invalid syntax$` + err := i.Set(invalidValue) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Set function fails with nil +func Test_Int_Set_Nil(t *testing.T) { + var i *customtypes.Int + val := "42" + + expectedErrorPattern := `^failed to set Int value: .* Int is nil$` + err := i.Set(val) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test String function +func Test_Int_String(t *testing.T) { + i := customtypes.Int(42) + + expected := "42" + actual := i.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} diff --git a/internal/customtypes/multi_service.go b/internal/customtypes/multi_service.go deleted file mode 100644 index 2cb905c..0000000 --- a/internal/customtypes/multi_service.go +++ /dev/null @@ -1,133 +0,0 @@ -package customtypes - -import ( - "fmt" - "slices" - "strings" - - "github.com/spf13/pflag" -) - -const ( - ENUM_SERVICE_PINGONE_PLATFORM string = "pingone-platform" - ENUM_SERVICE_PINGONE_SSO string = "pingone-sso" - ENUM_SERVICE_PINGONE_MFA string = "pingone-mfa" - ENUM_SERVICE_PINGONE_PROTECT string = "pingone-protect" - ENUM_SERVICE_PINGFEDERATE string = "pingfederate" -) - -type MultiService map[string]bool - -// Verify that the custom type satisfies the pflag.Value interface -var _ pflag.Value = (*MultiService)(nil) - -// Implement pflag.Value interface for custom type in cobra MultiService parameter - -func NewMultiService() *MultiService { - ms := map[string]bool{ - ENUM_SERVICE_PINGFEDERATE: true, - ENUM_SERVICE_PINGONE_PLATFORM: true, - ENUM_SERVICE_PINGONE_SSO: true, - ENUM_SERVICE_PINGONE_MFA: true, - ENUM_SERVICE_PINGONE_PROTECT: true, - } - - return (*MultiService)(&ms) -} - -func (ms MultiService) GetServices() []string { - enabledExportServices := []string{} - - if ms == nil { - return enabledExportServices - } - - for k, v := range ms { - if v { - enabledExportServices = append(enabledExportServices, k) - } - } - - slices.Sort(enabledExportServices) - - return enabledExportServices -} - -func (ms *MultiService) Set(services string) error { - if ms == nil { - return fmt.Errorf("failed to set MultiService value: %s. MultiService is nil", services) - } - - *ms = map[string]bool{} - - serviceList := strings.Split(services, ",") - - for _, service := range serviceList { - switch service { - case ENUM_SERVICE_PINGFEDERATE: - (*ms)[ENUM_SERVICE_PINGFEDERATE] = true - case ENUM_SERVICE_PINGONE_PLATFORM: - (*ms)[ENUM_SERVICE_PINGONE_PLATFORM] = true - case ENUM_SERVICE_PINGONE_SSO: - (*ms)[ENUM_SERVICE_PINGONE_SSO] = true - case ENUM_SERVICE_PINGONE_MFA: - (*ms)[ENUM_SERVICE_PINGONE_MFA] = true - case ENUM_SERVICE_PINGONE_PROTECT: - (*ms)[ENUM_SERVICE_PINGONE_PROTECT] = true - default: - return fmt.Errorf("unrecognized service '%s'. Must be one of: %s", service, strings.Join(MultiServiceValidValues(), ", ")) - } - } - return nil -} - -func (ms MultiService) ContainsPingOneService() bool { - if ms == nil { - return false - } - - return ms[ENUM_SERVICE_PINGONE_PLATFORM] || - ms[ENUM_SERVICE_PINGONE_SSO] || - ms[ENUM_SERVICE_PINGONE_MFA] || - ms[ENUM_SERVICE_PINGONE_PROTECT] -} - -func (ms MultiService) ContainsPingFederateService() bool { - if ms == nil { - return false - } - - return ms[ENUM_SERVICE_PINGFEDERATE] -} - -func (ms MultiService) Type() string { - return "string" -} - -func (ms MultiService) String() string { - if ms == nil { - return "" - } - - enabledExportServices := ms.GetServices() - - if len(enabledExportServices) == 0 { - return "" - } - - return strings.Join(enabledExportServices, ",") -} - -func MultiServiceValidValues() []string { - allServices := []string{ - ENUM_SERVICE_PINGFEDERATE, - ENUM_SERVICE_PINGONE_PLATFORM, - ENUM_SERVICE_PINGONE_SSO, - ENUM_SERVICE_PINGONE_MFA, - ENUM_SERVICE_PINGONE_PROTECT, - } - - slices.Sort(allServices) - - return allServices -} diff --git a/internal/customtypes/multi_service_test.go b/internal/customtypes/multi_service_test.go deleted file mode 100644 index 1079b71..0000000 --- a/internal/customtypes/multi_service_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package customtypes_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/customtypes" - "github.com/pingidentity/pingctl/internal/testing/testutils" -) - -// Test MultiService NewMultiService function -func Test_MultiService_NewMultiService(t *testing.T) { - multiService := customtypes.NewMultiService() - - if multiService == nil { - t.Fatalf("NewMultiService returned nil") - } - - if len(multiService.GetServices()) == 0 { - t.Fatalf("NewMultiService returned empty Services") - } -} - -// Test MultiService Set function -func Test_MultiService_Set(t *testing.T) { - multiService := customtypes.NewMultiService() - - service := customtypes.ENUM_SERVICE_PINGONE_MFA - err := multiService.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) - } - - services := multiService.GetServices() - if len(services) != 1 { - t.Errorf("GetServices returned: %v, expected: %v", services, service) - } - - if services[0] != service { - t.Errorf("GetServices returned: %v, expected: %v", services, service) - } -} - -// Test MultiService Set function with invalid value -func Test_MultiService_Set_InvalidValue(t *testing.T) { - multiService := customtypes.NewMultiService() - - invalidValue := "invalid" - expectedErrorPattern := `^unrecognized service '.*'. Must be one of: .*$` - err := multiService.Set(invalidValue) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test MultiService Set function with nil -func Test_MultiService_Set_Nil(t *testing.T) { - var multiService *customtypes.MultiService - - service := customtypes.ENUM_SERVICE_PINGONE_MFA - expectedErrorPattern := `^failed to set MultiService value: .* MultiService is nil$` - err := multiService.Set(service) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test MultiService ContainsPingOneService function -func Test_MultiService_ContainsPingOneService(t *testing.T) { - multiService := customtypes.NewMultiService() - - service := customtypes.ENUM_SERVICE_PINGONE_MFA - err := multiService.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) - } - - if !multiService.ContainsPingOneService() { - t.Errorf("ContainsPingOneService returned false, expected true") - } -} - -// Test MultiService ContainsPingFederateService function -func Test_MultiService_ContainsPingFederateService(t *testing.T) { - multiService := customtypes.NewMultiService() - - service := customtypes.ENUM_SERVICE_PINGFEDERATE - err := multiService.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) - } - - if !multiService.ContainsPingFederateService() { - t.Errorf("ContainsPingFederateService returned false, expected true") - } -} - -// Test MultiService String function -func Test_MultiService_String(t *testing.T) { - multiService := customtypes.NewMultiService() - - service := customtypes.ENUM_SERVICE_PINGONE_MFA - err := multiService.Set(service) - if err != nil { - t.Errorf("Set returned error: %v", err) - } - - expected := service - actual := multiService.String() - if actual != expected { - t.Errorf("String returned: %s, expected: %s", actual, expected) - } -} diff --git a/internal/customtypes/output_format.go b/internal/customtypes/output_format.go index 2af81a0..66ef2c4 100644 --- a/internal/customtypes/output_format.go +++ b/internal/customtypes/output_format.go @@ -25,9 +25,11 @@ func (o *OutputFormat) Set(outputFormat string) error { return fmt.Errorf("failed to set Output Format value: %s. Output Format is nil", outputFormat) } - switch outputFormat { - case ENUM_OUTPUT_FORMAT_TEXT, ENUM_OUTPUT_FORMAT_JSON: - *o = OutputFormat(outputFormat) + switch { + case strings.EqualFold(outputFormat, ENUM_OUTPUT_FORMAT_TEXT): + *o = OutputFormat(ENUM_OUTPUT_FORMAT_TEXT) + case strings.EqualFold(outputFormat, ENUM_OUTPUT_FORMAT_JSON): + *o = OutputFormat(ENUM_OUTPUT_FORMAT_JSON) default: return fmt.Errorf("unrecognized Output Format: '%s'. Must be one of: %s", outputFormat, strings.Join(OutputFormatValidValues(), ", ")) } diff --git a/internal/customtypes/pingfederate_auth_type.go b/internal/customtypes/pingfederate_auth_type.go new file mode 100644 index 0000000..115169e --- /dev/null +++ b/internal/customtypes/pingfederate_auth_type.go @@ -0,0 +1,59 @@ +package customtypes + +import ( + "fmt" + "slices" + "strings" + + "github.com/spf13/pflag" +) + +const ( + ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC string = "basicAuth" + ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN string = "accessTokenAuth" + ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS string = "clientCredentialsAuth" +) + +type PingfederateAuthenticationType string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*PingfederateAuthenticationType)(nil) + +// Implement pflag.Value interface for custom type in cobra MultiService parameter +func (pat *PingfederateAuthenticationType) Set(authType string) error { + if pat == nil { + return fmt.Errorf("failed to set Pingfederate Authentication Type value: %s. Pingfederate Authentication Type is nil", authType) + } + + switch { + case strings.EqualFold(authType, ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC): + *pat = PingfederateAuthenticationType(ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC) + case strings.EqualFold(authType, ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN): + *pat = PingfederateAuthenticationType(ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN) + case strings.EqualFold(authType, ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS): + *pat = PingfederateAuthenticationType(ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS) + default: + return fmt.Errorf("unrecognized Pingfederate Authentication Type: '%s'. Must be one of: %s", authType, strings.Join(PingfederateAuthenticationTypeValidValues(), ", ")) + } + return nil +} + +func (pat PingfederateAuthenticationType) Type() string { + return "string" +} + +func (pat PingfederateAuthenticationType) String() string { + return string(pat) +} + +func PingfederateAuthenticationTypeValidValues() []string { + types := []string{ + ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC, + ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_ACCESS_TOKEN, + ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_CLIENT_CREDENTIALS, + } + + slices.Sort(types) + + return types +} diff --git a/internal/customtypes/pingfederate_auth_type_test.go b/internal/customtypes/pingfederate_auth_type_test.go new file mode 100644 index 0000000..c6db803 --- /dev/null +++ b/internal/customtypes/pingfederate_auth_type_test.go @@ -0,0 +1,47 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test PingfederateAuthType Set function +func Test_PingfederateAuthType_Set(t *testing.T) { + // Create a new PingfederateAuthType + pingAuthType := new(customtypes.PingfederateAuthenticationType) + + err := pingAuthType.Set(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC) + testutils.CheckExpectedError(t, err, nil) +} + +// Test Set function fails with invalid value +func Test_PingfederateAuthType_Set_InvalidValue(t *testing.T) { + pingAuthType := new(customtypes.PingfederateAuthenticationType) + + invalidValue := "invalid" + expectedErrorPattern := `^unrecognized Pingfederate Authentication Type: '.*'\. Must be one of: .*$` + err := pingAuthType.Set(invalidValue) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Set function fails with nil +func Test_PingfederateAuthType_Set_Nil(t *testing.T) { + var pingAuthType *customtypes.PingfederateAuthenticationType + + expectedErrorPattern := `^failed to set Pingfederate Authentication Type value: .*\. Pingfederate Authentication Type is nil$` + err := pingAuthType.Set(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test String function +func Test_PingfederateAuthType_String(t *testing.T) { + pingAuthType := customtypes.PingfederateAuthenticationType(customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC) + + expected := customtypes.ENUM_PINGFEDERATE_AUTHENTICATION_TYPE_BASIC + actual := pingAuthType.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} diff --git a/internal/customtypes/pingone_auth_type.go b/internal/customtypes/pingone_auth_type.go new file mode 100644 index 0000000..3628ed7 --- /dev/null +++ b/internal/customtypes/pingone_auth_type.go @@ -0,0 +1,51 @@ +package customtypes + +import ( + "fmt" + "slices" + "strings" + + "github.com/spf13/pflag" +) + +const ( + ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER string = "worker" +) + +type PingoneAuthenticationType string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*PingoneAuthenticationType)(nil) + +// Implement pflag.Value interface for custom type in cobra MultiService parameter +func (pat *PingoneAuthenticationType) Set(authType string) error { + if pat == nil { + return fmt.Errorf("failed to set Pingone Authentication Type value: %s. Pingone Authentication Type is nil", authType) + } + + switch { + case strings.EqualFold(authType, ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER): + *pat = PingoneAuthenticationType(ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER) + default: + return fmt.Errorf("unrecognized Pingone Authentication Type: '%s'. Must be one of: %s", authType, strings.Join(PingoneAuthenticationTypeValidValues(), ", ")) + } + return nil +} + +func (pat PingoneAuthenticationType) Type() string { + return "string" +} + +func (pat PingoneAuthenticationType) String() string { + return string(pat) +} + +func PingoneAuthenticationTypeValidValues() []string { + types := []string{ + ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER, + } + + slices.Sort(types) + + return types +} diff --git a/internal/customtypes/pingone_auth_type_test.go b/internal/customtypes/pingone_auth_type_test.go new file mode 100644 index 0000000..3aba300 --- /dev/null +++ b/internal/customtypes/pingone_auth_type_test.go @@ -0,0 +1,47 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test Pingone Authentication Type Set function +func Test_PingoneAuthType_Set(t *testing.T) { + // Create a new PingoneAuthType + pingAuthType := new(customtypes.PingoneAuthenticationType) + + err := pingAuthType.Set(customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER) + testutils.CheckExpectedError(t, err, nil) +} + +// Test Set function fails with invalid value +func Test_PingoneAuthType_Set_InvalidValue(t *testing.T) { + pingAuthType := new(customtypes.PingoneAuthenticationType) + + invalidValue := "invalid" + expectedErrorPattern := `^unrecognized Pingone Authentication Type: '.*'\. Must be one of: .*$` + err := pingAuthType.Set(invalidValue) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Set function fails with nil +func Test_PingoneAuthType_Set_Nil(t *testing.T) { + var pingAuthType *customtypes.PingoneAuthenticationType + + expectedErrorPattern := `^failed to set Pingone Authentication Type value: .*\. Pingone Authentication Type is nil$` + err := pingAuthType.Set(customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test String function +func Test_PingoneAuthType_String(t *testing.T) { + pingAuthType := customtypes.PingoneAuthenticationType(customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER) + + expected := customtypes.ENUM_PINGONE_AUTHENTICATION_TYPE_WORKER + actual := pingAuthType.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} diff --git a/internal/customtypes/pingone_region.go b/internal/customtypes/pingone_region.go deleted file mode 100644 index c63d4a9..0000000 --- a/internal/customtypes/pingone_region.go +++ /dev/null @@ -1,56 +0,0 @@ -package customtypes - -import ( - "fmt" - "slices" - "strings" - - "github.com/spf13/pflag" -) - -const ( - ENUM_PINGONE_REGION_AP string = "AsiaPacific" - ENUM_PINGONE_REGION_CA string = "Canada" - ENUM_PINGONE_REGION_EU string = "Europe" - ENUM_PINGONE_REGION_NA string = "NorthAmerica" -) - -type PingOneRegion string - -// Verify that the custom type satisfies the pflag.Value interface -var _ pflag.Value = (*PingOneRegion)(nil) - -// Implement pflag.Value interface for custom type in cobra pingone-region parameter - -func (p *PingOneRegion) Set(region string) error { - if p == nil { - return fmt.Errorf("failed to set PingOne Region value: %s. PingOne Region is nil", region) - } - switch region { - case ENUM_PINGONE_REGION_AP, ENUM_PINGONE_REGION_CA, ENUM_PINGONE_REGION_EU, ENUM_PINGONE_REGION_NA: - *p = PingOneRegion(region) - default: - return fmt.Errorf("unrecognized PingOne Region: '%s'. Must be one of: %s", region, strings.Join(PingOneRegionValidValues(), ", ")) - } - return nil -} - -func (p PingOneRegion) Type() string { - return "string" -} - -func (p PingOneRegion) String() string { - return string(p) -} - -func PingOneRegionValidValues() []string { - pingoneRegions := []string{ - ENUM_PINGONE_REGION_AP, - ENUM_PINGONE_REGION_CA, - ENUM_PINGONE_REGION_EU, - ENUM_PINGONE_REGION_NA} - - slices.Sort(pingoneRegions) - - return pingoneRegions -} diff --git a/internal/customtypes/pingone_region_code.go b/internal/customtypes/pingone_region_code.go new file mode 100644 index 0000000..24c99e9 --- /dev/null +++ b/internal/customtypes/pingone_region_code.go @@ -0,0 +1,73 @@ +package customtypes + +import ( + "fmt" + "slices" + "strings" + + "github.com/spf13/pflag" +) + +const ( + ENUM_PINGONE_REGION_CODE_AP string = "AP" + ENUM_PINGONE_REGION_CODE_AU string = "AU" + ENUM_PINGONE_REGION_CODE_CA string = "CA" + ENUM_PINGONE_REGION_CODE_EU string = "EU" + ENUM_PINGONE_REGION_CODE_NA string = "NA" + + ENUM_PINGONE_TLD_AP string = "asia" + ENUM_PINGONE_TLD_AU string = "com.au" + ENUM_PINGONE_TLD_CA string = "ca" + ENUM_PINGONE_TLD_EU string = "eu" + ENUM_PINGONE_TLD_NA string = "com" +) + +type PingoneRegionCode string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*PingoneRegionCode)(nil) + +// Implement pflag.Value interface for custom type in cobra pingone-region parameter + +func (prc *PingoneRegionCode) Set(regionCode string) error { + if prc == nil { + return fmt.Errorf("failed to set Pingone Region Code value: %s. Pingone Region Code is nil", regionCode) + } + switch { + case strings.EqualFold(regionCode, ENUM_PINGONE_REGION_CODE_AP): + *prc = PingoneRegionCode(ENUM_PINGONE_REGION_CODE_AP) + case strings.EqualFold(regionCode, ENUM_PINGONE_REGION_CODE_AU): + *prc = PingoneRegionCode(ENUM_PINGONE_REGION_CODE_AU) + case strings.EqualFold(regionCode, ENUM_PINGONE_REGION_CODE_CA): + *prc = PingoneRegionCode(ENUM_PINGONE_REGION_CODE_CA) + case strings.EqualFold(regionCode, ENUM_PINGONE_REGION_CODE_EU): + *prc = PingoneRegionCode(ENUM_PINGONE_REGION_CODE_EU) + case strings.EqualFold(regionCode, ENUM_PINGONE_REGION_CODE_NA): + *prc = PingoneRegionCode(ENUM_PINGONE_REGION_CODE_NA) + default: + return fmt.Errorf("unrecognized Pingone Region Code: '%s'. Must be one of: %s", regionCode, strings.Join(PingoneRegionCodeValidValues(), ", ")) + } + return nil +} + +func (prc PingoneRegionCode) Type() string { + return "string" +} + +func (prc PingoneRegionCode) String() string { + return string(prc) +} + +func PingoneRegionCodeValidValues() []string { + pingoneRegionCodes := []string{ + ENUM_PINGONE_REGION_CODE_AP, + ENUM_PINGONE_REGION_CODE_AU, + ENUM_PINGONE_REGION_CODE_CA, + ENUM_PINGONE_REGION_CODE_EU, + ENUM_PINGONE_REGION_CODE_NA, + } + + slices.Sort(pingoneRegionCodes) + + return pingoneRegionCodes +} diff --git a/internal/customtypes/pingone_region_test.go b/internal/customtypes/pingone_region_code_test.go similarity index 58% rename from internal/customtypes/pingone_region_test.go rename to internal/customtypes/pingone_region_code_test.go index bf0e77e..3883ec8 100644 --- a/internal/customtypes/pingone_region_test.go +++ b/internal/customtypes/pingone_region_code_test.go @@ -9,9 +9,9 @@ import ( // Test PingOneRegion Set function func Test_PingOneRegion_Set(t *testing.T) { - pingoneRegion := new(customtypes.PingOneRegion) + prc := new(customtypes.PingoneRegionCode) - err := pingoneRegion.Set(customtypes.ENUM_PINGONE_REGION_CA) + err := prc.Set(customtypes.ENUM_PINGONE_REGION_CODE_AP) if err != nil { t.Errorf("Set returned error: %v", err) } @@ -19,31 +19,31 @@ func Test_PingOneRegion_Set(t *testing.T) { // Test Set function fails with invalid value func Test_PingOneRegion_Set_InvalidValue(t *testing.T) { - pingoneRegion := new(customtypes.PingOneRegion) + prc := new(customtypes.PingoneRegionCode) invalidValue := "invalid" - expectedErrorPattern := `^unrecognized PingOne Region: '.*'\. Must be one of: .*$` - err := pingoneRegion.Set(invalidValue) + expectedErrorPattern := `^unrecognized Pingone Region Code: '.*'\. Must be one of: .*$` + err := prc.Set(invalidValue) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Set function fails with nil func Test_PingOneRegion_Set_Nil(t *testing.T) { - var pingoneRegion *customtypes.PingOneRegion + var prc *customtypes.PingoneRegionCode - val := customtypes.ENUM_PINGONE_REGION_CA + val := customtypes.ENUM_PINGONE_REGION_CODE_AP - expectedErrorPattern := `^failed to set PingOne Region value: .* PingOne Region is nil$` - err := pingoneRegion.Set(val) + expectedErrorPattern := `^failed to set Pingone Region Code value: .* Pingone Region Code is nil$` + err := prc.Set(val) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test String function func Test_PingOneRegion_String(t *testing.T) { - pingoneRegion := customtypes.PingOneRegion(customtypes.ENUM_PINGONE_REGION_CA) + pingoneRegion := customtypes.PingoneRegionCode(customtypes.ENUM_PINGONE_REGION_CODE_CA) - expected := customtypes.ENUM_PINGONE_REGION_CA + expected := customtypes.ENUM_PINGONE_REGION_CODE_CA actual := pingoneRegion.String() if actual != expected { t.Errorf("String returned: %s, expected: %s", actual, expected) diff --git a/internal/customtypes/request_services.go b/internal/customtypes/request_services.go new file mode 100644 index 0000000..27ce1ce --- /dev/null +++ b/internal/customtypes/request_services.go @@ -0,0 +1,51 @@ +package customtypes + +import ( + "fmt" + "slices" + "strings" + + "github.com/spf13/pflag" +) + +const ( + ENUM_REQUEST_SERVICE_PINGONE string = "pingone" +) + +type RequestService string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*RequestService)(nil) + +// Implement pflag.Value interface for custom type in cobra MultiService parameter +func (rs *RequestService) Set(service string) error { + if rs == nil { + return fmt.Errorf("failed to set RequestService value: %s. RequestService is nil", service) + } + + switch { + case strings.EqualFold(service, ENUM_REQUEST_SERVICE_PINGONE): + *rs = RequestService(ENUM_REQUEST_SERVICE_PINGONE) + default: + return fmt.Errorf("unrecognized Request Service: '%s'. Must be one of: %s", service, strings.Join(RequestServiceValidValues(), ", ")) + } + return nil +} + +func (rs RequestService) Type() string { + return "string" +} + +func (rs RequestService) String() string { + return string(rs) +} + +func RequestServiceValidValues() []string { + allServices := []string{ + ENUM_REQUEST_SERVICE_PINGONE, + } + + slices.Sort(allServices) + + return allServices +} diff --git a/internal/customtypes/request_services_test.go b/internal/customtypes/request_services_test.go new file mode 100644 index 0000000..dfe0d1f --- /dev/null +++ b/internal/customtypes/request_services_test.go @@ -0,0 +1,47 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test Request Services Set function +func Test_RequestServices_Set(t *testing.T) { + rs := new(customtypes.RequestService) + + service := customtypes.ENUM_REQUEST_SERVICE_PINGONE + err := rs.Set(service) + testutils.CheckExpectedError(t, err, nil) +} + +// Test Set function fails with invalid value +func Test_RequestServices_Set_InvalidValue(t *testing.T) { + rs := new(customtypes.RequestService) + + invalidValue := "invalid" + expectedErrorPattern := `^unrecognized Request Service: '.*'\. Must be one of: .*$` + err := rs.Set(invalidValue) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Set function fails with nil +func Test_RequestServices_Set_Nil(t *testing.T) { + var rs *customtypes.RequestService + + expectedErrorPattern := `^failed to set RequestService value: .*\. RequestService is nil$` + err := rs.Set(customtypes.ENUM_REQUEST_SERVICE_PINGONE) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test String function +func Test_RequestServices_String(t *testing.T) { + rs := customtypes.RequestService(customtypes.ENUM_REQUEST_SERVICE_PINGONE) + + expected := customtypes.ENUM_REQUEST_SERVICE_PINGONE + actual := rs.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} diff --git a/internal/output/output.go b/internal/output/output.go index c6b24db..080e112 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -107,8 +107,8 @@ func printText(opts Opts) { if opts.Fields != nil { fmt.Println(cyan("Additional Information:")) for k, v := range opts.Fields { - fmt.Println(cyan("%s: %s", k, v)) - l.Info().Msgf("%s: %s", k, v) + fmt.Println(cyan("%s: %v", k, v)) + l.Info().Msgf("%s: %v", k, v) } } diff --git a/internal/profiles/validate.go b/internal/profiles/validate.go index 1dbf854..d63885e 100644 --- a/internal/profiles/validate.go +++ b/internal/profiles/validate.go @@ -89,7 +89,7 @@ func validateProfileValues(pName string, profileViper *viper.Viper) (err error) case *customtypes.Bool: continue case string: - b := customtypes.Bool(false) + b := new(customtypes.Bool) if err = b.Set(typedValue); err != nil { return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a boolean value: %v", pName, typedValue, key, err) } @@ -103,7 +103,7 @@ func validateProfileValues(pName string, profileViper *viper.Viper) (err error) case *customtypes.UUID: continue case string: - u := customtypes.UUID("") + u := new(customtypes.UUID) if err = u.Set(typedValue); err != nil { return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a UUID value: %v", pName, typedValue, key, err) } @@ -115,31 +115,31 @@ func validateProfileValues(pName string, profileViper *viper.Viper) (err error) case *customtypes.OutputFormat: continue case string: - o := customtypes.OutputFormat("") + o := new(customtypes.OutputFormat) if err = o.Set(typedValue); err != nil { return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an output format value: %v", pName, typedValue, key, err) } default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an output format value", pName, typedValue, key) } - case options.ENUM_PINGONE_REGION: + case options.ENUM_PINGONE_REGION_CODE: switch typedValue := vValue.(type) { - case *customtypes.PingOneRegion: + case *customtypes.PingoneRegionCode: continue case string: - p := customtypes.PingOneRegion("") - if err = p.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a PingOne region value: %v", pName, typedValue, key, err) + prc := new(customtypes.PingoneRegionCode) + if err = prc.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a Pingone Region Code value: %v", pName, typedValue, key, err) } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingOne region value", pName, typedValue, key) + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a Pingone Region Code value", pName, typedValue, key) } case options.ENUM_STRING: switch typedValue := vValue.(type) { case *customtypes.String: continue case string: - s := customtypes.String("") + s := new(customtypes.String) if err = s.Set(typedValue); err != nil { return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a string value: %v", pName, typedValue, key, err) } @@ -151,37 +151,126 @@ func validateProfileValues(pName string, profileViper *viper.Viper) (err error) case *customtypes.StringSlice: continue case string: - ss := customtypes.StringSlice([]string{}) + ss := new(customtypes.StringSlice) if err = ss.Set(typedValue); err != nil { return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a string slice value: %v", pName, typedValue, key, err) } + case []any: + ss := new(customtypes.StringSlice) + for _, v := range typedValue { + switch innerTypedValue := v.(type) { + case string: + if err = ss.Set(innerTypedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a string slice value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string slice value", pName, typedValue, key) + } + } default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string slice value", pName, typedValue, key) } - case options.ENUM_MULTI_SERVICE: + case options.ENUM_EXPORT_SERVICES: switch typedValue := vValue.(type) { - case *customtypes.MultiService: + case *customtypes.ExportServices: continue case string: - ms := customtypes.NewMultiService() - if err = ms.Set(typedValue); err != nil { - return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a multi-service value: %v", pName, typedValue, key, err) + es := new(customtypes.ExportServices) + if err = es.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a export service value: %v", pName, typedValue, key, err) + } + case []any: + es := new(customtypes.ExportServices) + for _, v := range typedValue { + switch innerTypedValue := v.(type) { + case string: + if err = es.Set(innerTypedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a export service value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a export service value", pName, typedValue, key) + } + } default: - return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a multi-service value", pName, typedValue, key) + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a export service value", pName, typedValue, key) } case options.ENUM_EXPORT_FORMAT: switch typedValue := vValue.(type) { case *customtypes.ExportFormat: continue case string: - ef := customtypes.ExportFormat("") + ef := new(customtypes.ExportFormat) if err = ef.Set(typedValue); err != nil { return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an export format value: %v", pName, typedValue, key, err) } default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an export format value", pName, typedValue, key) } + case options.ENUM_REQUEST_HTTP_METHOD: + switch typedValue := vValue.(type) { + case *customtypes.HTTPMethod: + continue + case string: + hm := new(customtypes.HTTPMethod) + if err = hm.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an HTTP method value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an HTTP method value", pName, typedValue, key) + } + case options.ENUM_REQUEST_SERVICE: + switch typedValue := vValue.(type) { + case *customtypes.RequestService: + continue + case string: + rs := new(customtypes.RequestService) + if err = rs.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a request service value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a request service value", pName, typedValue, key) + } + case options.ENUM_INT: + switch typedValue := vValue.(type) { + case *customtypes.Int: + continue + case int: + continue + case int64: + continue + case string: + i := new(customtypes.Int) + if err = i.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an int value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an int value", pName, typedValue, key) + } + case options.ENUM_PINGFEDERATE_AUTH_TYPE: + switch typedValue := vValue.(type) { + case *customtypes.PingfederateAuthenticationType: + continue + case string: + pfa := new(customtypes.PingfederateAuthenticationType) + if err = pfa.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a PingFederate Authentication Type value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingFederate Authentication Type value", pName, typedValue, key) + } + case options.ENUM_PINGONE_AUTH_TYPE: + switch typedValue := vValue.(type) { + case *customtypes.PingoneAuthenticationType: + continue + case string: + pat := new(customtypes.PingoneAuthenticationType) + if err = pat.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a PingOne Authentication Type value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingOne Authentication Type value", pName, typedValue, key) + } default: return fmt.Errorf("profile '%s': variable type '%s' for key '%s' is not recognized", pName, opt.Type, key) } diff --git a/internal/profiles/viper.go b/internal/profiles/viper.go index 2dbb81f..aa3b6c9 100644 --- a/internal/profiles/viper.go +++ b/internal/profiles/viper.go @@ -342,11 +342,21 @@ func GetOptionValue(opt options.Option) (pFlagValue string, err error) { } } - if vValue != nil { - pFlagValue = fmt.Sprintf("%v", vValue) - if pFlagValue != "" { - return pFlagValue, nil + switch typedValue := vValue.(type) { + case nil: + // Do nothing + case string: + return typedValue, nil + case []string: + return strings.Join(typedValue, ","), nil + case []any: + strSlice := []string{} + for _, v := range typedValue { + strSlice = append(strSlice, fmt.Sprintf("%v", v)) } + return strings.Join(strSlice, ","), nil + default: + return fmt.Sprintf("%v", typedValue), nil } } @@ -355,5 +365,5 @@ func GetOptionValue(opt options.Option) (pFlagValue string, err error) { return pFlagValue, nil } - return pFlagValue, fmt.Errorf("failed to initialize %s option: %s. no value found", opt.Type, opt.CobraParamName) + return pFlagValue, fmt.Errorf("failed to get option value: no value found: %v", opt) } diff --git a/internal/testing/testutils/utils.go b/internal/testing/testutils/utils.go index 6bd77b1..f43f910 100644 --- a/internal/testing/testutils/utils.go +++ b/internal/testing/testutils/utils.go @@ -9,6 +9,7 @@ import ( "sync" "testing" + "github.com/patrickcping/pingone-go-sdk-v2/management" "github.com/patrickcping/pingone-go-sdk-v2/pingone" "github.com/pingidentity/pingctl/internal/configuration" "github.com/pingidentity/pingctl/internal/configuration/options" @@ -25,7 +26,7 @@ var ( func GetEnvironmentID() string { envIdOnce.Do(func() { - environmentId = os.Getenv(options.PlatformExportPingoneWorkerEnvironmentIDOption.EnvVar) + environmentId = os.Getenv(options.PlatformExportPingoneEnvironmentIDOption.EnvVar) }) return environmentId @@ -39,20 +40,21 @@ func GetPingOneClientInfo(t *testing.T) *connector.PingOneClientInfo { configuration.InitAllOptions() // Grab environment vars for initializing the API client. // These are set in GitHub Actions. - clientID := os.Getenv(options.PlatformExportPingoneWorkerClientIDOption.EnvVar) - clientSecret := os.Getenv(options.PlatformExportPingoneWorkerClientSecretOption.EnvVar) + clientID := os.Getenv(options.PingoneAuthenticationWorkerClientIDOption.EnvVar) + clientSecret := os.Getenv(options.PingoneAuthenticationWorkerClientSecretOption.EnvVar) environmentId := GetEnvironmentID() - region := os.Getenv(options.PlatformExportPingoneRegionOption.EnvVar) + regionCode := os.Getenv(options.PingoneRegionCodeOption.EnvVar) + sdkRegionCode := management.EnumRegionCode(regionCode) - if clientID == "" || clientSecret == "" || environmentId == "" || region == "" { - t.Fatalf("Unable to retrieve env var value for one or more of clientID, clientSecret, environmentID, region.") + if clientID == "" || clientSecret == "" || environmentId == "" || regionCode == "" { + t.Fatalf("Unable to retrieve env var value for one or more of clientID, clientSecret, environmentID, regionCode.") } apiConfig := &pingone.Config{ ClientID: &clientID, ClientSecret: &clientSecret, EnvironmentID: &environmentId, - Region: region, + RegionCode: &sdkRegionCode, } // Make empty context for testing @@ -80,10 +82,10 @@ func GetPingFederateClientInfo(t *testing.T) *connector.PingFederateClientInfo { configuration.InitAllOptions() - httpsHost := os.Getenv(options.PlatformExportPingfederateHTTPSHostOption.EnvVar) - adminApiPath := os.Getenv(options.PlatformExportPingfederateAdminAPIPathOption.EnvVar) - pfUsername := os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar) - pfPassword := os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar) + httpsHost := os.Getenv(options.PingfederateHTTPSHostOption.EnvVar) + adminApiPath := os.Getenv(options.PingfederateAdminAPIPathOption.EnvVar) + pfUsername := os.Getenv(options.PingfederateBasicAuthUsernameOption.EnvVar) + pfPassword := os.Getenv(options.PingfederateBasicAuthPasswordOption.EnvVar) if httpsHost == "" || adminApiPath == "" || pfUsername == "" || pfPassword == "" { t.Fatalf("Unable to retrieve env var value for one or more of httpsHost, adminApiPath, pfUsername, pfPassword.") diff --git a/internal/testing/testutils_terraform/terraform_utils.go b/internal/testing/testutils_terraform/terraform_utils.go index 18aaf0c..dae0a5d 100644 --- a/internal/testing/testutils_terraform/terraform_utils.go +++ b/internal/testing/testutils_terraform/terraform_utils.go @@ -13,6 +13,7 @@ import ( "github.com/pingidentity/pingctl/internal/connector" "github.com/pingidentity/pingctl/internal/connector/common" + "github.com/pingidentity/pingctl/internal/customtypes" ) var ( @@ -76,7 +77,7 @@ func singleResourceTerraformPlanGenerateConfigOut(t *testing.T, resource connect } // Export the resource - if err := common.WriteFiles([]connector.ExportableResource{resource}, connector.ENUMEXPORTFORMAT_HCL, exportDir, true); err != nil { + if err := common.WriteFiles([]connector.ExportableResource{resource}, customtypes.ENUM_EXPORT_FORMAT_HCL, exportDir, true); err != nil { t.Fatalf("Failed to export application resource: %v", err) } diff --git a/internal/testing/testutils_viper/viper_utils.go b/internal/testing/testutils_viper/viper_utils.go index 90c7618..9e81495 100644 --- a/internal/testing/testutils_viper/viper_utils.go +++ b/internal/testing/testutils_viper/viper_utils.go @@ -16,58 +16,50 @@ const ( ) var ( - configFileContents string - defaultConfigFileContents string = fmt.Sprintf(`activeProfile: default + configFileContents string + defaultConfigFileContentsPattern string = `activeProfile: default default: description: "default description" - pingctl: - color: true - outputFormat: text + color: true + outputFormat: text export: outputDirectory: %s + service: pingone: - region: %s - worker: - clientid: %s - clientsecret: %s - environmentid: %s + regionCode: %s + authentication: + type: worker + worker: + clientid: %s + clientsecret: %s + environmentid: %s pingfederate: - adminapipath: "%s" - clientcredentialsauth: - clientid: "%s" - clientsecret: "%s" - scopes: "%s" - tokenurl: "%s" - httpshost: "%s" + adminapipath: %s + authentication: + type: clientcredentialsauth + clientcredentialsauth: + clientid: %s + clientsecret: %s + scopes: %s + tokenurl: %s + httpshost: %s insecureTrustAllTLS: true xBypassExternalValidationHeader: true production: description: "test profile description" - pingctl: - color: true - outputFormat: text - export: + color: true + outputFormat: text + service: pingfederate: insecureTrustAllTLS: false - xBypassExternalValidationHeader: false`, - outputDirectoryReplacement, - os.Getenv(options.PlatformExportPingoneRegionOption.EnvVar), - os.Getenv(options.PlatformExportPingoneWorkerClientIDOption.EnvVar), - os.Getenv(options.PlatformExportPingoneWorkerClientSecretOption.EnvVar), - os.Getenv(options.PlatformExportPingoneWorkerEnvironmentIDOption.EnvVar), - os.Getenv(options.PlatformExportPingfederateAdminAPIPathOption.EnvVar), - os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), - os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), - os.Getenv(options.PlatformExportPingfederateScopesOption.EnvVar), - os.Getenv(options.PlatformExportPingfederateTokenURLOption.EnvVar), - os.Getenv(options.PlatformExportPingfederateHTTPSHostOption.EnvVar)) + xBypassExternalValidationHeader: false` ) func CreateConfigFile(t *testing.T) string { t.Helper() if configFileContents == "" { - configFileContents = strings.Replace(defaultConfigFileContents, outputDirectoryReplacement, t.TempDir(), 1) + configFileContents = strings.Replace(getDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir(), 1) } configFilepath := t.TempDir() + "/config.yaml" @@ -102,7 +94,7 @@ func InitVipers(t *testing.T) { configuration.InitAllOptions() - configFileContents = strings.Replace(defaultConfigFileContents, outputDirectoryReplacement, t.TempDir(), 1) + configFileContents = strings.Replace(getDefaultConfigFileContents(), outputDirectoryReplacement, t.TempDir(), 1) configureMainViper(t) } @@ -113,3 +105,18 @@ func InitVipersCustomFile(t *testing.T, fileContents string) { configFileContents = fileContents configureMainViper(t) } + +func getDefaultConfigFileContents() string { + return fmt.Sprintf(defaultConfigFileContentsPattern, + outputDirectoryReplacement, + os.Getenv(options.PingoneRegionCodeOption.EnvVar), + os.Getenv(options.PingoneAuthenticationWorkerClientIDOption.EnvVar), + os.Getenv(options.PingoneAuthenticationWorkerClientSecretOption.EnvVar), + os.Getenv(options.PingoneAuthenticationWorkerEnvironmentIDOption.EnvVar), + os.Getenv(options.PingfederateAdminAPIPathOption.EnvVar), + os.Getenv(options.PingfederateClientCredentialsAuthClientIDOption.EnvVar), + os.Getenv(options.PingfederateClientCredentialsAuthClientSecretOption.EnvVar), + os.Getenv(options.PingfederateClientCredentialsAuthScopesOption.EnvVar), + os.Getenv(options.PingfederateClientCredentialsAuthTokenURLOption.EnvVar), + os.Getenv(options.PingfederateHTTPSHostOption.EnvVar)) +}