github.com/navikt/knorten@v0.0.0-20240419132333-1333f46ed8b6/pkg/team/gcp.go (about)

     1  package team
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  
     9  	secretmanager "cloud.google.com/go/secretmanager/apiv1"
    10  	"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
    11  	"github.com/navikt/knorten/pkg/database/gensql"
    12  	"github.com/navikt/knorten/pkg/gcp"
    13  	"github.com/navikt/knorten/pkg/k8s"
    14  	"google.golang.org/api/googleapi"
    15  	iamv1 "google.golang.org/api/iam/v1"
    16  )
    17  
    18  func (c Client) createGCPTeamResources(ctx context.Context, team gensql.Team) error {
    19  	if c.dryRun {
    20  		return nil
    21  	}
    22  
    23  	sa, err := c.createIAMServiceAccount(ctx, team.ID)
    24  	if err != nil {
    25  		return err
    26  	}
    27  
    28  	secret, err := c.createSecret(ctx, team.Slug, team.ID)
    29  	if err != nil {
    30  		return err
    31  	}
    32  
    33  	if err := c.createServiceAccountSecretAccessorBinding(ctx, sa.Email, secret.Name); err != nil {
    34  		return err
    35  	}
    36  
    37  	if err := gcp.SetUsersSecretOwnerBinding(ctx, team.Users, secret.Name); err != nil {
    38  		return err
    39  	}
    40  
    41  	if err := c.createSAWorkloadIdentityBinding(ctx, sa.Email, team.ID); err != nil {
    42  		return err
    43  	}
    44  
    45  	return nil
    46  }
    47  
    48  func (c Client) createSAWorkloadIdentityBinding(ctx context.Context, email, teamID string) error {
    49  	service, err := iamv1.NewService(ctx)
    50  	if err != nil {
    51  		return err
    52  	}
    53  
    54  	resource := fmt.Sprintf("projects/%v/serviceAccounts/%v", c.gcpProject, email)
    55  
    56  	policy, err := service.Projects.ServiceAccounts.GetIamPolicy(resource).Do()
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	namespace := k8s.TeamIDToNamespace(teamID)
    62  	bindings := policy.Bindings
    63  	if !c.updateRoleBindingIfExists(bindings, "roles/iam.workloadIdentityUser", namespace, teamID) {
    64  		bindings = append(bindings, &iamv1.Binding{
    65  			Members: []string{fmt.Sprintf("serviceAccount:%v.svc.id.goog[%v/%v]", c.gcpProject, namespace, teamID)},
    66  			Role:    "roles/iam.workloadIdentityUser",
    67  		})
    68  	}
    69  
    70  	_, err = service.Projects.ServiceAccounts.SetIamPolicy(resource, &iamv1.SetIamPolicyRequest{
    71  		Policy: &iamv1.Policy{
    72  			Bindings: bindings,
    73  		},
    74  	}).Do()
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	return nil
    80  }
    81  
    82  func (c Client) updateRoleBindingIfExists(bindings []*iamv1.Binding, role, namespace, team string) bool {
    83  	for _, binding := range bindings {
    84  		if binding.Role == role {
    85  			binding.Members = append(binding.Members, fmt.Sprintf("serviceAccount:%v.svc.id.goog[%v/%v]", c.gcpProject, namespace, team))
    86  			return true
    87  		}
    88  	}
    89  
    90  	return false
    91  }
    92  
    93  func (c Client) createSecret(ctx context.Context, slug, teamID string) (*secretmanagerpb.Secret, error) {
    94  	return gcp.CreateSecret(ctx, c.gcpProject, c.gcpRegion, teamID, map[string]string{"team": slug})
    95  }
    96  
    97  func (c Client) createServiceAccountSecretAccessorBinding(ctx context.Context, sa, secret string) error {
    98  	client, err := secretmanager.NewClient(ctx)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	defer client.Close()
   103  
   104  	handle := client.IAM(secret)
   105  	policy, err := handle.Policy(ctx)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	policy.Add("serviceAccount:"+sa, "roles/secretmanager.secretAccessor")
   111  	if err = handle.SetPolicy(ctx, policy); err != nil {
   112  		return err
   113  	}
   114  
   115  	return nil
   116  }
   117  
   118  func (c Client) createIAMServiceAccount(ctx context.Context, team string) (*iamv1.ServiceAccount, error) {
   119  	service, err := iamv1.NewService(ctx)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	request := &iamv1.CreateServiceAccountRequest{
   125  		AccountId: team,
   126  		ServiceAccount: &iamv1.ServiceAccount{
   127  			DisplayName: fmt.Sprintf("Service account for team %v", team),
   128  		},
   129  	}
   130  
   131  	account, err := service.Projects.ServiceAccounts.Create("projects/"+c.gcpProject, request).Do()
   132  	if err != nil {
   133  		var gError *googleapi.Error
   134  		ok := errors.As(err, &gError)
   135  		if ok {
   136  			if gError.Code == http.StatusConflict {
   137  				serviceAccountName := fmt.Sprintf("projects/%v/serviceAccounts/%v@%v.iam.gserviceaccount.com", c.gcpProject, team, c.gcpProject)
   138  				return service.Projects.ServiceAccounts.Get(serviceAccountName).Do()
   139  			}
   140  		}
   141  
   142  		return nil, err
   143  	}
   144  
   145  	return account, nil
   146  }
   147  
   148  func (c Client) updateGCPTeamResources(ctx context.Context, team gensql.Team) error {
   149  	if c.dryRun {
   150  		return nil
   151  	}
   152  
   153  	if err := c.createServiceAccountSecretAccessorBinding(ctx, fmt.Sprintf("%v@%v.iam.gserviceaccount.com", team.ID, c.gcpProject), fmt.Sprintf("projects/%v/secrets/%v", c.gcpProject, team.ID)); err != nil {
   154  		return err
   155  	}
   156  
   157  	return gcp.SetUsersSecretOwnerBinding(ctx, team.Users, fmt.Sprintf("projects/%v/secrets/%v", c.gcpProject, team.ID))
   158  }
   159  
   160  func (c Client) deleteGCPTeamResources(ctx context.Context, teamID string) error {
   161  	if c.dryRun {
   162  		return nil
   163  	}
   164  
   165  	if err := c.deleteIAMServiceAccount(ctx, teamID); err != nil {
   166  		return err
   167  	}
   168  
   169  	if err := gcp.DeleteSecret(ctx, c.gcpProject, teamID); err != nil {
   170  		return err
   171  	}
   172  
   173  	return nil
   174  }
   175  
   176  func (c Client) deleteIAMServiceAccount(ctx context.Context, teamID string) error {
   177  	service, err := iamv1.NewService(ctx)
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	sa := fmt.Sprintf("projects/%v/serviceAccounts/%v@%v.iam.gserviceaccount.com", c.gcpProject, teamID, c.gcpProject)
   183  	_, err = service.Projects.ServiceAccounts.Delete(sa).Do()
   184  	if err != nil {
   185  		var apiError *googleapi.Error
   186  		ok := errors.As(err, &apiError)
   187  		if ok && apiError.Code == http.StatusNotFound {
   188  			return nil
   189  		}
   190  
   191  		return err
   192  	}
   193  
   194  	return nil
   195  }