...

Source file src/edge-infra.dev/pkg/edge/api/services/iam_service.go

Documentation: edge-infra.dev/pkg/edge/api/services

     1  package services
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/rs/zerolog/log"
    10  	"google.golang.org/api/cloudresourcemanager/v1"
    11  	"google.golang.org/api/googleapi"
    12  	"google.golang.org/api/iam/v1"
    13  	"google.golang.org/api/option"
    14  	"k8s.io/apimachinery/pkg/util/wait"
    15  	"k8s.io/client-go/util/retry"
    16  
    17  	edgeiam "edge-infra.dev/pkg/lib/gcp/iam"
    18  )
    19  
    20  type IAMService interface {
    21  	CreateServiceAccount(ctx context.Context, projectID, serviceAccountName, displayName, description string) (*iam.ServiceAccount, error)
    22  	CreatePolicyBinding(ctx context.Context, serviceAccountProjectID, policyProjectID, serviceAccountName string, roles []IAMRole) (*cloudresourcemanager.Policy, error)
    23  	CreateSAKey(ctx context.Context, projectID string, serviceAccount *iam.ServiceAccount) (*iam.ServiceAccountKey, error)
    24  	CreateSAandSAKey(ctx context.Context, ProjectID, serviceAccountName, displayName, description string) (*edgeiam.Component, error)
    25  	CreateOrUpdateEdgeIAM(ctx context.Context, serviceAccountProjectID, policyProjectID, serviceAccountName string, componentIAM *edgeiam.Component, roles []IAMRole) (*edgeiam.Component, error)
    26  }
    27  
    28  type iamService struct {
    29  	opts []option.ClientOption
    30  	sync.Mutex
    31  }
    32  
    33  type IAMRole struct {
    34  	name        string
    35  	roleType    string
    36  	wiNameSpace string
    37  	wiK8sSA     string
    38  }
    39  
    40  const (
    41  	WIType      = "wiUser"
    42  	GenericType = "generic"
    43  )
    44  
    45  func NewIamRole(name string) IAMRole {
    46  	return IAMRole{
    47  		name:        name,
    48  		roleType:    GenericType,
    49  		wiNameSpace: "",
    50  		wiK8sSA:     "",
    51  	}
    52  }
    53  
    54  func NewWIIamRole(name, ns, k8sSAName string) IAMRole {
    55  	return IAMRole{
    56  		name:        name,
    57  		roleType:    WIType,
    58  		wiNameSpace: ns,
    59  		wiK8sSA:     k8sSAName,
    60  	}
    61  }
    62  
    63  func (r IAMRole) getMemberName(projectID, name string) string {
    64  	memberstr := edgeiam.StandardSvcAccountMember(name, projectID)
    65  	if r.roleType == WIType {
    66  		memberstr = edgeiam.WorkloadIdentityMember(projectID, r.wiNameSpace, r.wiK8sSA)
    67  	}
    68  	return memberstr
    69  }
    70  
    71  var (
    72  	defaultRetry = wait.Backoff{
    73  		Duration: 500 * time.Millisecond, // every .5 seconds
    74  		Steps:    10,                     // 10 times
    75  		Factor:   1.5,
    76  		Jitter:   0.1,
    77  	}
    78  )
    79  
    80  // CreateServiceAccount calls iam package to create service account.
    81  func (s *iamService) CreateServiceAccount(ctx context.Context, projectID, serviceAccountName, displayName, description string) (*iam.ServiceAccount, error) {
    82  	iamservice, err := edgeiam.NewIAMService(ctx, s.opts...)
    83  	project := fmt.Sprintf("projects/%s", projectID)
    84  	if err != nil {
    85  		log.Ctx(ctx).Err(fmt.Errorf("failed to init iamservice & failed to create configconnectormeta resource name: %s %s %w", project, serviceAccountName, err)).
    86  			Msg("create service account failed")
    87  		return nil, err
    88  	}
    89  
    90  	//create service account
    91  	serviceAccount := edgeiam.NewServiceAccount(displayName, description)
    92  	serviceAccountRequest := edgeiam.NewServiceAccountRequest(serviceAccountName, serviceAccount)
    93  	serviceAccount, err = iamservice.CreateServiceAccount(ctx, project, serviceAccountRequest)
    94  	if err != nil {
    95  		log.Ctx(ctx).Err(fmt.Errorf("failed to create service account: %w", err)).
    96  			Msg("create service account failed")
    97  		if errorIs409(err) {
    98  			return handleErrorIs409(ctx, serviceAccountName, projectID, project, iamservice)
    99  		}
   100  		return nil, err
   101  	}
   102  	return serviceAccount, nil
   103  }
   104  
   105  func handleErrorIs409(ctx context.Context, name, projectID, project string, iamservice *edgeiam.IAMService) (*iam.ServiceAccount, error) {
   106  	saEmail := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", name, projectID)
   107  	projectName := fmt.Sprintf("%s/serviceAccounts/%s", project, saEmail)
   108  	sa, err := iamservice.GetServiceAccount(ctx, projectName)
   109  	if err != nil {
   110  		log.Ctx(ctx).Err(fmt.Errorf("SA not found: %s, %w", projectName, err)).
   111  			Msg("getting service account failed")
   112  		return nil, err
   113  	}
   114  	saKeyProjectID := fmt.Sprintf("%s/serviceAccounts/%s", project, sa.Email)
   115  	err = cleanupSAKeys(ctx, iamservice, saKeyProjectID)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	return sa, nil
   120  }
   121  
   122  func cleanupSAKeys(ctx context.Context, iamservice *edgeiam.IAMService, saKeyProjectID string) error {
   123  	if err := iamservice.DeleteExcessServiceAccountKeys(ctx, saKeyProjectID); err != nil {
   124  		if errorIs404(err) {
   125  			log.Ctx(ctx).Err(fmt.Errorf("SA keys not found: %s, %w", saKeyProjectID, err)).
   126  				Msg("deleting service account key failed")
   127  		} else {
   128  			log.Ctx(ctx).Err(err).Msg("deleting service account key failed")
   129  			return err
   130  		}
   131  	}
   132  	return nil
   133  }
   134  
   135  func (s *iamService) CreateSAKey(ctx context.Context, projectID string, serviceAccount *iam.ServiceAccount) (*iam.ServiceAccountKey, error) {
   136  	iamservice, err := edgeiam.NewIAMService(ctx, s.opts...)
   137  	if err != nil {
   138  		log.Ctx(ctx).Err(fmt.Errorf("failed to init iamservice: %s %w", projectID, err)).
   139  			Msg("create service account failed")
   140  		return nil, err
   141  	}
   142  
   143  	project := fmt.Sprintf("projects/%s", projectID)
   144  	//create service account api key
   145  	serviceAccountKeyRequest := edgeiam.NewServiceAccountKeyRequest()
   146  	//projects/{PROJECT_ID}/serviceAccounts/{ACCOUNT} where account is sa email.
   147  	saKeyProjectID := fmt.Sprintf("%s/serviceAccounts/%s", project, serviceAccount.Email)
   148  	serviceAccountKey, err := iamservice.CreateServiceAccountKey(ctx, saKeyProjectID, serviceAccountKeyRequest)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	return serviceAccountKey, nil
   153  }
   154  
   155  // CreatePolicyBinding calls iam package to create policy binding with provided roles.
   156  func (s *iamService) CreatePolicyBinding(ctx context.Context, serviceAccountProjectID, policyProjectID, serviceAccountName string, roles []IAMRole) (*cloudresourcemanager.Policy, error) {
   157  	modifiedIamPolicy := &cloudresourcemanager.Policy{}
   158  	if len(roles) > 0 {
   159  		crmService, err := edgeiam.NewCRMService(ctx, s.opts...)
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  		iampolicy, err := crmService.GetPolicy(ctx, policyProjectID)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  		bindingRoles := make(map[string]int)
   168  		for index, binding := range iampolicy.Bindings {
   169  			bindingRoles[binding.Role] = index
   170  		}
   171  		iampolicy = addBindingsToIAMPolicy(roles, serviceAccountName, serviceAccountProjectID, bindingRoles, iampolicy)
   172  		modifiedIamPolicy, err = crmService.SetPolicy(ctx, policyProjectID, iampolicy)
   173  		if err != nil {
   174  			return nil, err
   175  		}
   176  	}
   177  	return modifiedIamPolicy, nil
   178  }
   179  
   180  func addBindingsToIAMPolicy(roles []IAMRole, name, projectID string, bindingRoles map[string]int, iampolicy *cloudresourcemanager.Policy) *cloudresourcemanager.Policy {
   181  	for _, role := range roles {
   182  		memberstr := role.getMemberName(projectID, name)
   183  		if value, exists := bindingRoles[role.name]; exists {
   184  			iampolicy.Bindings[value].Members = append(iampolicy.Bindings[value].Members, memberstr)
   185  		} else {
   186  			policyBinding := edgeiam.CreatePolicyBinding(role.name, []string{memberstr})
   187  			iampolicy.Bindings = append(iampolicy.Bindings, policyBinding)
   188  		}
   189  	}
   190  	return iampolicy
   191  }
   192  
   193  func (s *iamService) CreateSAandSAKey(ctx context.Context, projectID, serviceAccountName, displayName, description string) (*edgeiam.Component, error) {
   194  	s.Lock()
   195  	defer s.Unlock()
   196  	sa, err := s.CreateServiceAccount(ctx, projectID, serviceAccountName, displayName, description)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	var saKey *iam.ServiceAccountKey
   201  	// We retry on 409 conflict, 400 bad request, and 404 not found
   202  	err = retry.OnError(defaultRetry, func(err error) bool { return isErrorCode(err, 409, 400, 404) }, func() error {
   203  		saKey, err = s.CreateSAKey(ctx, projectID, sa)
   204  		if err != nil {
   205  			log.Ctx(ctx).Err(fmt.Errorf("failed to create sa key: %s %v", projectID, sa)).
   206  				Msg("create service account failed. Will retry.")
   207  		}
   208  		return err
   209  	})
   210  	if err != nil {
   211  		log.Ctx(ctx).Err(fmt.Errorf("failed to create sa key: %s %v", projectID, sa)).
   212  			Msg("create service account failed. Will NOT retry.")
   213  		return nil, err
   214  	}
   215  	return edgeiam.NewComponent(saKey, sa, nil), nil
   216  }
   217  
   218  func (s *iamService) CreateOrUpdateEdgeIAM(ctx context.Context, serviceAccountProjectID, policyProjectID, serviceAccountName string, componentIAM *edgeiam.Component, roles []IAMRole) (*edgeiam.Component, error) {
   219  	s.Lock()
   220  	defer s.Unlock()
   221  	var (
   222  		modifiedIamPolicy *cloudresourcemanager.Policy
   223  		err               error
   224  	)
   225  	// We retry on 409 conflict, 400 bad request, and 404 not found
   226  	err = retry.OnError(defaultRetry, func(err error) bool { return isErrorCode(err, 409, 400, 404) }, func() error {
   227  		modifiedIamPolicy, err = s.CreatePolicyBinding(ctx, serviceAccountProjectID, policyProjectID, serviceAccountName, roles)
   228  		if err != nil {
   229  			log.Ctx(ctx).Err(fmt.Errorf("failed to update iam policy: %v", err)).
   230  				Msg("update iam policy failed due to etag conflict. Will retry.")
   231  		}
   232  		return err
   233  	})
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  	return edgeiam.NewComponent(componentIAM.APIKey, componentIAM.ServiceAccount, modifiedIamPolicy), nil
   238  }
   239  
   240  func isErrorCode(err error, codes ...int) bool {
   241  	if e, ok := err.(*googleapi.Error); ok {
   242  		for _, code := range codes {
   243  			if e.Code == code {
   244  				return true
   245  			}
   246  		}
   247  	}
   248  	return false
   249  }
   250  
   251  // errorIs409 checks the returned gcp error code and returns true if it is a 409 alreadyExists.
   252  func errorIs409(err error) bool {
   253  	if e, ok := err.(*googleapi.Error); ok {
   254  		return e.Code == 409
   255  	}
   256  	return false
   257  }
   258  
   259  // errorIs404 checks the returned gcp error code and returns true if it is a 404 not found.
   260  func errorIs404(err error) bool {
   261  	if e, ok := err.(*googleapi.Error); ok {
   262  		return e.Code == 404
   263  	}
   264  	return false
   265  }
   266  
   267  // NewIAMService creates a new IAM Service.
   268  func NewIAMService(opts ...option.ClientOption) IAMService { //nolint
   269  	return &iamService{
   270  		opts: opts,
   271  	}
   272  }
   273  

View as plain text