Skip to content

Commit

Permalink
Support custom roles
Browse files Browse the repository at this point in the history
  • Loading branch information
JakobGray committed Jun 24, 2024
1 parent 6040f8b commit c11da5d
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 79 deletions.
111 changes: 35 additions & 76 deletions cmd/ocm/gcp/create-wif-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"os"
"path/filepath"
"reflect"
"strings"

"github.com/googleapis/gax-go/v2/apierror"
Expand All @@ -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"
Expand All @@ -38,6 +38,7 @@ var (

const (
poolDescription = "Created by the OLM CLI"
roleDescription = "Created by the OLM CLI"

openShiftAudience = "openshift"
)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
18 changes: 16 additions & 2 deletions cmd/ocm/gcp/delete-wif-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
}
Expand Down
5 changes: 4 additions & 1 deletion cmd/ocm/gcp/generate-wif-script.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
144 changes: 144 additions & 0 deletions cmd/ocm/gcp/iam.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit c11da5d

Please sign in to comment.