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