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 }