From 0275d67d17ea06dce46d17107ce8a6cc6f6a5af4 Mon Sep 17 00:00:00 2001 From: Renan Campos Date: Fri, 13 Sep 2024 16:40:08 -0400 Subject: [PATCH] Revert "Grant access to support group during WifConfig creation (#663)" (#664) This reverts commit a39ce2ed488beade506f19552eeeda0d9554cb13. --- cmd/ocm/gcp/create-wif-config.go | 324 +++++++++++++++---- cmd/ocm/gcp/delete-wif-config.go | 2 +- cmd/ocm/gcp/gcp_client_shim.go | 484 ----------------------------- cmd/ocm/gcp/generate-wif-script.go | 2 +- cmd/ocm/gcp/iam.go | 147 +++++++++ cmd/ocm/gcp/scripting.go | 36 --- go.mod | 2 +- go.sum | 4 +- pkg/gcp/client.go | 261 +++++++++++----- pkg/gcp/error_handlers.go | 31 ++ 10 files changed, 635 insertions(+), 658 deletions(-) delete mode 100644 cmd/ocm/gcp/gcp_client_shim.go create mode 100644 cmd/ocm/gcp/iam.go diff --git a/cmd/ocm/gcp/create-wif-config.go b/cmd/ocm/gcp/create-wif-config.go index 2f59858c..8c4b4a05 100644 --- a/cmd/ocm/gcp/create-wif-config.go +++ b/cmd/ocm/gcp/create-wif-config.go @@ -6,14 +6,22 @@ import ( "log" "os" "path/filepath" + "reflect" "strconv" + "strings" + "github.com/googleapis/gax-go/v2/apierror" + + "cloud.google.com/go/iam/admin/apiv1/adminpb" "github.com/openshift-online/ocm-cli/pkg/gcp" "github.com/openshift-online/ocm-cli/pkg/ocm" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" "github.com/pkg/errors" "github.com/spf13/cobra" + "google.golang.org/api/googleapi" + iamv1 "google.golang.org/api/iam/v1" + "google.golang.org/grpc/codes" ) var ( @@ -37,8 +45,8 @@ func NewCreateWorkloadIdentityConfiguration() *cobra.Command { createWifConfigCmd := &cobra.Command{ Use: "wif-config", Short: "Create workload identity configuration", - PreRunE: validationForCreateWorkloadIdentityConfigurationCmd, RunE: createWorkloadIdentityConfigurationCmd, + PreRunE: validationForCreateWorkloadIdentityConfigurationCmd, } createWifConfigCmd.PersistentFlags().StringVar(&CreateWifConfigOpts.Name, "name", "", @@ -57,41 +65,8 @@ func NewCreateWorkloadIdentityConfiguration() *cobra.Command { return createWifConfigCmd } -func validationForCreateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { - if CreateWifConfigOpts.Name == "" { - return fmt.Errorf("Name is required") - } - if CreateWifConfigOpts.Project == "" { - return fmt.Errorf("Project is required") - } - - if CreateWifConfigOpts.TargetDir == "" { - pwd, err := os.Getwd() - if err != nil { - return errors.Wrapf(err, "failed to get current directory") - } - - CreateWifConfigOpts.TargetDir = pwd - } - - fPath, err := filepath.Abs(CreateWifConfigOpts.TargetDir) - if err != nil { - return errors.Wrapf(err, "failed to resolve full path") - } - - sResult, err := os.Stat(fPath) - if os.IsNotExist(err) { - return fmt.Errorf("directory %s does not exist", fPath) - } - if !sResult.IsDir() { - return fmt.Errorf("file %s exists and is not a directory", fPath) - } - return nil -} - func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { ctx := context.Background() - log := log.Default() gcpClient, err := gcp.NewGcpClient(ctx) if err != nil { @@ -99,12 +74,7 @@ func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e } log.Println("Creating workload identity configuration...") - wifConfig, err := createWorkloadIdentityConfiguration( - ctx, - gcpClient, - CreateWifConfigOpts.Name, - CreateWifConfigOpts.Project, - ) + wifConfig, err := createWorkloadIdentityConfiguration(gcpClient, CreateWifConfigOpts.Name, CreateWifConfigOpts.Project) if err != nil { return errors.Wrapf(err, "failed to create WIF config") } @@ -112,7 +82,7 @@ func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e if CreateWifConfigOpts.DryRun { log.Printf("Writing script files to %s", CreateWifConfigOpts.TargetDir) - projectNum, err := gcpClient.ProjectNumberFromId(ctx, wifConfig.Gcp().ProjectId()) + projectNum, err := gcpClient.ProjectNumberFromId(wifConfig.Gcp().ProjectId()) if err != nil { return errors.Wrapf(err, "failed to get project number from id") } @@ -123,40 +93,58 @@ func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e return nil } - gcpClientWifConfigShim := NewGcpClientWifConfigShim(GcpClientWifConfigShimSpec{ - GcpClient: gcpClient, - WifConfig: wifConfig, - }) - - if err := gcpClientWifConfigShim.GrantSupportAccess(ctx, log); err != nil { - log.Printf("Failed to grant support access to project: %s", err) - return fmt.Errorf("To clean up, run the following command: ocm gcp delete wif-config %s", wifConfig.ID()) - } - - if err := gcpClientWifConfigShim.CreateWorkloadIdentityPool(ctx, log); err != nil { + if err = createWorkloadIdentityPool(ctx, gcpClient, wifConfig); err != nil { log.Printf("Failed to create workload identity pool: %s", err) return fmt.Errorf("To clean up, run the following command: ocm gcp delete wif-config %s", wifConfig.ID()) } - if err = gcpClientWifConfigShim.CreateWorkloadIdentityProvider(ctx, log); err != nil { + if err = createWorkloadIdentityProvider(ctx, gcpClient, wifConfig); err != nil { log.Printf("Failed to create workload identity provider: %s", err) return fmt.Errorf("To clean up, run the following command: ocm gcp delete wif-config %s", wifConfig.ID()) } - if err = gcpClientWifConfigShim.CreateServiceAccounts(ctx, log); err != nil { + if err = createServiceAccounts(ctx, gcpClient, wifConfig); err != nil { log.Printf("Failed to create IAM service accounts: %s", err) return fmt.Errorf("To clean up, run the following command: ocm gcp delete wif-config %s", wifConfig.ID()) } return nil } -func createWorkloadIdentityConfiguration( - ctx context.Context, - client gcp.GcpClient, - displayName string, - projectId string, -) (*cmv1.WifConfig, error) { - projectNum, err := client.ProjectNumberFromId(ctx, projectId) +func validationForCreateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { + if CreateWifConfigOpts.Name == "" { + return fmt.Errorf("Name is required") + } + if CreateWifConfigOpts.Project == "" { + return fmt.Errorf("Project is required") + } + + if CreateWifConfigOpts.TargetDir == "" { + pwd, err := os.Getwd() + if err != nil { + return errors.Wrapf(err, "failed to get current directory") + } + + CreateWifConfigOpts.TargetDir = pwd + } + + fPath, err := filepath.Abs(CreateWifConfigOpts.TargetDir) + if err != nil { + return errors.Wrapf(err, "failed to resolve full path") + } + + sResult, err := os.Stat(fPath) + if os.IsNotExist(err) { + return fmt.Errorf("directory %s does not exist", fPath) + } + if !sResult.IsDir() { + return fmt.Errorf("file %s exists and is not a directory", fPath) + } + return nil +} + +func createWorkloadIdentityConfiguration(client gcp.GcpClient, displayName, projectId string) (*cmv1.WifConfig, error) { + + projectNum, err := client.ProjectNumberFromId(projectId) if err != nil { return nil, errors.Wrapf(err, "failed to get GCP project number from project id") } @@ -194,3 +182,217 @@ func createWorkloadIdentityConfiguration( return response.Body(), nil } + +func createWorkloadIdentityPool(ctx context.Context, client gcp.GcpClient, + wifConfig *cmv1.WifConfig) error { + poolId := wifConfig.Gcp().WorkloadIdentityPool().PoolId() + project := wifConfig.Gcp().ProjectId() + + parentResourceForPool := fmt.Sprintf("projects/%s/locations/global", project) + poolResource := fmt.Sprintf("%s/workloadIdentityPools/%s", parentResourceForPool, poolId) + resp, err := client.GetWorkloadIdentityPool(ctx, poolResource) + if resp != nil && resp.State == "DELETED" { + log.Printf("Workload identity pool %s was deleted", poolId) + _, err := client.UndeleteWorkloadIdentityPool(ctx, poolResource, &iamv1.UndeleteWorkloadIdentityPoolRequest{}) + if err != nil { + return errors.Wrapf(err, "failed to undelete workload identity pool %s", poolId) + } + } else if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && + strings.Contains(gerr.Message, "Requested entity was not found") { + pool := &iamv1.WorkloadIdentityPool{ + Name: poolId, + DisplayName: poolId, + Description: poolDescription, + State: "ACTIVE", + Disabled: false, + } + + _, err := client.CreateWorkloadIdentityPool(ctx, parentResourceForPool, poolId, pool) + if err != nil { + return errors.Wrapf(err, "failed to create workload identity pool %s", poolId) + } + log.Printf("Workload identity pool created with name %s", poolId) + } else { + return errors.Wrapf(err, "failed to check if there is existing workload identity pool %s", poolId) + } + } else { + log.Printf("Workload identity pool %s already exists", poolId) + } + + return nil +} + +func createWorkloadIdentityProvider(ctx context.Context, client gcp.GcpClient, + wifConfig *cmv1.WifConfig) error { + projectId := wifConfig.Gcp().ProjectId() + poolId := wifConfig.Gcp().WorkloadIdentityPool().PoolId() + jwks := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().Jwks() + audiences := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().AllowedAudiences() + issuerUrl := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IssuerUrl() + providerId := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IdentityProviderId() + + parent := fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", projectId, poolId) + providerResource := fmt.Sprintf("%s/providers/%s", parent, providerId) + + _, err := client.GetWorkloadIdentityProvider(ctx, providerResource) + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && + strings.Contains(gerr.Message, "Requested entity was not found") { + provider := &iamv1.WorkloadIdentityPoolProvider{ + Name: providerId, + DisplayName: providerId, + Description: poolDescription, + State: "ACTIVE", + Disabled: false, + Oidc: &iamv1.Oidc{ + AllowedAudiences: audiences, + IssuerUri: issuerUrl, + JwksJson: jwks, + }, + AttributeMapping: map[string]string{ + "google.subject": "assertion.sub", + }, + } + + _, err := client.CreateWorkloadIdentityProvider(ctx, parent, providerId, provider) + if err != nil { + return errors.Wrapf(err, "failed to create workload identity provider %s", providerId) + } + log.Printf("workload identity provider created with name %s", providerId) + } else { + return errors.Wrapf(err, "failed to check if there is existing workload identity provider %s in pool %s", + providerId, poolId) + } + } else { + return errors.Errorf("workload identity provider %s already exists in pool %s", providerId, poolId) + } + + return nil +} + +func createServiceAccounts(ctx context.Context, gcpClient gcp.GcpClient, wifConfig *cmv1.WifConfig) error { + projectId := wifConfig.Gcp().ProjectId() + fmtRoleResourceId := func(role *cmv1.WifRole) string { + if role.Predefined() { + return fmt.Sprintf("roles/%s", role.RoleId()) + } else { + return fmt.Sprintf("projects/%s/roles/%s", projectId, role.RoleId()) + } + } + + // Create service accounts + for _, serviceAccount := range wifConfig.Gcp().ServiceAccounts() { + serviceAccountID := serviceAccount.ServiceAccountId() + serviceAccountName := wifConfig.DisplayName() + "-" + serviceAccountID + serviceAccountDesc := poolDescription + " for WIF config " + wifConfig.DisplayName() + + _, err := createServiceAccount(gcpClient, serviceAccountID, serviceAccountName, serviceAccountDesc, projectId, true) + if err != nil { + return errors.Wrap(err, "Failed to create IAM service account") + } + log.Printf("IAM service account %s created", serviceAccountID) + } + + // Create roles that aren't predefined + for _, serviceAccount := range wifConfig.Gcp().ServiceAccounts() { + for _, role := range serviceAccount.Roles() { + if role.Predefined() { + continue + } + roleID := role.RoleId() + roleTitle := role.RoleId() + permissions := role.Permissions() + existingRole, err := GetRole(gcpClient, fmtRoleResourceId(role)) + if err != nil { + if gerr, ok := err.(*apierror.APIError); ok && gerr.GRPCStatus().Code() == codes.NotFound { + _, err = CreateRole(gcpClient, permissions, roleTitle, + roleID, roleDescription, projectId) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to create %s", roleID)) + } + log.Printf("Role %q created", roleID) + continue + } else { + return errors.Wrap(err, "Failed to check if role exists") + } + } + + // Undelete role if it was deleted + if existingRole.Deleted { + _, err = UndeleteRole(gcpClient, fmtRoleResourceId(role)) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to undelete custom role %q", roleID)) + } + existingRole.Deleted = false + log.Printf("Role %q undeleted", roleID) + } + + // Update role if permissions have changed + if !reflect.DeepEqual(existingRole.IncludedPermissions, permissions) { + existingRole.IncludedPermissions = permissions + _, err := UpdateRole(gcpClient, existingRole, fmtRoleResourceId(role)) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to update %s", roleID)) + } + log.Printf("Role %q updated", roleID) + } + } + } + + // Bind roles and grant access + for _, serviceAccount := range wifConfig.Gcp().ServiceAccounts() { + serviceAccountID := serviceAccount.ServiceAccountId() + + roles := make([]string, 0, len(serviceAccount.Roles())) + for _, role := range serviceAccount.Roles() { + roles = append(roles, fmtRoleResourceId(role)) + } + member := fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccountID, projectId) + err := EnsurePolicyBindingsForProject(gcpClient, roles, member, projectId) + if err != nil { + return errors.Errorf("Failed to bind roles to service account %s: %s", serviceAccountID, err) + } + + switch serviceAccount.AccessMethod() { + case cmv1.WifAccessMethodImpersonate: + if err := gcpClient.AttachImpersonator(serviceAccount.ServiceAccountId(), projectId, + wifConfig.Gcp().ImpersonatorEmail()); err != nil { + return errors.Wrapf(err, "Failed to attach impersonator to service account %s", + serviceAccount.ServiceAccountId()) + } + case cmv1.WifAccessMethodWif: + if err := gcpClient.AttachWorkloadIdentityPool(serviceAccount, + wifConfig.Gcp().WorkloadIdentityPool().PoolId(), projectId); err != nil { + return errors.Wrapf(err, "Failed to attach workload identity pool to service account %s", + serviceAccount.ServiceAccountId()) + } + default: + log.Printf("Warning: %s is not a supported access type\n", serviceAccount.AccessMethod()) + } + } + + return nil +} + +func createServiceAccount(gcpClient gcp.GcpClient, svcAcctID, svcAcctName, svcAcctDescription, + projectName string, allowExisting bool) (*adminpb.ServiceAccount, error) { + request := &adminpb.CreateServiceAccountRequest{ + Name: fmt.Sprintf("projects/%s", projectName), + AccountId: svcAcctID, + ServiceAccount: &adminpb.ServiceAccount{ + DisplayName: svcAcctName, + Description: svcAcctDescription, + }, + } + svcAcct, err := gcpClient.CreateServiceAccount(context.TODO(), request) + if err != nil { + pApiError, ok := err.(*apierror.APIError) + if ok { + if pApiError.GRPCStatus().Code() == codes.AlreadyExists && allowExisting { + return svcAcct, nil + } + } + } + return svcAcct, err +} diff --git a/cmd/ocm/gcp/delete-wif-config.go b/cmd/ocm/gcp/delete-wif-config.go index 576c3f14..6dd6b020 100644 --- a/cmd/ocm/gcp/delete-wif-config.go +++ b/cmd/ocm/gcp/delete-wif-config.go @@ -113,7 +113,7 @@ func deleteServiceAccounts(ctx context.Context, gcpClient gcp.GcpClient, for _, serviceAccount := range wifConfig.Gcp().ServiceAccounts() { serviceAccountID := serviceAccount.ServiceAccountId() log.Println("Deleting service account", serviceAccountID) - err := gcpClient.DeleteServiceAccount(ctx, serviceAccountID, projectId, allowMissing) + err := gcpClient.DeleteServiceAccount(serviceAccountID, projectId, allowMissing) if err != nil { return errors.Wrapf(err, "Failed to delete service account %q", serviceAccountID) } diff --git a/cmd/ocm/gcp/gcp_client_shim.go b/cmd/ocm/gcp/gcp_client_shim.go deleted file mode 100644 index f5454297..00000000 --- a/cmd/ocm/gcp/gcp_client_shim.go +++ /dev/null @@ -1,484 +0,0 @@ -package gcp - -import ( - "context" - "fmt" - "log" - "reflect" - "strings" - - "cloud.google.com/go/iam/admin/apiv1/adminpb" - "github.com/googleapis/gax-go/v2/apierror" - cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" - "github.com/pkg/errors" - cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1" - "google.golang.org/api/googleapi" - iamv1 "google.golang.org/api/iam/v1" - "google.golang.org/grpc/codes" - - "github.com/openshift-online/ocm-cli/pkg/gcp" -) - -type GcpClientWifConfigShim interface { - CreateServiceAccounts(ctx context.Context, log *log.Logger) error - CreateWorkloadIdentityPool(ctx context.Context, log *log.Logger) error - CreateWorkloadIdentityProvider(ctx context.Context, log *log.Logger) error - GrantSupportAccess(ctx context.Context, log *log.Logger) error -} - -type shim struct { - wifConfig *cmv1.WifConfig - gcpClient gcp.GcpClient -} - -type GcpClientWifConfigShimSpec struct { - WifConfig *cmv1.WifConfig - GcpClient gcp.GcpClient -} - -func NewGcpClientWifConfigShim(spec GcpClientWifConfigShimSpec) GcpClientWifConfigShim { - return &shim{ - wifConfig: spec.WifConfig, - gcpClient: spec.GcpClient, - } -} - -func (c *shim) CreateWorkloadIdentityPool( - ctx context.Context, - log *log.Logger, -) error { - poolId := c.wifConfig.Gcp().WorkloadIdentityPool().PoolId() - project := c.wifConfig.Gcp().ProjectId() - - parentResourceForPool := fmt.Sprintf("projects/%s/locations/global", project) - poolResource := fmt.Sprintf("%s/workloadIdentityPools/%s", parentResourceForPool, poolId) - resp, err := c.gcpClient.GetWorkloadIdentityPool(ctx, poolResource) - if resp != nil && resp.State == "DELETED" { - log.Printf("Workload identity pool %s was deleted", poolId) - _, err := c.gcpClient.UndeleteWorkloadIdentityPool( - ctx, poolResource, &iamv1.UndeleteWorkloadIdentityPoolRequest{}, - ) - if err != nil { - return errors.Wrapf(err, "failed to undelete workload identity pool %s", poolId) - } - } else if err != nil { - if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && - strings.Contains(gerr.Message, "Requested entity was not found") { - pool := &iamv1.WorkloadIdentityPool{ - Name: poolId, - DisplayName: poolId, - Description: poolDescription, - State: "ACTIVE", - Disabled: false, - } - - _, err := c.gcpClient.CreateWorkloadIdentityPool(ctx, parentResourceForPool, poolId, pool) - if err != nil { - return errors.Wrapf(err, "failed to create workload identity pool %s", poolId) - } - log.Printf("Workload identity pool created with name %s", poolId) - } else { - return errors.Wrapf(err, "failed to check if there is existing workload identity pool %s", poolId) - } - } else { - log.Printf("Workload identity pool %s already exists", poolId) - } - - return nil -} - -func (c *shim) CreateWorkloadIdentityProvider( - ctx context.Context, - log *log.Logger, -) error { - projectId := c.wifConfig.Gcp().ProjectId() - poolId := c.wifConfig.Gcp().WorkloadIdentityPool().PoolId() - jwks := c.wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().Jwks() - audiences := c.wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().AllowedAudiences() - issuerUrl := c.wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IssuerUrl() - providerId := c.wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IdentityProviderId() - - parent := fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", projectId, poolId) - providerResource := fmt.Sprintf("%s/providers/%s", parent, providerId) - - _, err := c.gcpClient.GetWorkloadIdentityProvider(ctx, providerResource) - if err != nil { - if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && - strings.Contains(gerr.Message, "Requested entity was not found") { - provider := &iamv1.WorkloadIdentityPoolProvider{ - Name: providerId, - DisplayName: providerId, - Description: poolDescription, - State: "ACTIVE", - Disabled: false, - Oidc: &iamv1.Oidc{ - AllowedAudiences: audiences, - IssuerUri: issuerUrl, - JwksJson: jwks, - }, - AttributeMapping: map[string]string{ - "google.subject": "assertion.sub", - }, - } - - _, err := c.gcpClient.CreateWorkloadIdentityProvider(ctx, parent, providerId, provider) - if err != nil { - return errors.Wrapf(err, "failed to create workload identity provider %s", providerId) - } - log.Printf("Workload identity provider created with name %s", providerId) - } else { - return errors.Wrapf(err, "failed to check if there is existing workload identity provider %s in pool %s", - providerId, poolId) - } - } else { - return errors.Errorf("workload identity provider %s already exists in pool %s", providerId, poolId) - } - - return nil -} - -func (c *shim) CreateServiceAccounts( - ctx context.Context, - log *log.Logger, -) error { - for _, serviceAccount := range c.wifConfig.Gcp().ServiceAccounts() { - if err := c.createServiceAccount(ctx, log, serviceAccount, c.wifConfig); err != nil { - return err - } - if err := c.createOrUpdateRoles(ctx, log, serviceAccount.Roles()); err != nil { - return err - } - if err := c.bindRolesToServiceAccount(ctx, serviceAccount); err != nil { - return err - } - if err := c.grantAccessToServiceAccount(ctx, serviceAccount); err != nil { - return err - } - } - return nil -} - -func (c *shim) GrantSupportAccess( - ctx context.Context, - log *log.Logger, -) error { - support := c.wifConfig.Gcp().Support() - if err := c.createOrUpdateRoles(ctx, log, support.Roles()); err != nil { - return err - } - if err := c.bindRolesToGroup(ctx, support.Principal(), support.Roles()); err != nil { - return err - } - log.Printf("support access granted to %s", support.Principal()) - return nil -} - -func (c *shim) createServiceAccount( - ctx context.Context, - log *log.Logger, - serviceAccount *cmv1.WifServiceAccount, - wifConfig *cmv1.WifConfig, -) error { - serviceAccountId := serviceAccount.ServiceAccountId() - serviceAccountName := wifConfig.DisplayName() + "-" + serviceAccountId - serviceAccountDesc := poolDescription + " for WIF config " + wifConfig.DisplayName() - - request := &adminpb.CreateServiceAccountRequest{ - Name: fmt.Sprintf("projects/%s", c.wifConfig.Gcp().ProjectId()), - AccountId: serviceAccountId, - ServiceAccount: &adminpb.ServiceAccount{ - DisplayName: serviceAccountName, - Description: serviceAccountDesc, - }, - } - _, err := c.gcpClient.CreateServiceAccount(ctx, request) - if err != nil { - pApiError, ok := err.(*apierror.APIError) - if ok { - if pApiError.GRPCStatus().Code() == codes.AlreadyExists { - return nil - } - } - } - if err != nil { - return errors.Wrap(err, "Failed to create IAM service account") - } - log.Printf("IAM service account %s created", serviceAccountId) - return nil -} - -func (c *shim) createOrUpdateRoles(ctx context.Context, log *log.Logger, roles []*cmv1.WifRole) error { - for _, role := range roles { - if role.Predefined() { - continue - } - roleID := role.RoleId() - roleTitle := role.RoleId() - permissions := role.Permissions() - existingRole, err := c.getRole(ctx, c.fmtRoleResourceId(role)) - if err != nil { - if gerr, ok := err.(*apierror.APIError); ok && gerr.GRPCStatus().Code() == codes.NotFound { - _, err = c.createRole( - ctx, - permissions, - roleTitle, - roleID, - roleDescription, - c.wifConfig.Gcp().ProjectId(), - ) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("Failed to create %s", roleID)) - } - log.Printf("Role %q created", roleID) - continue - } else { - return errors.Wrap(err, "Failed to check if role exists") - } - } - - // Undelete role if it was deleted - if existingRole.Deleted { - _, err = c.undeleteRole(ctx, c.fmtRoleResourceId(role)) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("Failed to undelete custom role %q", roleID)) - } - existingRole.Deleted = false - log.Printf("Role %q undeleted", roleID) - } - - // Update role if permissions have changed - if !reflect.DeepEqual(existingRole.IncludedPermissions, permissions) { - existingRole.IncludedPermissions = permissions - _, err := c.updateRole(ctx, existingRole, c.fmtRoleResourceId(role)) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("Failed to update %s", roleID)) - } - log.Printf("Role %q updated", roleID) - } - } - return nil -} - -func (c *shim) bindRolesToServiceAccount(ctx context.Context, serviceAccount *cmv1.WifServiceAccount) error { - serviceAccountId := serviceAccount.ServiceAccountId() - roles := serviceAccount.Roles() - - return c.bindRolesToPrincipal( - ctx, - fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccountId, c.wifConfig.Gcp().ProjectId()), - roles, - ) -} - -func (c *shim) bindRolesToGroup(ctx context.Context, groupEmail string, roles []*cmv1.WifRole) error { - return c.bindRolesToPrincipal( - ctx, - fmt.Sprintf("group:%s", groupEmail), - roles, - ) -} - -func (c *shim) bindRolesToPrincipal(ctx context.Context, principal string, roles []*cmv1.WifRole) error { - formattedRoles := make([]string, 0, len(roles)) - for _, role := range roles { - formattedRoles = append(formattedRoles, c.fmtRoleResourceId(role)) - } - err := c.ensurePolicyBindingsForProject( - ctx, - formattedRoles, - principal, - c.wifConfig.Gcp().ProjectId(), - ) - if err != nil { - return errors.Errorf("Failed to bind roles to principal %s: %s", principal, err) - } - return nil -} - -func (c *shim) grantAccessToServiceAccount( - ctx context.Context, - serviceAccount *cmv1.WifServiceAccount, -) error { - switch serviceAccount.AccessMethod() { - case cmv1.WifAccessMethodImpersonate: - if err := c.gcpClient.AttachImpersonator( - ctx, - serviceAccount.ServiceAccountId(), - c.wifConfig.Gcp().ProjectId(), - c.wifConfig.Gcp().ImpersonatorEmail(), - ); err != nil { - return errors.Wrapf(err, "Failed to attach impersonator to service account %s", - serviceAccount.ServiceAccountId()) - } - case cmv1.WifAccessMethodWif: - if err := c.gcpClient.AttachWorkloadIdentityPool( - ctx, - serviceAccount, - c.wifConfig.Gcp().WorkloadIdentityPool().PoolId(), - c.wifConfig.Gcp().ProjectId(), - ); err != nil { - return errors.Wrapf(err, "Failed to attach workload identity pool to service account %s", - serviceAccount.ServiceAccountId()) - } - case cmv1.WifAccessMethodVm: - // Service accounts with the "vm" access method require no external access - return nil - default: - log.Printf("Warning: %s is not a supported access type\n", serviceAccount.AccessMethod()) - } - return nil -} - -func (c *shim) fmtRoleResourceId(role *cmv1.WifRole) string { - if role.Predefined() { - return fmt.Sprintf("roles/%s", role.RoleId()) - } else { - return fmt.Sprintf("projects/%s/roles/%s", c.wifConfig.Gcp().ProjectId(), role.RoleId()) - } -} - -// GetRole fetches the role created to satisfy a credentials request. -// Custom roles should follow the format projects/{project}/roles/{role_id}. -func (c *shim) getRole(ctx context.Context, roleName string) (*adminpb.Role, error) { - role, err := c.gcpClient.GetRole(ctx, &adminpb.GetRoleRequest{ - Name: roleName, - }) - return role, err -} - -// CreateRole creates a new role given permissions -func (c *shim) createRole(ctx context.Context, permissions []string, roleTitle, roleId, roleDescription, - projectName string) (*adminpb.Role, error) { - role, err := c.gcpClient.CreateRole(ctx, &adminpb.CreateRoleRequest{ - Role: &adminpb.Role{ - Title: roleTitle, - 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. -// Custom roles should follow the format projects/{project}/roles/{role_id}. -func (c *shim) updateRole(ctx context.Context, role *adminpb.Role, roleName string) (*adminpb.Role, error) { - updated, err := c.gcpClient.UpdateRole(ctx, &adminpb.UpdateRoleRequest{ - Name: roleName, - Role: role, - }) - if err != nil { - return nil, err - } - return updated, nil -} - -// UndeleteRole undeletes a previously deleted role that has not yet been pruned -func (c *shim) undeleteRole(ctx context.Context, roleName string) (*adminpb.Role, error) { - role, err := c.gcpClient.UndeleteRole(ctx, &adminpb.UndeleteRoleRequest{ - Name: roleName, - }) - return role, err -} - -// EnsurePolicyBindingsForProject ensures that given roles and member, appropriate binding is added to project. -// Roles should be in the format projects/{project}/roles/{role_id} for custom roles and roles/{role_id} -// for predefined roles. -func (c *shim) ensurePolicyBindingsForProject( - ctx context.Context, - roles []string, - member string, - projectName string, -) error { - needPolicyUpdate := false - - policy, err := c.gcpClient.GetProjectIamPolicy(ctx, 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 := c.addPolicyBindingForProject(policy, definedRole, member) - if modified { - needPolicyUpdate = true - } - - } - - if needPolicyUpdate { - return c.setProjectIamPolicy(ctx, policy) - } - - // If we made it this far there were no updates needed - return nil -} - -func (c *shim) setProjectIamPolicy( - ctx context.Context, - policy *cloudresourcemanager.Policy, -) error { - _, err := c.gcpClient.SetProjectIamPolicy( - ctx, - c.wifConfig.Gcp().ProjectId(), - &cloudresourcemanager.SetIamPolicyRequest{ - Policy: policy, - }) - if err != nil { - return fmt.Errorf("error setting project policy: %v", err) - } - return nil -} - -func (c *shim) addPolicyBindingForProject( - policy *cloudresourcemanager.Policy, - roleName string, - memberName string, -) bool { - for i, binding := range policy.Bindings { - if binding.Role == roleName { - return c.addMemberToBindingForProject(memberName, policy.Bindings[i]) - } - } - - // if we didn't find an existing binding entry, then make one - c.createMemberRoleBindingForProject(policy, roleName, memberName) - - return true -} - -// adds member to existing binding. returns bool indicating if an entry was made -func (c *shim) 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 (c *shim) createMemberRoleBindingForProject( - policy *cloudresourcemanager.Policy, - roleName string, - memberName string, -) { - policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{ - Members: []string{memberName}, - Role: roleName, - }) -} diff --git a/cmd/ocm/gcp/generate-wif-script.go b/cmd/ocm/gcp/generate-wif-script.go index 5a6ca45e..1b4bf166 100644 --- a/cmd/ocm/gcp/generate-wif-script.go +++ b/cmd/ocm/gcp/generate-wif-script.go @@ -68,7 +68,7 @@ func generateCreateScriptCmd(cmd *cobra.Command, argv []string) error { } wifConfig := response.Body() - projectNum, err := gcpClient.ProjectNumberFromId(ctx, wifConfig.Gcp().ProjectId()) + projectNum, err := gcpClient.ProjectNumberFromId(wifConfig.Gcp().ProjectId()) if err != nil { return errors.Wrapf(err, "failed to get project number from id") } diff --git a/cmd/ocm/gcp/iam.go b/cmd/ocm/gcp/iam.go new file mode 100644 index 00000000..d308a71b --- /dev/null +++ b/cmd/ocm/gcp/iam.go @@ -0,0 +1,147 @@ +package gcp + +import ( + "context" + "fmt" + + "cloud.google.com/go/iam/admin/apiv1/adminpb" + "github.com/openshift-online/ocm-cli/pkg/gcp" + + cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1" +) + +// EnsurePolicyBindingsForProject ensures that given roles and member, appropriate binding is added to project. +// Roles should be in the format projects/{project}/roles/{role_id} for custom roles and roles/{role_id} +// for predefined roles. +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. +// Custom roles should follow the format projects/{project}/roles/{role_id}. +func GetRole(gcpClient gcp.GcpClient, roleName string) (*adminpb.Role, error) { + role, err := gcpClient.GetRole(context.TODO(), &adminpb.GetRoleRequest{ + Name: roleName, + }) + return role, err +} + +// CreateRole creates a new role given permissions +func CreateRole(gcpClient gcp.GcpClient, permissions []string, roleTitle, roleId, roleDescription, + projectName string) (*adminpb.Role, error) { + role, err := gcpClient.CreateRole(context.TODO(), &adminpb.CreateRoleRequest{ + Role: &adminpb.Role{ + Title: roleTitle, + 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. +// Custom roles should follow the format projects/{project}/roles/{role_id}. +func UpdateRole(gcpClient gcp.GcpClient, role *adminpb.Role, roleName string) (*adminpb.Role, error) { + updated, err := gcpClient.UpdateRole(context.TODO(), &adminpb.UpdateRoleRequest{ + Name: roleName, + Role: role, + }) + if err != nil { + return nil, err + } + return updated, 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 8095eed2..cf0f0ab4 100644 --- a/cmd/ocm/gcp/scripting.go +++ b/cmd/ocm/gcp/scripting.go @@ -79,8 +79,6 @@ func generateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { // Append the script to create the service accounts scriptContent += createServiceAccountScriptContent(wifConfig, projectNum) - scriptContent += grantSupportAccessScriptContent(wifConfig) - return scriptContent } @@ -188,40 +186,6 @@ func createServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int return sb.String() } -func grantSupportAccessScriptContent(wifConfig *cmv1.WifConfig) string { - var sb strings.Builder - - sb.WriteString("\n# Create roles:\n") - for _, role := range wifConfig.Gcp().Support().Roles() { - if !role.Predefined() { - roleId := strings.ReplaceAll(role.RoleId(), "-", "_") - project := wifConfig.Gcp().ProjectId() - permissions := strings.Join(role.Permissions(), ",") - roleName := roleId - roleDesc := roleDescription + " for WIF config " + wifConfig.DisplayName() - //nolint:lll - sb.WriteString(fmt.Sprintf("gcloud iam roles create %s --project=%s --title=%s --description=\"%s\" --stage=GA --permissions=%s\n", - roleId, project, roleName, roleDesc, permissions)) - } - } - sb.WriteString("\n# Bind support roles:\n") - for _, sa := range wifConfig.Gcp().ServiceAccounts() { - for _, role := range sa.Roles() { - project := wifConfig.Gcp().ProjectId() - member := fmt.Sprintf("group:%s@%s.iam.gserviceaccount.com", wifConfig.Gcp().Support().Principal(), project) - var roleResource string - if role.Predefined() { - roleResource = fmt.Sprintf("roles/%s", role.RoleId()) - } else { - roleResource = fmt.Sprintf("projects/%s/roles/%s", project, role.RoleId()) - } - sb.WriteString(fmt.Sprintf("gcloud projects add-iam-policy-binding %s --member=%s --role=%s\n", - project, member, roleResource)) - } - } - return sb.String() -} - func fmtMembers(sa *cmv1.WifServiceAccount, projectNum int64, poolId string) []string { members := []string{} for _, saName := range sa.CredentialRequest().ServiceAccountNames() { diff --git a/go.mod b/go.mod index 33e25fd5..b13b626b 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/nwidger/jsoncolor v0.3.2 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 - github.com/openshift-online/ocm-sdk-go v0.1.439 + github.com/openshift-online/ocm-sdk-go v0.1.437 github.com/openshift/rosa v1.2.24 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 91084163..392cf96b 100644 --- a/go.sum +++ b/go.sum @@ -361,8 +361,8 @@ github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= -github.com/openshift-online/ocm-sdk-go v0.1.439 h1:ELrJjmYgtzhdUY1cOJ0chtbhBEGz682EiTvojt5/xVM= -github.com/openshift-online/ocm-sdk-go v0.1.439/go.mod h1:CiAu2jwl3ITKOxkeV0Qnhzv4gs35AmpIzVABQLtcI2Y= +github.com/openshift-online/ocm-sdk-go v0.1.437 h1:2xFFOu3lvrrA0wfz4zUmX7UxcWQvqfbgBWelLLB5T2I= +github.com/openshift-online/ocm-sdk-go v0.1.437/go.mod h1:CiAu2jwl3ITKOxkeV0Qnhzv4gs35AmpIzVABQLtcI2Y= github.com/openshift/rosa v1.2.24 h1:vv0yYnWHx6CCPEAau/0rS54P2ksaf+uWXb1TQPWxiYE= github.com/openshift/rosa v1.2.24/go.mod h1:MVXB27O3PF8WoOic23I03mmq6/9kVxpFx6FKyLMCyrQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= diff --git a/pkg/gcp/client.go b/pkg/gcp/client.go index 371734e5..10876f8c 100644 --- a/pkg/gcp/client.go +++ b/pkg/gcp/client.go @@ -2,6 +2,7 @@ package gcp import ( "context" + "encoding/base64" "fmt" "cloud.google.com/go/iam" @@ -13,32 +14,42 @@ import ( cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1" iamv1 "google.golang.org/api/iam/v1" + "google.golang.org/api/iterator" secretmanager "google.golang.org/api/secretmanager/v1" ) type GcpClient interface { - /* - ListServiceAccounts(ctx context.Context, project string, filter func(s string) bool) ([]string, error) //nolint:lll - DeleteRole(context.Context, *adminpb.DeleteRoleRequest) (*adminpb.Role, error) - ListRoles(context.Context, *adminpb.ListRolesRequest) (*adminpb.ListRolesResponse, error) - */ - AttachImpersonator(ctx context.Context, saId, projectId, impersonatorResourceId string) error - AttachWorkloadIdentityPool(ctx context.Context, sa *cmv1.WifServiceAccount, poolId, projectId string) error - CreateRole(context.Context, *adminpb.CreateRoleRequest) (*adminpb.Role, error) - CreateServiceAccount(ctx context.Context, request *adminpb.CreateServiceAccountRequest) (*adminpb.ServiceAccount, error) //nolint:lll - CreateWorkloadIdentityPool(ctx context.Context, parent, poolID string, pool *iamv1.WorkloadIdentityPool) (*iamv1.Operation, error) //nolint:lll + ListServiceAccounts(project string, filter func(s string) bool) ([]string, error) //nolint:lll + + CreateServiceAccount(ctx context.Context, request *adminpb.CreateServiceAccountRequest) (*adminpb.ServiceAccount, error) //nolint:lll + + CreateWorkloadIdentityPool(ctx context.Context, parent, poolID string, pool *iamv1.WorkloadIdentityPool) (*iamv1.Operation, error) //nolint:lll + GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) //nolint:lll + DeleteWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.Operation, error) //nolint:lll + UndeleteWorkloadIdentityPool(ctx context.Context, resource string, request *iamv1.UndeleteWorkloadIdentityPoolRequest) (*iamv1.Operation, error) //nolint:lll + CreateWorkloadIdentityProvider(ctx context.Context, parent, providerID string, provider *iamv1.WorkloadIdentityPoolProvider) (*iamv1.Operation, error) //nolint:lll - DeleteServiceAccount(ctx context.Context, saName string, project string, allowMissing bool) error - DeleteWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.Operation, error) //nolint:lll - GetProjectIamPolicy(ctx context.Context, projectName string, request *cloudresourcemanager.GetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll + GetWorkloadIdentityProvider(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPoolProvider, error) //nolint:lll + + DeleteServiceAccount(saName string, project string, allowMissing bool) error + + GetProjectIamPolicy(projectName string, request *cloudresourcemanager.GetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll + SetProjectIamPolicy(svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll + + AttachImpersonator(saId, projectId, impersonatorResourceId string) error + AttachWorkloadIdentityPool(sa *cmv1.WifServiceAccount, poolId, projectId string) error + + SaveSecret(secretId, projectId string, secretData []byte) error + RetreiveSecret(secretId string, projectId string) ([]byte, error) + + ProjectNumberFromId(projectId string) (int64, error) + GetRole(context.Context, *adminpb.GetRoleRequest) (*adminpb.Role, error) - GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) //nolint:lll - GetWorkloadIdentityProvider(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPoolProvider, error) //nolint:lll - ProjectNumberFromId(ctx context.Context, projectId string) (int64, error) - SetProjectIamPolicy(ctx context.Context, svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll - UndeleteRole(context.Context, *adminpb.UndeleteRoleRequest) (*adminpb.Role, error) - UndeleteWorkloadIdentityPool(ctx context.Context, resource string, request *iamv1.UndeleteWorkloadIdentityPoolRequest) (*iamv1.Operation, error) //nolint:lll + 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 gcpClient struct { @@ -84,10 +95,50 @@ func NewGcpClient(ctx context.Context) (GcpClient, error) { }, nil } -func (c *gcpClient) AttachImpersonator(ctx context.Context, saId, projectId string, impersonatorEmail string) error { +func (c *gcpClient) CreateServiceAccount(ctx context.Context, + request *adminpb.CreateServiceAccountRequest) (*adminpb.ServiceAccount, error) { + svcAcct, err := c.iamClient.CreateServiceAccount(ctx, request) + return svcAcct, err +} + +func (c *gcpClient) DeleteServiceAccount(saName string, project string, allowMissing bool) error { + name := fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", project, saName, project) + err := c.iamClient.DeleteServiceAccount(context.Background(), &adminpb.DeleteServiceAccountRequest{ + Name: name, + }) + if err != nil { + return c.handleDeleteServiceAccountError(err, allowMissing) + } + return nil +} + +func (c *gcpClient) ListServiceAccounts(project string, filter func(string) bool) ([]string, error) { + out := []string{} + // Listing objects follow the iterator pattern specified here: + // https://github.com/googleapis/google-cloud-go/wiki/Iterator-Guidelines + saIterator := c.iamClient.ListServiceAccounts(context.Background(), &adminpb.ListServiceAccountsRequest{ + Name: fmt.Sprintf("projects/%s", project), + // The pagesize can be adjusted for optimized network load. + // PageSize: 5, + }) + for sa, err := saIterator.Next(); err != iterator.Done; sa, err = saIterator.Next() { + if err != nil { + return nil, c.handleListServiceAccountError(err) + } + // Example: + // To list all service accounts: + // filter = func(s string) bool { return true } + if filter(sa.Name) { + out = append(out, sa.Name) + } + } + return out, nil +} + +func (c *gcpClient) AttachImpersonator(saId, projectId string, impersonatorEmail string) error { saResourceId := fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", projectId, saId, projectId) - policy, err := c.iamClient.GetIamPolicy(ctx, &iampb.GetIamPolicyRequest{ + policy, err := c.iamClient.GetIamPolicy(context.Background(), &iampb.GetIamPolicyRequest{ Resource: saResourceId, }) if err != nil { @@ -96,7 +147,7 @@ func (c *gcpClient) AttachImpersonator(ctx context.Context, saId, projectId stri policy.Add( fmt.Sprintf("serviceAccount:%s", impersonatorEmail), iam.RoleName("roles/iam.serviceAccountTokenCreator")) - _, err = c.iamClient.SetIamPolicy(ctx, &iamadmin.SetIamPolicyRequest{ + _, err = c.iamClient.SetIamPolicy(context.Background(), &iamadmin.SetIamPolicyRequest{ Resource: saResourceId, Policy: policy, }) @@ -106,20 +157,15 @@ func (c *gcpClient) AttachImpersonator(ctx context.Context, saId, projectId stri return nil } -func (c *gcpClient) AttachWorkloadIdentityPool( - ctx context.Context, - sa *cmv1.WifServiceAccount, - poolId string, - projectId string, -) error { +func (c *gcpClient) AttachWorkloadIdentityPool(sa *cmv1.WifServiceAccount, poolId, projectId string) error { saResourceId := c.fmtSaResourceId(sa.ServiceAccountId(), projectId) - projectNum, err := c.ProjectNumberFromId(ctx, projectId) + projectNum, err := c.ProjectNumberFromId(projectId) if err != nil { return c.handleAttachWorkloadIdentityPoolError(err) } - policy, err := c.iamClient.GetIamPolicy(ctx, &iampb.GetIamPolicyRequest{ + policy, err := c.iamClient.GetIamPolicy(context.Background(), &iampb.GetIamPolicyRequest{ Resource: saResourceId, }) if err != nil { @@ -134,7 +180,7 @@ func (c *gcpClient) AttachWorkloadIdentityPool( ), iam.RoleName("roles/iam.workloadIdentityUser")) } - _, err = c.iamClient.SetIamPolicy(ctx, &iamadmin.SetIamPolicyRequest{ + _, err = c.iamClient.SetIamPolicy(context.Background(), &iamadmin.SetIamPolicyRequest{ Resource: saResourceId, Policy: policy, }) @@ -144,16 +190,89 @@ func (c *gcpClient) AttachWorkloadIdentityPool( return nil } -func (c *gcpClient) CreateRole(ctx context.Context, request *adminpb.CreateRoleRequest) (*adminpb.Role, error) { - return c.iamClient.CreateRole(ctx, request) +// - secretResource: The resource name of the secret is in the format +// `projects/*/secrets/*` +// - secretData: Can be anything. +func (c *gcpClient) SaveSecret(secretName, secretProject string, secretData []byte) error { + _, err := c.secretManager.Projects.Secrets.Create("projects/"+secretProject, &secretmanager.Secret{ + // This is an undocumented required field. + // https://github.com/hashicorp/terraform-provider-google/issues/11395 + Replication: &secretmanager.Replication{Automatic: &secretmanager.Automatic{}}, + }).SecretId(secretName).Do() + if err != nil { + err = c.handleSaveSecretError(err) + if err != nil { + return err + } + } + _, err = c.secretManager.Projects.Locations.Secrets.AddVersion( + fmt.Sprintf("projects/%s/secrets/%s", secretProject, secretName), + &secretmanager.AddSecretVersionRequest{ + Payload: &secretmanager.SecretPayload{ + Data: base64.StdEncoding.EncodeToString(secretData), + }, + }).Do() + if err != nil { + return c.handleSaveSecretError(err) + } + return nil } -func (c *gcpClient) CreateServiceAccount( - ctx context.Context, - request *adminpb.CreateServiceAccountRequest, -) (*adminpb.ServiceAccount, error) { - svcAcct, err := c.iamClient.CreateServiceAccount(ctx, request) - return svcAcct, err +// - name: The resource name of the secret is in the format +// `projects/*/secrets/*/versions/*` or +// `projects/*/locations/*/secrets/*/versions/*`. +// `projects/*/secrets/*/versions/latest` or +// `projects/*/locations/*/secrets/*/versions/latest` is an alias to +// the most recently created SecretVersion. +func (c *gcpClient) RetreiveSecret(secretId string, projectId string) ([]byte, error) { + secretResource := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", projectId, secretId) + resp, err := c.secretManager.Projects.Secrets.Versions.Access(secretResource).Do() + if err != nil { + c.handleRetrieveSecretError(err) + } + return base64.StdEncoding.DecodeString(resp.Payload.Data) +} + +type WorkloadIdentityPoolSpec struct { + Audience []string + IssuerUrl string + PoolName string + ProjectId string + Jwks string + PoolIdentityProviderId string +} + +func (c *gcpClient) CreateWorkloadIdentityPool2(spec WorkloadIdentityPoolSpec) error { + // Note: The parent parameter should be in the following format: + // projects/*/locations/* + // https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools/create + if _, err := c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Create( + fmt.Sprintf("projects/%s/locations/global", spec.ProjectId), &iamv1.WorkloadIdentityPool{ + DisplayName: spec.PoolName, + Description: "Workload Identity pool created by prototype", + }).WorkloadIdentityPoolId(spec.PoolName).Do(); err != nil { + if err != nil { + return err + } + } + if _, err := c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Providers.Create( + fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", spec.ProjectId, spec.PoolName), + &iamv1.WorkloadIdentityPoolProvider{ + AttributeMapping: map[string]string{ + "google.subject": "assertion.sub", + }, + Description: "Identity Provider created by prototype", + Oidc: &iamv1.Oidc{ + AllowedAudiences: []string{ + "openshift", + }, + IssuerUri: spec.IssuerUrl, + JwksJson: spec.Jwks, + }, + }).WorkloadIdentityPoolProviderId(spec.PoolIdentityProviderId).Do(); err != nil { + return err + } + return nil } //nolint:lll @@ -162,19 +281,8 @@ func (c *gcpClient) CreateWorkloadIdentityPool(ctx context.Context, parent, pool } //nolint:lll -func (c *gcpClient) CreateWorkloadIdentityProvider(ctx context.Context, parent, providerID string, provider *iamv1.WorkloadIdentityPoolProvider) (*iamv1.Operation, error) { - return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Providers.Create(parent, provider).WorkloadIdentityPoolProviderId(providerID).Context(ctx).Do() -} - -func (c *gcpClient) DeleteServiceAccount(ctx context.Context, saName string, project string, allowMissing bool) error { - name := fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", project, saName, project) - err := c.iamClient.DeleteServiceAccount(ctx, &adminpb.DeleteServiceAccountRequest{ - Name: name, - }) - if err != nil { - return c.handleDeleteServiceAccountError(err, allowMissing) - } - return nil +func (c *gcpClient) GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) { + return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Get(resource).Context(ctx).Do() } //nolint:lll @@ -183,21 +291,13 @@ func (c *gcpClient) DeleteWorkloadIdentityPool(ctx context.Context, resource str } //nolint:lll -func (c *gcpClient) GetProjectIamPolicy( - ctx context.Context, - projectName string, - request *cloudresourcemanager.GetIamPolicyRequest, -) (*cloudresourcemanager.Policy, error) { - return c.cloudResourceManager.Projects.GetIamPolicy(projectName, 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) UndeleteWorkloadIdentityPool(ctx context.Context, resource string, request *iamv1.UndeleteWorkloadIdentityPoolRequest) (*iamv1.Operation, error) { + return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Undelete(resource, request).Context(ctx).Do() } //nolint:lll -func (c *gcpClient) GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) { - return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Get(resource).Context(ctx).Do() +func (c *gcpClient) CreateWorkloadIdentityProvider(ctx context.Context, parent, providerID string, provider *iamv1.WorkloadIdentityPoolProvider) (*iamv1.Operation, error) { + return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Providers.Create(parent, provider).WorkloadIdentityPoolProviderId(providerID).Context(ctx).Do() } //nolint:lll @@ -205,7 +305,7 @@ func (c *gcpClient) GetWorkloadIdentityProvider(ctx context.Context, resource st return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Providers.Get(resource).Context(ctx).Do() } -func (c *gcpClient) ProjectNumberFromId(ctx context.Context, projectId string) (int64, error) { +func (c *gcpClient) ProjectNumberFromId(projectId string) (int64, error) { project, err := c.cloudResourceManager.Projects.Get(projectId).Do() if err != nil { return 0, err @@ -214,19 +314,36 @@ func (c *gcpClient) ProjectNumberFromId(ctx context.Context, projectId string) ( } //nolint:lll -func (c *gcpClient) SetProjectIamPolicy(ctx context.Context, svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) { - return c.cloudResourceManager.Projects.SetIamPolicy(svcAcctResource, request).Context(ctx).Do() +func (c *gcpClient) GetProjectIamPolicy(projectName string, request *cloudresourcemanager.GetIamPolicyRequest) (*cloudresourcemanager.Policy, error) { + return c.cloudResourceManager.Projects.GetIamPolicy(projectName, request).Context(context.Background()).Do() } -func (c *gcpClient) UndeleteRole(ctx context.Context, request *adminpb.UndeleteRoleRequest) (*adminpb.Role, error) { - return c.iamClient.UndeleteRole(ctx, request) +//nolint:lll +func (c *gcpClient) SetProjectIamPolicy(svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) { + return c.cloudResourceManager.Projects.SetIamPolicy(svcAcctResource, request).Context(context.Background()).Do() } -//nolint:lll -func (c *gcpClient) UndeleteWorkloadIdentityPool(ctx context.Context, resource string, request *iamv1.UndeleteWorkloadIdentityPoolRequest) (*iamv1.Operation, error) { - return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Undelete(resource, request).Context(ctx).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) +} + +//nolint:lll +func (c *gcpClient) ListRoles(ctx context.Context, request *adminpb.ListRolesRequest) (*adminpb.ListRolesResponse, error) { + return c.iamClient.ListRoles(ctx, request) +} diff --git a/pkg/gcp/error_handlers.go b/pkg/gcp/error_handlers.go index a8d6bd2d..8d8dc423 100644 --- a/pkg/gcp/error_handlers.go +++ b/pkg/gcp/error_handlers.go @@ -2,8 +2,10 @@ package gcp import ( "fmt" + "net/http" "github.com/googleapis/gax-go/v2/apierror" + "google.golang.org/api/googleapi" "google.golang.org/grpc/codes" ) @@ -24,6 +26,14 @@ func (c *gcpClient) handleAttachWorkloadIdentityPoolError(err error) error { return fmt.Errorf(pApiError.Error()) } +func (c *gcpClient) handleListServiceAccountError(err error) error { + pApiError, ok := err.(*apierror.APIError) + if !ok { + return fmt.Errorf("Unexpected error") + } + return fmt.Errorf(pApiError.Details().String()) +} + func (c *gcpClient) handleDeleteServiceAccountError(err error, allowMissing bool) error { pApiError, ok := err.(*apierror.APIError) if !ok { @@ -34,3 +44,24 @@ func (c *gcpClient) handleDeleteServiceAccountError(err error, allowMissing bool } return fmt.Errorf(pApiError.Details().String()) } + +func (c *gcpClient) handleRetrieveSecretError(err error) ([]byte, error) { + gApiError, ok := err.(*googleapi.Error) + if !ok { + return []byte{}, fmt.Errorf("Unexpected error") + } + return []byte{}, gApiError +} + +// Errors that can't be converted to *googleapi.Error are unexpected +// If the secret already exists, this is not considered an error +func (c *gcpClient) handleSaveSecretError(err error) error { + gApiError, ok := err.(*googleapi.Error) + if !ok { + return fmt.Errorf("Unexpected error") + } + if gApiError.Code == http.StatusConflict { + return nil + } + return gApiError +}