1package gcpsecrets 2 3import ( 4 "context" 5 "crypto/sha256" 6 "encoding/base64" 7 "fmt" 8 9 "github.com/hashicorp/errwrap" 10 "github.com/hashicorp/go-gcp-common/gcputil" 11 "github.com/hashicorp/go-multierror" 12 "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/iamutil" 13 "github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util" 14 "github.com/hashicorp/vault/sdk/helper/useragent" 15 "github.com/hashicorp/vault/sdk/logical" 16 "google.golang.org/api/googleapi" 17 "google.golang.org/api/iam/v1" 18) 19 20const ( 21 flagCanDeleteServiceAccount = true 22 flagMustKeepServiceAccount = false 23) 24 25type ( 26 // gcpAccountResources is a wrapper around the GCP resources Vault creates to generate credentials. 27 // This includes a Vault-managed GCP service account (required), IAM bindings, and/or key via TokenGenerator 28 // (for generating access tokens). 29 gcpAccountResources struct { 30 accountId gcputil.ServiceAccountId 31 bindings ResourceBindings 32 tokenGen *TokenGenerator 33 } 34 35 // ResourceBindings represent a map of GCP resource name to IAM roles to be bound on that resource. 36 ResourceBindings map[string]util.StringSet 37 38 // TokenGenerator wraps the service account key and params required to create access tokens. 39 TokenGenerator struct { 40 KeyName string 41 B64KeyJSON string 42 Scopes []string 43 } 44) 45 46func (rb ResourceBindings) asOutput() map[string][]string { 47 out := make(map[string][]string, len(rb)) 48 for k, v := range rb { 49 out[k] = v.ToSlice() 50 } 51 return out 52} 53 54func (rb ResourceBindings) sub(toRemove ResourceBindings) ResourceBindings { 55 subbed := make(ResourceBindings) 56 for r, iamRoles := range rb { 57 toRemoveIamRoles, ok := toRemove[r] 58 if ok { 59 iamRoles = iamRoles.Sub(toRemoveIamRoles) 60 } 61 subbed[r] = iamRoles 62 } 63 return subbed 64} 65 66func getStringHash(bindingsRaw string) string { 67 ssum := sha256.Sum256([]byte(bindingsRaw)) 68 return base64.StdEncoding.EncodeToString(ssum[:]) 69} 70 71func (b *backend) createNewTokenGen(ctx context.Context, req *logical.Request, parent string, scopes []string) (*TokenGenerator, error) { 72 b.Logger().Debug("creating new TokenGenerator (service account key)", "account", parent, "scopes", scopes) 73 74 iamAdmin, err := b.IAMAdminClient(req.Storage) 75 if err != nil { 76 return nil, err 77 } 78 79 key, err := iamAdmin.Projects.ServiceAccounts.Keys.Create( 80 parent, 81 &iam.CreateServiceAccountKeyRequest{ 82 PrivateKeyType: privateKeyTypeJson, 83 }).Context(ctx).Do() 84 if err != nil { 85 return nil, err 86 } 87 return &TokenGenerator{ 88 KeyName: key.Name, 89 B64KeyJSON: key.PrivateKeyData, 90 Scopes: scopes, 91 }, nil 92} 93 94func (b *backend) createIamBindings(ctx context.Context, req *logical.Request, saEmail string, binds ResourceBindings) error { 95 b.Logger().Debug("creating IAM bindings", "account_email", saEmail, "bindings", binds) 96 httpC, err := b.HTTPClient(req.Storage) 97 if err != nil { 98 return err 99 } 100 apiHandle := iamutil.GetApiHandle(httpC, useragent.String()) 101 102 for resourceName, roles := range binds { 103 b.Logger().Debug("setting IAM binding", "resource", resourceName, "roles", roles) 104 resource, err := b.resources.Parse(resourceName) 105 if err != nil { 106 return err 107 } 108 109 b.Logger().Debug("getting IAM policy for resource name", "name", resourceName) 110 p, err := resource.GetIamPolicy(ctx, apiHandle) 111 if err != nil { 112 return nil 113 } 114 115 b.Logger().Debug("got IAM policy for resource name", "name", resourceName) 116 changed, newP := p.AddBindings(&iamutil.PolicyDelta{ 117 Roles: roles, 118 Email: saEmail, 119 }) 120 if !changed || newP == nil { 121 continue 122 } 123 124 b.Logger().Debug("setting IAM policy for resource name", "name", resourceName) 125 if _, err := resource.SetIamPolicy(ctx, apiHandle, newP); err != nil { 126 return errwrap.Wrapf(fmt.Sprintf("unable to set IAM policy for resource %q: {{err}}", resourceName), err) 127 } 128 } 129 130 return nil 131} 132 133func (b *backend) createServiceAccount(ctx context.Context, req *logical.Request, project, saName, descriptor string) (*iam.ServiceAccount, error) { 134 createSaReq := &iam.CreateServiceAccountRequest{ 135 AccountId: saName, 136 ServiceAccount: &iam.ServiceAccount{ 137 DisplayName: roleSetServiceAccountDisplayName(descriptor), 138 }, 139 } 140 141 b.Logger().Debug("creating service account", 142 "project", project, 143 "request", createSaReq) 144 145 iamAdmin, err := b.IAMAdminClient(req.Storage) 146 if err != nil { 147 return nil, err 148 } 149 150 return iamAdmin.Projects.ServiceAccounts.Create(fmt.Sprintf("projects/%s", project), createSaReq).Context(ctx).Do() 151} 152 153// tryDeleteGcpAccountResources creates WALs to clean up a service account's 154// bindings, key, and account (if removeServiceAccount is true) 155func (b *backend) tryDeleteGcpAccountResources(ctx context.Context, req *logical.Request, boundResources *gcpAccountResources, removeServiceAccount bool, walIds []string) []string { 156 if boundResources == nil { 157 b.Logger().Debug("skip deletion for nil roleset resources") 158 return nil 159 } 160 161 b.Logger().Debug("try to delete GCP account resources", "bound_resources", boundResources, "remove_service_account", removeServiceAccount) 162 163 iamAdmin, err := b.IAMAdminClient(req.Storage) 164 if err != nil { 165 return []string{err.Error()} 166 } 167 168 warnings := make([]string, 0) 169 if boundResources.tokenGen != nil { 170 if err := b.deleteTokenGenKey(ctx, iamAdmin, boundResources.tokenGen); err != nil { 171 w := fmt.Sprintf("unable to delete key under service account %q (WAL entry to clean-up later has been added): %v", boundResources.accountId.ResourceName(), err) 172 warnings = append(warnings, w) 173 } 174 } 175 176 if merr := b.removeBindings(ctx, req, boundResources.accountId.EmailOrId, boundResources.bindings); merr != nil { 177 for _, err := range merr.Errors { 178 w := fmt.Sprintf("unable to delete IAM policy bindings for service account %q (WAL entry to clean-up later has been added): %v", boundResources.accountId.EmailOrId, err) 179 warnings = append(warnings, w) 180 } 181 } 182 183 if removeServiceAccount { 184 if err := b.deleteServiceAccount(ctx, iamAdmin, boundResources.accountId); err != nil { 185 w := fmt.Sprintf("unable to delete service account %q (WAL entry to clean-up later has been added): %v", boundResources.accountId.ResourceName(), err) 186 warnings = append(warnings, w) 187 } 188 } 189 190 // If resources were deleted, we don't need the WAL rollbacks we created for these resources. 191 if len(warnings) == 0 { 192 b.tryDeleteWALs(ctx, req.Storage, walIds...) 193 } 194 195 return nil 196} 197 198func (b *backend) deleteTokenGenKey(ctx context.Context, iamAdmin *iam.Service, tgen *TokenGenerator) error { 199 if tgen == nil || tgen.KeyName == "" { 200 return nil 201 } 202 203 _, err := iamAdmin.Projects.ServiceAccounts.Keys.Delete(tgen.KeyName).Context(ctx).Do() 204 if err != nil && !isGoogleAccountKeyNotFoundErr(err) { 205 return errwrap.Wrapf("unable to delete service account key: {{err}}", err) 206 } 207 return nil 208} 209 210func (b *backend) removeBindings(ctx context.Context, req *logical.Request, email string, bindings ResourceBindings) (allErr *multierror.Error) { 211 httpC, err := b.HTTPClient(req.Storage) 212 if err != nil { 213 return &multierror.Error{Errors: []error{err}} 214 } 215 216 apiHandle := iamutil.GetApiHandle(httpC, useragent.String()) 217 218 for resName, roles := range bindings { 219 resource, err := b.resources.Parse(resName) 220 if err != nil { 221 allErr = multierror.Append(allErr, errwrap.Wrapf(fmt.Sprintf("unable to delete role binding for resource '%s': {{err}}", resName), err)) 222 continue 223 } 224 225 p, err := resource.GetIamPolicy(ctx, apiHandle) 226 if err != nil { 227 allErr = multierror.Append(allErr, errwrap.Wrapf(fmt.Sprintf("unable to delete role binding for resource '%s': {{err}}", resName), err)) 228 continue 229 } 230 231 changed, newP := p.RemoveBindings(&iamutil.PolicyDelta{ 232 Email: email, 233 Roles: roles, 234 }) 235 if !changed { 236 continue 237 } 238 if _, err = resource.SetIamPolicy(ctx, apiHandle, newP); err != nil { 239 allErr = multierror.Append(allErr, errwrap.Wrapf(fmt.Sprintf("unable to delete role binding for resource '%s': {{err}}", resName), err)) 240 continue 241 } 242 } 243 return 244} 245 246func (b *backend) deleteServiceAccount(ctx context.Context, iamAdmin *iam.Service, account gcputil.ServiceAccountId) error { 247 if account.EmailOrId == "" { 248 return nil 249 } 250 251 _, err := iamAdmin.Projects.ServiceAccounts.Delete(account.ResourceName()).Context(ctx).Do() 252 if err != nil && !isGoogleAccountNotFoundErr(err) { 253 return errwrap.Wrapf("unable to delete service account: {{err}}", err) 254 } 255 return nil 256} 257 258func isGoogleAccountNotFoundErr(err error) bool { 259 return isGoogleApiErrorWithCodes(err, 404) 260} 261 262func isGoogleAccountKeyNotFoundErr(err error) bool { 263 return isGoogleApiErrorWithCodes(err, 403, 404) 264} 265 266func isGoogleAccountUnauthorizedErr(err error) bool { 267 return isGoogleApiErrorWithCodes(err, 403) 268} 269 270func isGoogleApiErrorWithCodes(err error, validErrCodes ...int) bool { 271 if err == nil { 272 return false 273 } 274 275 gErr, ok := err.(*googleapi.Error) 276 if !ok { 277 wrapErrV := errwrap.GetType(err, &googleapi.Error{}) 278 if wrapErrV == nil { 279 return false 280 } 281 gErr = wrapErrV.(*googleapi.Error) 282 } 283 284 for _, code := range validErrCodes { 285 if gErr.Code == code { 286 return true 287 } 288 } 289 290 return false 291} 292 293func emailForServiceAccountName(project, accountName string) string { 294 return fmt.Sprintf(serviceAccountEmailTemplate, accountName, project) 295} 296 297func roleSetServiceAccountDisplayName(name string) string { 298 fullDisplayName := fmt.Sprintf(serviceAccountDisplayNameTmpl, name) 299 displayName := fullDisplayName 300 if len(fullDisplayName) > serviceAccountDisplayNameMaxLen { 301 truncIndex := serviceAccountDisplayNameMaxLen - serviceAccountDisplayNameHashLen 302 h := fmt.Sprintf("%x", sha256.Sum256([]byte(fullDisplayName[truncIndex:]))) 303 displayName = fullDisplayName[:truncIndex] + h[:serviceAccountDisplayNameHashLen] 304 } 305 return displayName 306} 307