diff --git a/cmd/ocm/gcp/create-wif-config.go b/cmd/ocm/gcp/create-wif-config.go index b4bc2ee2..d125847d 100644 --- a/cmd/ocm/gcp/create-wif-config.go +++ b/cmd/ocm/gcp/create-wif-config.go @@ -6,6 +6,7 @@ import ( "log" "os" "path/filepath" + "reflect" "strings" "github.com/googleapis/gax-go/v2/apierror" @@ -17,7 +18,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/googleapi" "google.golang.org/api/iam/v1" iamv1 "google.golang.org/api/iam/v1" @@ -38,6 +38,7 @@ var ( const ( poolDescription = "Created by the OLM CLI" + roleDescription = "Created by the OLM CLI" openShiftAudience = "openshift" ) @@ -248,6 +249,39 @@ func createServiceAccounts(ctx context.Context, gcpClient gcp.GcpClient, wifOutp log.Printf("IAM service account %s created", serviceAccountID) } + // Create roles that aren't predefined + for _, serviceAccount := range wifOutput.Status.ServiceAccounts { + for _, role := range serviceAccount.Roles { + if role.Predefined { + continue + } + roleID := role.Id + roleName := role.Id + permissions := role.Permissions + existingRole, err := GetRole(gcpClient, roleID, projectId) + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && strings.Contains(gerr.Message, "Requested entity was not found") { + existingRole, err = CreateRole(gcpClient, permissions, roleName, roleID, roleDescription, projectId) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to create %s", roleName)) + } + log.Printf("Role %s created", roleID) + } else { + return errors.Wrap(err, "Failed to check if role exists") + } + } + // Update role if permissions have changed + if !reflect.DeepEqual(existingRole.IncludedPermissions, permissions) { + existingRole.IncludedPermissions = permissions + _, err := UpdateRole(gcpClient, existingRole, roleName) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to update %s", roleName)) + } + log.Printf("Role %s updated", roleID) + } + } + } + // Bind roles and grant access for _, serviceAccount := range wifOutput.Status.ServiceAccounts { serviceAccountID := serviceAccount.GetId() @@ -314,78 +348,3 @@ func generateServiceAccountID(serviceAccount models.ServiceAccount) string { } return serviceAccountID } - -// EnsurePolicyBindingsForProject ensures that given roles and member, appropriate binding is added to project -func EnsurePolicyBindingsForProject(gcpClient gcp.GcpClient, roles []string, member string, projectName string) error { - needPolicyUpdate := false - - policy, err := gcpClient.GetProjectIamPolicy(projectName, &cloudresourcemanager.GetIamPolicyRequest{}) - - if err != nil { - return fmt.Errorf("error fetching policy for project: %v", err) - } - - // Validate that each role exists, and add the policy binding as needed - for _, definedRole := range roles { - // Earlier we've verified that the requested roles already exist. - - // Add policy binding - modified := addPolicyBindingForProject(policy, definedRole, member) - if modified { - needPolicyUpdate = true - } - - } - - if needPolicyUpdate { - return setProjectIamPolicy(gcpClient, policy, projectName) - } - - // If we made it this far there were no updates needed - return nil -} - -func addPolicyBindingForProject(policy *cloudresourcemanager.Policy, roleName, memberName string) bool { - for i, binding := range policy.Bindings { - if binding.Role == roleName { - return addMemberToBindingForProject(memberName, policy.Bindings[i]) - } - } - - // if we didn't find an existing binding entry, then make one - createMemberRoleBindingForProject(policy, roleName, memberName) - - return true -} - -func createMemberRoleBindingForProject(policy *cloudresourcemanager.Policy, roleName, memberName string) { - policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{ - Members: []string{memberName}, - Role: roleName, - }) -} - -// adds member to existing binding. returns bool indicating if an entry was made -func addMemberToBindingForProject(memberName string, binding *cloudresourcemanager.Binding) bool { - for _, member := range binding.Members { - if member == memberName { - // already present - return false - } - } - - binding.Members = append(binding.Members, memberName) - return true -} - -func setProjectIamPolicy(gcpClient gcp.GcpClient, policy *cloudresourcemanager.Policy, projectName string) error { - policyRequest := &cloudresourcemanager.SetIamPolicyRequest{ - Policy: policy, - } - - _, err := gcpClient.SetProjectIamPolicy(projectName, policyRequest) - if err != nil { - return fmt.Errorf("error setting project policy: %v", err) - } - return nil -} diff --git a/cmd/ocm/gcp/delete-wif-config.go b/cmd/ocm/gcp/delete-wif-config.go index b6c91670..a736a3e3 100644 --- a/cmd/ocm/gcp/delete-wif-config.go +++ b/cmd/ocm/gcp/delete-wif-config.go @@ -35,6 +35,9 @@ func NewDeleteWorkloadIdentityConfiguration() *cobra.Command { PersistentPreRun: validationForDeleteWorkloadIdentityConfigurationCmd, } + deleteWorkloadIdentityPoolCmd.PersistentFlags().BoolVar(&DeleteWorkloadIdentityConfigurationOpts.DryRun, "dry-run", false, "Skip creating objects, and just save what would have been created into files") + deleteWorkloadIdentityPoolCmd.PersistentFlags().StringVar(&DeleteWorkloadIdentityConfigurationOpts.TargetDir, "output-dir", "", "Directory to place generated files (defaults to current directory)") + return deleteWorkloadIdentityPoolCmd } @@ -61,12 +64,23 @@ func deleteWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) { if err != nil { log.Fatalf("failed to create backend client: %v", err) } - gcpClient, err := gcp.NewGcpClient(context.Background()) + + wifConfig, err := ocmClient.GetWifConfig(wifConfigId) if err != nil { log.Fatal(err) } - wifConfig, err := ocmClient.GetWifConfig(wifConfigId) + if DeleteWorkloadIdentityConfigurationOpts.DryRun { + log.Printf("Writing script files to %s", DeleteWorkloadIdentityConfigurationOpts.TargetDir) + + err := createDeleteScript(DeleteWorkloadIdentityConfigurationOpts.TargetDir, &wifConfig) + if err != nil { + log.Fatalf("Failed to create script files: %s", err) + } + return + } + + gcpClient, err := gcp.NewGcpClient(context.Background()) if err != nil { log.Fatal(err) } diff --git a/cmd/ocm/gcp/generate-wif-script.go b/cmd/ocm/gcp/generate-wif-script.go index e057f865..98446fa3 100644 --- a/cmd/ocm/gcp/generate-wif-script.go +++ b/cmd/ocm/gcp/generate-wif-script.go @@ -56,6 +56,9 @@ func generateCreateScriptCmd(cmd *cobra.Command, argv []string) { log.Printf("Writing script files to %s", GenerateScriptOpts.TargetDir) if err := createScript(GenerateScriptOpts.TargetDir, &wifConfig); err != nil { - log.Fatalf("failed to generate script: %v", err) + log.Fatalf("failed to generate create script: %v", err) + } + if err := createDeleteScript(GenerateScriptOpts.TargetDir, &wifConfig); err != nil { + log.Fatalf("failed to generate delete script: %v", err) } } diff --git a/cmd/ocm/gcp/iam.go b/cmd/ocm/gcp/iam.go new file mode 100644 index 00000000..05970669 --- /dev/null +++ b/cmd/ocm/gcp/iam.go @@ -0,0 +1,144 @@ +package gcp + +import ( + "context" + "fmt" + "log" + + "cloud.google.com/go/iam/admin/apiv1/adminpb" + "github.com/openshift-online/ocm-cli/pkg/gcp" + + "google.golang.org/api/cloudresourcemanager/v1" +) + +// EnsurePolicyBindingsForProject ensures that given roles and member, appropriate binding is added to project +func EnsurePolicyBindingsForProject(gcpClient gcp.GcpClient, roles []string, member string, projectName string) error { + needPolicyUpdate := false + + policy, err := gcpClient.GetProjectIamPolicy(projectName, &cloudresourcemanager.GetIamPolicyRequest{}) + + if err != nil { + return fmt.Errorf("error fetching policy for project: %v", err) + } + + // Validate that each role exists, and add the policy binding as needed + for _, definedRole := range roles { + // Earlier we've verified that the requested roles already exist. + + // Add policy binding + modified := addPolicyBindingForProject(policy, definedRole, member) + if modified { + needPolicyUpdate = true + } + + } + + if needPolicyUpdate { + return setProjectIamPolicy(gcpClient, policy, projectName) + } + + // If we made it this far there were no updates needed + return nil +} + +func addPolicyBindingForProject(policy *cloudresourcemanager.Policy, roleName, memberName string) bool { + for i, binding := range policy.Bindings { + if binding.Role == roleName { + return addMemberToBindingForProject(memberName, policy.Bindings[i]) + } + } + + // if we didn't find an existing binding entry, then make one + createMemberRoleBindingForProject(policy, roleName, memberName) + + return true +} + +func createMemberRoleBindingForProject(policy *cloudresourcemanager.Policy, roleName, memberName string) { + policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{ + Members: []string{memberName}, + Role: roleName, + }) +} + +// adds member to existing binding. returns bool indicating if an entry was made +func addMemberToBindingForProject(memberName string, binding *cloudresourcemanager.Binding) bool { + for _, member := range binding.Members { + if member == memberName { + // already present + return false + } + } + + binding.Members = append(binding.Members, memberName) + return true +} + +func setProjectIamPolicy(gcpClient gcp.GcpClient, policy *cloudresourcemanager.Policy, projectName string) error { + policyRequest := &cloudresourcemanager.SetIamPolicyRequest{ + Policy: policy, + } + + _, err := gcpClient.SetProjectIamPolicy(projectName, policyRequest) + if err != nil { + return fmt.Errorf("error setting project policy: %v", err) + } + return nil +} + +/* Custom Role Creation */ + +// GetRole fetches the role created to satisfy a credentials request +func GetRole(gcpClient gcp.GcpClient, roleID, projectName string) (*adminpb.Role, error) { + log.Printf("role id %v", roleID) + role, err := gcpClient.GetRole(context.TODO(), &adminpb.GetRoleRequest{ + Name: fmt.Sprintf("projects/%s/roles/%s", projectName, roleID), + }) + return role, err +} + +// CreateRole creates a new role given permissions +func CreateRole(gcpClient gcp.GcpClient, permissions []string, roleName, roleID, roleDescription, projectName string) (*adminpb.Role, error) { + role, err := gcpClient.CreateRole(context.TODO(), &adminpb.CreateRoleRequest{ + Role: &adminpb.Role{ + Title: roleName, + Description: roleDescription, + IncludedPermissions: permissions, + Stage: adminpb.Role_GA, + }, + Parent: fmt.Sprintf("projects/%s", projectName), + RoleId: roleID, + }) + if err != nil { + return nil, err + } + return role, nil +} + +// UpdateRole updates an existing role given permissions +func UpdateRole(gcpClient gcp.GcpClient, role *adminpb.Role, roleName string) (*adminpb.Role, error) { + role, err := gcpClient.UpdateRole(context.TODO(), &adminpb.UpdateRoleRequest{ + Name: roleName, + Role: role, + }) + if err != nil { + return nil, err + } + return role, nil +} + +// DeleteRole deletes the role created to satisfy a credentials request +func DeleteRole(gcpClient gcp.GcpClient, roleName string) (*adminpb.Role, error) { + role, err := gcpClient.DeleteRole(context.TODO(), &adminpb.DeleteRoleRequest{ + Name: roleName, + }) + return role, err +} + +// UndeleteRole undeletes a previously deleted role that has not yet been pruned +func UndeleteRole(gcpClient gcp.GcpClient, roleName string) (*adminpb.Role, error) { + role, err := gcpClient.UndeleteRole(context.TODO(), &adminpb.UndeleteRoleRequest{ + Name: roleName, + }) + return role, err +} diff --git a/cmd/ocm/gcp/scripting.go b/cmd/ocm/gcp/scripting.go index 1254202a..d7f7b24f 100644 --- a/cmd/ocm/gcp/scripting.go +++ b/cmd/ocm/gcp/scripting.go @@ -31,6 +31,48 @@ func createScript(targetDir string, wifConfig *models.WifConfigOutput) error { return nil } +func createDeleteScript(targetDir string, wifConfig *models.WifConfigOutput) error { + // Write the script content to the path + scriptContent := generateDeleteScriptContent(wifConfig) + err := os.WriteFile(filepath.Join(targetDir, "delete.sh"), []byte(scriptContent), 0644) + if err != nil { + return err + } + return nil +} + +func generateDeleteScriptContent(wifConfig *models.WifConfigOutput) string { + scriptContent := "#!/bin/bash\n" + + // Append the script to delete the service accounts + scriptContent += deleteServiceAccountScriptContent(wifConfig) + + // Append the script to delete the workload identity pool + scriptContent += deleteIdentityPoolScriptContent(wifConfig) + + return scriptContent +} +func deleteServiceAccountScriptContent(wifConfig *models.WifConfigOutput) string { + var sb strings.Builder + sb.WriteString("\n# Delete service accounts:\n") + for _, sa := range wifConfig.Status.ServiceAccounts { + project := wifConfig.Spec.ProjectId + serviceAccountID := sa.GetId() + sb.WriteString(fmt.Sprintf("gcloud iam service-accounts delete %s --project=%s\n", + serviceAccountID, project)) + } + return sb.String() +} + +func deleteIdentityPoolScriptContent(wifConfig *models.WifConfigOutput) string { + pool := wifConfig.Status.WorkloadIdentityPoolData + // Delete the workload identity pool + return fmt.Sprintf(` +# Delete the workload identity pool +gcloud iam workload-identity-pools delete %s --project=%s --location=global +`, pool.PoolId, pool.ProjectId) +} + func generateScriptContent(wifConfig *models.WifConfigOutput) string { poolSpec := gcp.WorkloadIdentityPoolSpec{ PoolName: wifConfig.Status.WorkloadIdentityPoolData.PoolId, @@ -98,6 +140,20 @@ func createServiceAccountScriptContent(wifConfig *models.WifConfigOutput) string sb.WriteString(fmt.Sprintf("gcloud iam service-accounts create %s --display-name=%s --description=\"%s\" --project=%s\n", serviceAccountID, serviceAccountName, serviceAccountDesc, project)) } + sb.WriteString("\n# Create roles:\n") + for _, sa := range wifConfig.Status.ServiceAccounts { + for _, role := range sa.Roles { + if !role.Predefined { + roleId := strings.ReplaceAll(role.Id, "-", "_") + project := wifConfig.Spec.ProjectId + permissions := strings.Join(role.Permissions, ",") + roleName := roleId + serviceAccountDesc := roleDescription + " for WIF config " + wifConfig.Spec.DisplayName + sb.WriteString(fmt.Sprintf("gcloud iam roles create %s --project=%s --title=%s --description=\"%s\" --stage=GA --permissions=%s\n", + roleId, project, roleName, serviceAccountDesc, permissions)) + } + } + } sb.WriteString("\n# Bind service account roles:\n") for _, sa := range wifConfig.Status.ServiceAccounts { for _, role := range sa.Roles { diff --git a/cmd/ocm/gcp/scripting_test.go b/cmd/ocm/gcp/scripting_test.go new file mode 100644 index 00000000..1b3e47a4 --- /dev/null +++ b/cmd/ocm/gcp/scripting_test.go @@ -0,0 +1,61 @@ +package gcp + +import ( + "os" + "testing" + + . "github.com/onsi/gomega" +) + +func TestCreateScript(t *testing.T) { + tempDir, err := os.MkdirTemp("", "test") + Expect(err).To(BeNil()) + +} + +// // Create a temporary directory for testing +// tempDir, err := ioutil.TempDir("", "test") +// assert.NoError(t, err) +// defer os.RemoveAll(tempDir) + +// // Create a mock WifConfigOutput +// wifConfig := &models.WifConfigOutput{ +// Status: &models.WifConfigStatus{ +// WorkloadIdentityPoolData: &models.WorkloadIdentityPoolData{ +// Jwks: "mock-jwks", +// }, +// }, +// } + +// // Call the createScript function +// err = createScript(tempDir, wifConfig) +// assert.NoError(t, err) + +// // Verify that the script.sh file was created +// scriptPath := filepath.Join(tempDir, "script.sh") +// _, err = os.Stat(scriptPath) +// assert.NoError(t, err) + +// // Verify the content of the script.sh file +// scriptContent, err := ioutil.ReadFile(scriptPath) +// assert.NoError(t, err) +// expectedScriptContent := "#!/bin/bash\n" + +// "# Create a workload identity pool\n" + +// "...\n" + +// "# Create a workload identity provider\n" + +// "...\n" + +// "# Create service accounts:\n" + +// "...\n" +// assert.Equal(t, expectedScriptContent, string(scriptContent)) + +// // Verify that the jwk.json file was created +// jwkPath := filepath.Join(tempDir, "jwk.json") +// _, err = os.Stat(jwkPath) +// assert.NoError(t, err) + +// // Verify the content of the jwk.json file +// jwkContent, err := ioutil.ReadFile(jwkPath) +// assert.NoError(t, err) +// expectedJwkContent := "mock-jwks" +// assert.Equal(t, expectedJwkContent, string(jwkContent)) +// } diff --git a/pkg/gcp/client.go b/pkg/gcp/client.go index c8ae3372..60164e81 100644 --- a/pkg/gcp/client.go +++ b/pkg/gcp/client.go @@ -41,6 +41,13 @@ type GcpClient interface { RetreiveSecret(secretId string, projectId string) ([]byte, error) ProjectNumberFromId(projectId string) (int64, error) + + GetRole(context.Context, *adminpb.GetRoleRequest) (*adminpb.Role, error) + CreateRole(context.Context, *adminpb.CreateRoleRequest) (*adminpb.Role, error) + UpdateRole(context.Context, *adminpb.UpdateRoleRequest) (*adminpb.Role, error) + DeleteRole(context.Context, *adminpb.DeleteRoleRequest) (*adminpb.Role, error) + UndeleteRole(context.Context, *adminpb.UndeleteRoleRequest) (*adminpb.Role, error) + ListRoles(context.Context, *adminpb.ListRolesRequest) (*adminpb.ListRolesResponse, error) } type ServiceAccount interface { @@ -308,3 +315,27 @@ func (c *gcpClient) GetProjectIamPolicy(projectName string, request *cloudresour func (c *gcpClient) SetProjectIamPolicy(svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) { return c.cloudResourceManager.Projects.SetIamPolicy(svcAcctResource, request).Context(context.Background()).Do() } + +func (c *gcpClient) GetRole(ctx context.Context, request *adminpb.GetRoleRequest) (*adminpb.Role, error) { + return c.iamClient.GetRole(ctx, request) +} + +func (c *gcpClient) CreateRole(ctx context.Context, request *adminpb.CreateRoleRequest) (*adminpb.Role, error) { + return c.iamClient.CreateRole(ctx, request) +} + +func (c *gcpClient) UpdateRole(ctx context.Context, request *adminpb.UpdateRoleRequest) (*adminpb.Role, error) { + return c.iamClient.UpdateRole(ctx, request) +} + +func (c *gcpClient) DeleteRole(ctx context.Context, request *adminpb.DeleteRoleRequest) (*adminpb.Role, error) { + return c.iamClient.DeleteRole(ctx, request) +} + +func (c *gcpClient) UndeleteRole(ctx context.Context, request *adminpb.UndeleteRoleRequest) (*adminpb.Role, error) { + return c.iamClient.UndeleteRole(ctx, request) +} + +func (c *gcpClient) ListRoles(ctx context.Context, request *adminpb.ListRolesRequest) (*adminpb.ListRolesResponse, error) { + return c.iamClient.ListRoles(ctx, request) +}