From c7e58fcef8554bc0c41ca732d799c2eb8b791cec Mon Sep 17 00:00:00 2001 From: wesleymccollam Date: Tue, 13 May 2025 17:34:41 -0400 Subject: [PATCH 1/3] add platform export support for pingone_custom_role --- cmd/config/add_profile_test.go | 15 +++ .../platform/pingone_platform_connector.go | 1 + .../pingone_platform_connector_test.go | 5 + .../pingone/platform/resources/custom_role.go | 89 +++++++++++++++++ .../platform/resources/custom_role_test.go | 33 +++++++ internal/profiles/koanf.go | 40 +------- .../custom_role.go | 99 +++++++++++++++++++ 7 files changed, 246 insertions(+), 36 deletions(-) create mode 100644 internal/connector/pingone/platform/resources/custom_role.go create mode 100644 internal/connector/pingone/platform/resources/custom_role_test.go create mode 100644 internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go diff --git a/cmd/config/add_profile_test.go b/cmd/config/add_profile_test.go index 8280a73c..043d2b95 100644 --- a/cmd/config/add_profile_test.go +++ b/cmd/config/add_profile_test.go @@ -18,6 +18,21 @@ func TestConfigAddProfileCmd_Execute(t *testing.T) { testutils.CheckExpectedError(t, err, nil) } +// Test config add profile with multiple case-insensitive profile names +func TestConfigAddProfileCmd_CaseInsensitiveProfileNamesExecute(t *testing.T) { + err := testutils_cobra.ExecutePingcli(t, "config", "add-profile", + "--name", "same-profile", + "--description", "test description", + "--set-active=false") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingcli(t, "config", "add-profile", + "--name", "SAME-PROFILE", + "--description", "test description", + "--set-active=false") + testutils.CheckExpectedError(t, err, nil) +} + // Test config add profile command fails when provided too many arguments func TestConfigAddProfileCmd_TooManyArgs(t *testing.T) { expectedErrorPattern := `^failed to execute 'pingcli config add-profile': command accepts 0 arg\(s\), received 1$` diff --git a/internal/connector/pingone/platform/pingone_platform_connector.go b/internal/connector/pingone/platform/pingone_platform_connector.go index 0852c42b..f9ab1a78 100644 --- a/internal/connector/pingone/platform/pingone_platform_connector.go +++ b/internal/connector/pingone/platform/pingone_platform_connector.go @@ -55,6 +55,7 @@ func (c *PingOnePlatformConnector) Export(format, outputDir string, overwriteExp resources.BrandingThemeDefault(&c.clientInfo), resources.Certificate(&c.clientInfo), resources.CustomDomain(&c.clientInfo), + resources.CustomRole(&c.clientInfo), resources.Environment(&c.clientInfo), resources.Form(&c.clientInfo), resources.FormsRecaptchaV2(&c.clientInfo), diff --git a/internal/connector/pingone/platform/pingone_platform_connector_test.go b/internal/connector/pingone/platform/pingone_platform_connector_test.go index 29903b8e..00c2ce68 100644 --- a/internal/connector/pingone/platform/pingone_platform_connector_test.go +++ b/internal/connector/pingone/platform/pingone_platform_connector_test.go @@ -77,6 +77,11 @@ func TestPlatformTerraformPlan(t *testing.T) { testableResource: pingone_platform_testable_resources.CustomDomain(t, clientInfo), ignoredErrors: nil, }, + { + name: "CustomRole", + testableResource: pingone_platform_testable_resources.CustomRole(t, clientInfo), + ignoredErrors: nil, + }, { name: "Environment", testableResource: pingone_platform_testable_resources.Environment(t, clientInfo), diff --git a/internal/connector/pingone/platform/resources/custom_role.go b/internal/connector/pingone/platform/resources/custom_role.go new file mode 100644 index 00000000..cf2aaedd --- /dev/null +++ b/internal/connector/pingone/platform/resources/custom_role.go @@ -0,0 +1,89 @@ +// Copyright © 2025 Ping Identity Corporation +// Code generated by ping-cli-generator + +package resources + +import ( + "fmt" + + "github.com/patrickcping/pingone-go-sdk-v2/management" + "github.com/pingidentity/pingcli/internal/connector" + "github.com/pingidentity/pingcli/internal/connector/common" + "github.com/pingidentity/pingcli/internal/connector/pingone" + "github.com/pingidentity/pingcli/internal/logger" +) + +// Verify that the resource satisfies the exportable resource interface +var ( + _ connector.ExportableResource = &PingOneCustomRoleResource{} +) + +type PingOneCustomRoleResource struct { + clientInfo *connector.ClientInfo +} + +// Utility method for creating a PingOneCustomRoleResource +func CustomRole(clientInfo *connector.ClientInfo) *PingOneCustomRoleResource { + return &PingOneCustomRoleResource{ + clientInfo: clientInfo, + } +} + +func (r *PingOneCustomRoleResource) ResourceType() string { + return "pingone_custom_role" +} + +func (r *PingOneCustomRoleResource) ExportAll() (*[]connector.ImportBlock, error) { + l := logger.Get() + l.Debug().Msgf("Exporting all '%s' Resources...", r.ResourceType()) + + importBlocks := []connector.ImportBlock{} + + customRoleData, err := r.getCustomRoleData() + if err != nil { + return nil, err + } + + for customRoleId, customRoleName := range customRoleData { + commentData := map[string]string{ + "Custom Role ID": customRoleId, + "Custom Role Name": customRoleName, + "Export Environment ID": r.clientInfo.PingOneExportEnvironmentID, + "Resource Type": r.ResourceType(), + } + + importBlock := connector.ImportBlock{ + ResourceType: r.ResourceType(), + ResourceName: customRoleName, + ResourceID: fmt.Sprintf("%s/%s", r.clientInfo.PingOneExportEnvironmentID, customRoleId), + CommentInformation: common.GenerateCommentInformation(commentData), + } + + importBlocks = append(importBlocks, importBlock) + } + + return &importBlocks, nil +} + +func (r *PingOneCustomRoleResource) getCustomRoleData() (map[string]string, error) { + customRoleData := make(map[string]string) + + iter := r.clientInfo.PingOneApiClient.ManagementAPIClient.CustomAdminRolesApi.ReadAllCustomAdminRoles(r.clientInfo.PingOneContext, r.clientInfo.PingOneExportEnvironmentID).Execute() + apiObjs, err := pingone.GetManagementAPIObjectsFromIterator[management.EntityArrayEmbeddedRolesInner](iter, "ReadAllCustomAdminRoles", "GetRoles", r.ResourceType()) + if err != nil { + return nil, err + } + + for _, innerObj := range apiObjs { + if innerObj.CustomAdminRole != nil { + customRoleId, customRoleIdOk := innerObj.CustomAdminRole.GetIdOk() + customRoleName, customRoleNameOk := innerObj.CustomAdminRole.GetNameOk() + + if customRoleIdOk && customRoleNameOk { + customRoleData[*customRoleId] = *customRoleName + } + } + } + + return customRoleData, nil +} diff --git a/internal/connector/pingone/platform/resources/custom_role_test.go b/internal/connector/pingone/platform/resources/custom_role_test.go new file mode 100644 index 00000000..c2b75823 --- /dev/null +++ b/internal/connector/pingone/platform/resources/custom_role_test.go @@ -0,0 +1,33 @@ +// Copyright © 2025 Ping Identity Corporation +// Code generated by ping-cli-generator + +package resources_test + +import ( + "fmt" + "testing" + + "github.com/pingidentity/pingcli/internal/connector" + "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_resource" + "github.com/pingidentity/pingcli/internal/testing/testutils_resource/pingone_platform_testable_resources" +) + +func Test_CustomRole(t *testing.T) { + clientInfo := testutils.GetClientInfo(t) + + tr := pingone_platform_testable_resources.CustomRole(t, clientInfo) + + tr.CreateResource(t) + defer tr.DeleteResource(t) + + expectedImportBlocks := []connector.ImportBlock{ + { + ResourceType: tr.ExportableResource.ResourceType(), + ResourceName: tr.ResourceInfo.CreationInfo[testutils_resource.ENUM_NAME], + ResourceID: fmt.Sprintf("%s/%s", clientInfo.PingOneExportEnvironmentID, tr.ResourceInfo.CreationInfo[testutils_resource.ENUM_ID]), + }, + } + + testutils.ValidateImportBlocks(t, tr.ExportableResource, &expectedImportBlocks) +} diff --git a/internal/profiles/koanf.go b/internal/profiles/koanf.go index d39a3644..682c5fd7 100644 --- a/internal/profiles/koanf.go +++ b/internal/profiles/koanf.go @@ -69,11 +69,8 @@ func KoanfValueFromOption(opt options.Option, pName string) (value string, ok bo ) // Case 1: Koanf Key is the ActiveProfile Key, get value from main koanf instance - if opt.KoanfKey != "" && strings.EqualFold(opt.KoanfKey, options.RootActiveProfileOption.KoanfKey) && mainKoanfInstance != nil { - kValue = mainKoanfInstance.KoanfInstance().Get("activeprofile") - if kValue == nil { - kValue = mainKoanfInstance.KoanfInstance().Get(opt.KoanfKey) - } + if opt.KoanfKey != "" && opt.KoanfKey == options.RootActiveProfileOption.KoanfKey && mainKoanfInstance != nil { + kValue = mainKoanfInstance.KoanfInstance().Get(opt.KoanfKey) } else { // // Case 2: --profile flag has been set, get value from set profile koanf instance // // Case 3: no --profile flag set, get value from active profile koanf instance defined in main koanf instance @@ -129,7 +126,7 @@ func (k KoanfConfig) ProfileNames() (profileNames []string) { mainKoanfKeys := k.KoanfInstance().All() for key := range mainKoanfKeys { // Do not add Active profile koanf key to profileNames - if strings.EqualFold(key, options.RootActiveProfileOption.KoanfKey) { + if key == options.RootActiveProfileOption.KoanfKey { continue } @@ -233,35 +230,6 @@ func (k KoanfConfig) GetProfileKoanf(pName string) (subKoanf *koanf.Koanf, err e } func (k KoanfConfig) WriteFile() (err error) { - // TODO - Remove this for loop prior to v0.7.0 release - for _, profileName := range k.ProfileNames() { - for key, val := range k.KoanfInstance().All() { - if profileName == key || !strings.Contains(key, profileName) { - continue - } - for _, opt := range options.Options() { - fullKoanfKeyValue := fmt.Sprintf("%s.%s", profileName, opt.KoanfKey) - if fullKoanfKeyValue == key { - continue - } - if strings.ToLower(fullKoanfKeyValue) == key { - err = k.KoanfInstance().Set(fullKoanfKeyValue, val) - if err != nil { - return fmt.Errorf("error setting koanf key %s: %w", fullKoanfKeyValue, err) - } - k.KoanfInstance().Delete(key) - } - } - } - } - - // TODO - Remove this originalActiveProfileKey logic prior to v0.7.0 release - // Delete the original active profile key if it exists and the new activeProfile exists - originalActiveProfileKey := strings.ToLower(options.RootActiveProfileOption.KoanfKey) - if k.KoanfInstance().Exists(originalActiveProfileKey) && k.KoanfInstance().Exists(options.RootActiveProfileOption.KoanfKey) { - k.KoanfInstance().Delete(strings.ToLower(originalActiveProfileKey)) - } - encodedConfig, err := k.KoanfInstance().Marshal(yaml.Parser()) if err != nil { return fmt.Errorf("error marshalling koanf: %w", err) @@ -323,7 +291,7 @@ func (k KoanfConfig) DefaultMissingKoanfKeys() (err error) { } for _, opt := range options.Options() { - if opt.KoanfKey == "" || strings.EqualFold(opt.KoanfKey, options.RootActiveProfileOption.KoanfKey) { + if opt.KoanfKey == "" || opt.KoanfKey == options.RootActiveProfileOption.KoanfKey { continue } diff --git a/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go b/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go new file mode 100644 index 00000000..ac7ab80b --- /dev/null +++ b/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go @@ -0,0 +1,99 @@ +// Copyright © 2025 Ping Identity Corporation +// Code generated by ping-cli-generator + +package pingone_platform_testable_resources + +import ( + "testing" + + "github.com/patrickcping/pingone-go-sdk-v2/management" + "github.com/pingidentity/pingcli/internal/connector" + "github.com/pingidentity/pingcli/internal/connector/common" + "github.com/pingidentity/pingcli/internal/connector/pingone/platform/resources" + "github.com/pingidentity/pingcli/internal/testing/testutils_resource" +) + +func CustomRole(t *testing.T, clientInfo *connector.ClientInfo) *testutils_resource.TestableResource { + t.Helper() + + return &testutils_resource.TestableResource{ + ClientInfo: clientInfo, + CreateFunc: createCustomRole, + DeleteFunc: deleteCustomRole, + Dependencies: nil, + ExportableResource: resources.CustomRole(clientInfo), + } +} + +func createCustomRole(t *testing.T, clientInfo *connector.ClientInfo, resourceType string, strArgs ...string) testutils_resource.ResourceInfo { + t.Helper() + + if len(strArgs) != 0 { + t.Errorf("Unexpected number of arguments provided to createCustomRole(): %v", strArgs) + return testutils_resource.ResourceInfo{} + } + + request := clientInfo.PingOneApiClient.ManagementAPIClient.CustomAdminRolesApi.CreateCustomAdminRole(clientInfo.PingOneContext, clientInfo.PingOneExportEnvironmentID) + clientStruct := management.CustomAdminRole{ + Name: "Custom Role", + ApplicableTo: []management.EnumCustomAdminRoleApplicableTo{ + management.ENUMCUSTOMADMINROLEAPPLICABLETO_ENVIRONMENT, + management.ENUMCUSTOMADMINROLEAPPLICABLETO_POPULATION, + }, + CanBeAssignedBy: []management.CustomAdminRoleCanAssignInner{ + { + Id: "29ddce68-cd7f-4b2a-b6fc-f7a19553b496", + }, + }, + Permissions: []management.CustomAdminRolePermissionsInner{ + { + Id: "licensing:read:license", + }, + }, + } + + request = request.CustomAdminRole(clientStruct) + + resource, response, err := request.Execute() + ok, err := common.HandleClientResponse(response, err, "CreateCustomAdminRole", resourceType) + if err != nil { + t.Errorf("Failed to execute PingOne client function\nResponse Status: %s\nResponse Body: %s\nError: %v", response.Status, response.Body, err) + return testutils_resource.ResourceInfo{} + } + if !ok { + t.Errorf("Failed to execute PingOne client function\nResponse Status: %s\nResponse Body: %s", response.Status, response.Body) + return testutils_resource.ResourceInfo{} + } + + return testutils_resource.ResourceInfo{ + DeletionIds: []string{ + *resource.Id, + }, + CreationInfo: map[testutils_resource.ResourceCreationInfoType]string{ + testutils_resource.ENUM_ID: *resource.Id, + testutils_resource.ENUM_NAME: resource.Name, + }, + } +} + +func deleteCustomRole(t *testing.T, clientInfo *connector.ClientInfo, resourceType string, ids ...string) { + t.Helper() + + if len(ids) != 1 { + t.Errorf("Unexpected number of arguments provided to deleteCustomRole(): %v", ids) + return + } + + request := clientInfo.PingOneApiClient.ManagementAPIClient.CustomAdminRolesApi.DeleteCustomAdminRole(clientInfo.PingOneContext, clientInfo.PingOneExportEnvironmentID, ids[0]) + + response, err := request.Execute() + ok, err := common.HandleClientResponse(response, err, "DeleteCustomAdminRole", resourceType) + if err != nil { + t.Errorf("Failed to execute PingOne client function\nResponse Status: %s\nResponse Body: %s\nError: %v", response.Status, response.Body, err) + return + } + if !ok { + t.Errorf("Failed to execute PingOne client function\nResponse Status: %s\nResponse Body: %s", response.Status, response.Body) + return + } +} From 3a6cda61652fa3c59db3b4b7ab4c5f06f0f516db Mon Sep 17 00:00:00 2001 From: wesleymccollam Date: Tue, 13 May 2025 17:39:51 -0400 Subject: [PATCH 2/3] golangci-lint fixes --- .../pingone_platform_testable_resources/custom_role.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go b/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go index ac7ab80b..f8e30adf 100644 --- a/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go +++ b/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go @@ -30,6 +30,7 @@ func createCustomRole(t *testing.T, clientInfo *connector.ClientInfo, resourceTy if len(strArgs) != 0 { t.Errorf("Unexpected number of arguments provided to createCustomRole(): %v", strArgs) + return testutils_resource.ResourceInfo{} } @@ -58,10 +59,12 @@ func createCustomRole(t *testing.T, clientInfo *connector.ClientInfo, resourceTy ok, err := common.HandleClientResponse(response, err, "CreateCustomAdminRole", resourceType) if err != nil { t.Errorf("Failed to execute PingOne client function\nResponse Status: %s\nResponse Body: %s\nError: %v", response.Status, response.Body, err) + return testutils_resource.ResourceInfo{} } if !ok { t.Errorf("Failed to execute PingOne client function\nResponse Status: %s\nResponse Body: %s", response.Status, response.Body) + return testutils_resource.ResourceInfo{} } @@ -81,6 +84,7 @@ func deleteCustomRole(t *testing.T, clientInfo *connector.ClientInfo, resourceTy if len(ids) != 1 { t.Errorf("Unexpected number of arguments provided to deleteCustomRole(): %v", ids) + return } @@ -90,10 +94,12 @@ func deleteCustomRole(t *testing.T, clientInfo *connector.ClientInfo, resourceTy ok, err := common.HandleClientResponse(response, err, "DeleteCustomAdminRole", resourceType) if err != nil { t.Errorf("Failed to execute PingOne client function\nResponse Status: %s\nResponse Body: %s\nError: %v", response.Status, response.Body, err) + return } if !ok { t.Errorf("Failed to execute PingOne client function\nResponse Status: %s\nResponse Body: %s", response.Status, response.Body) + return } } From 3881e12d81622b95e9505519ee625b5a5df951f8 Mon Sep 17 00:00:00 2001 From: wesleymccollam Date: Thu, 15 May 2025 16:04:35 -0400 Subject: [PATCH 3/3] get role id from client --- .../custom_role.go | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go b/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go index f8e30adf..1ce56052 100644 --- a/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go +++ b/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_role.go @@ -9,6 +9,7 @@ import ( "github.com/patrickcping/pingone-go-sdk-v2/management" "github.com/pingidentity/pingcli/internal/connector" "github.com/pingidentity/pingcli/internal/connector/common" + "github.com/pingidentity/pingcli/internal/connector/pingone" "github.com/pingidentity/pingcli/internal/connector/pingone/platform/resources" "github.com/pingidentity/pingcli/internal/testing/testutils_resource" ) @@ -34,6 +35,31 @@ func createCustomRole(t *testing.T, clientInfo *connector.ClientInfo, resourceTy return testutils_resource.ResourceInfo{} } + iter := clientInfo.PingOneApiClient.ManagementAPIClient.RolesApi.ReadAllRoles(clientInfo.PingOneContext).Execute() + apiObjs, err := pingone.GetManagementAPIObjectsFromIterator[management.EntityArrayEmbeddedRolesInner](iter, "ReadAllRoles", "GetRoles", resourceType) + if err != nil { + t.Errorf("Failed to execute PingOne client function\nError: %v", err) + + return testutils_resource.ResourceInfo{} + } + if len(apiObjs) == 0 { + t.Fatal("Failed to execute PingOne client function\n No built-in roles returned from ReadAllRoles()") + } + + var ( + roleId string + ) + + for _, role := range apiObjs { + if role.Role != nil { + if role.Role.Name != nil && *role.Role.Name == management.ENUMROLENAME_APPLICATION_OWNER { + roleId = *role.Role.Id + + break + } + } + } + request := clientInfo.PingOneApiClient.ManagementAPIClient.CustomAdminRolesApi.CreateCustomAdminRole(clientInfo.PingOneContext, clientInfo.PingOneExportEnvironmentID) clientStruct := management.CustomAdminRole{ Name: "Custom Role", @@ -43,7 +69,7 @@ func createCustomRole(t *testing.T, clientInfo *connector.ClientInfo, resourceTy }, CanBeAssignedBy: []management.CustomAdminRoleCanAssignInner{ { - Id: "29ddce68-cd7f-4b2a-b6fc-f7a19553b496", + Id: roleId, }, }, Permissions: []management.CustomAdminRolePermissionsInner{