1// Copyright 2019 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package storage
16
17import (
18	"context"
19	"errors"
20	"fmt"
21	"time"
22
23	"google.golang.org/api/iterator"
24	raw "google.golang.org/api/storage/v1"
25)
26
27// HMACState is the state of the HMAC key.
28//
29// This type is EXPERIMENTAL and subject to change or removal without notice.
30type HMACState string
31
32const (
33	// Active is the status for an active key that can be used to sign
34	// requests.
35	Active HMACState = "ACTIVE"
36
37	// Inactive is the status for an inactive key thus requests signed by
38	// this key will be denied.
39	Inactive HMACState = "INACTIVE"
40
41	// Deleted is the status for a key that is deleted.
42	// Once in this state the key cannot key cannot be recovered
43	// and does not count towards key limits. Deleted keys will be cleaned
44	// up later.
45	Deleted HMACState = "DELETED"
46)
47
48// HMACKey is the representation of a Google Cloud Storage HMAC key.
49//
50// HMAC keys are used to authenticate signed access to objects. To enable HMAC key
51// authentication, please visit https://cloud.google.com/storage/docs/migrating.
52//
53// This type is EXPERIMENTAL and subject to change or removal without notice.
54type HMACKey struct {
55	// The HMAC's secret key.
56	Secret string
57
58	// AccessID is the ID of the HMAC key.
59	AccessID string
60
61	// Etag is the HTTP/1.1 Entity tag.
62	Etag string
63
64	// ID is the ID of the HMAC key, including the ProjectID and AccessID.
65	ID string
66
67	// ProjectID is the ID of the project that owns the
68	// service account to which the key authenticates.
69	ProjectID string
70
71	// ServiceAccountEmail is the email address
72	// of the key's associated service account.
73	ServiceAccountEmail string
74
75	// CreatedTime is the creation time of the HMAC key.
76	CreatedTime time.Time
77
78	// UpdatedTime is the last modification time of the HMAC key metadata.
79	UpdatedTime time.Time
80
81	// State is the state of the HMAC key.
82	// It can be one of StateActive, StateInactive or StateDeleted.
83	State HMACState
84}
85
86// HMACKeyHandle helps provide access and management for HMAC keys.
87//
88// This type is EXPERIMENTAL and subject to change or removal without notice.
89type HMACKeyHandle struct {
90	projectID string
91	accessID  string
92
93	raw *raw.ProjectsHmacKeysService
94}
95
96// HMACKeyHandle creates a handle that will be used for HMACKey operations.
97//
98// This method is EXPERIMENTAL and subject to change or removal without notice.
99func (c *Client) HMACKeyHandle(projectID, accessID string) *HMACKeyHandle {
100	return &HMACKeyHandle{
101		projectID: projectID,
102		accessID:  accessID,
103		raw:       raw.NewProjectsHmacKeysService(c.raw),
104	}
105}
106
107// Get invokes an RPC to retrieve the HMAC key referenced by the
108// HMACKeyHandle's accessID.
109//
110// Options such as UserProjectForHMACKeys can be used to set the
111// userProject to be billed against for operations.
112//
113// This method is EXPERIMENTAL and subject to change or removal without notice.
114func (hkh *HMACKeyHandle) Get(ctx context.Context, opts ...HMACKeyOption) (*HMACKey, error) {
115	call := hkh.raw.Get(hkh.projectID, hkh.accessID)
116
117	desc := new(hmacKeyDesc)
118	for _, opt := range opts {
119		opt.withHMACKeyDesc(desc)
120	}
121	if desc.userProjectID != "" {
122		call = call.UserProject(desc.userProjectID)
123	}
124
125	setClientHeader(call.Header())
126
127	var metadata *raw.HmacKeyMetadata
128	var err error
129	err = runWithRetry(ctx, func() error {
130		metadata, err = call.Context(ctx).Do()
131		return err
132	})
133	if err != nil {
134		return nil, err
135	}
136
137	hkPb := &raw.HmacKey{
138		Metadata: metadata,
139	}
140	return pbHmacKeyToHMACKey(hkPb, false)
141}
142
143// Delete invokes an RPC to delete the key referenced by accessID, on Google Cloud Storage.
144// Only inactive HMAC keys can be deleted.
145// After deletion, a key cannot be used to authenticate requests.
146//
147// This method is EXPERIMENTAL and subject to change or removal without notice.
148func (hkh *HMACKeyHandle) Delete(ctx context.Context, opts ...HMACKeyOption) error {
149	delCall := hkh.raw.Delete(hkh.projectID, hkh.accessID)
150	desc := new(hmacKeyDesc)
151	for _, opt := range opts {
152		opt.withHMACKeyDesc(desc)
153	}
154	if desc.userProjectID != "" {
155		delCall = delCall.UserProject(desc.userProjectID)
156	}
157	setClientHeader(delCall.Header())
158
159	return runWithRetry(ctx, func() error {
160		return delCall.Context(ctx).Do()
161	})
162}
163
164func pbHmacKeyToHMACKey(pb *raw.HmacKey, updatedTimeCanBeNil bool) (*HMACKey, error) {
165	pbmd := pb.Metadata
166	if pbmd == nil {
167		return nil, errors.New("field Metadata cannot be nil")
168	}
169	createdTime, err := time.Parse(time.RFC3339, pbmd.TimeCreated)
170	if err != nil {
171		return nil, fmt.Errorf("field CreatedTime: %v", err)
172	}
173	updatedTime, err := time.Parse(time.RFC3339, pbmd.Updated)
174	if err != nil && !updatedTimeCanBeNil {
175		return nil, fmt.Errorf("field UpdatedTime: %v", err)
176	}
177
178	hmk := &HMACKey{
179		AccessID:    pbmd.AccessId,
180		Secret:      pb.Secret,
181		Etag:        pbmd.Etag,
182		ID:          pbmd.Id,
183		State:       HMACState(pbmd.State),
184		ProjectID:   pbmd.ProjectId,
185		CreatedTime: createdTime,
186		UpdatedTime: updatedTime,
187
188		ServiceAccountEmail: pbmd.ServiceAccountEmail,
189	}
190
191	return hmk, nil
192}
193
194// CreateHMACKey invokes an RPC for Google Cloud Storage to create a new HMACKey.
195//
196// This method is EXPERIMENTAL and subject to change or removal without notice.
197func (c *Client) CreateHMACKey(ctx context.Context, projectID, serviceAccountEmail string, opts ...HMACKeyOption) (*HMACKey, error) {
198	if projectID == "" {
199		return nil, errors.New("storage: expecting a non-blank projectID")
200	}
201	if serviceAccountEmail == "" {
202		return nil, errors.New("storage: expecting a non-blank service account email")
203	}
204
205	svc := raw.NewProjectsHmacKeysService(c.raw)
206	call := svc.Create(projectID, serviceAccountEmail)
207	desc := new(hmacKeyDesc)
208	for _, opt := range opts {
209		opt.withHMACKeyDesc(desc)
210	}
211	if desc.userProjectID != "" {
212		call = call.UserProject(desc.userProjectID)
213	}
214
215	setClientHeader(call.Header())
216
217	hkPb, err := call.Context(ctx).Do()
218	if err != nil {
219		return nil, err
220	}
221
222	return pbHmacKeyToHMACKey(hkPb, true)
223}
224
225// HMACKeyAttrsToUpdate defines the attributes of an HMACKey that will be updated.
226//
227// This type is EXPERIMENTAL and subject to change or removal without notice.
228type HMACKeyAttrsToUpdate struct {
229	// State is required and must be either StateActive or StateInactive.
230	State HMACState
231
232	// Etag is an optional field and it is the HTTP/1.1 Entity tag.
233	Etag string
234}
235
236// Update mutates the HMACKey referred to by accessID.
237//
238// This method is EXPERIMENTAL and subject to change or removal without notice.
239func (h *HMACKeyHandle) Update(ctx context.Context, au HMACKeyAttrsToUpdate, opts ...HMACKeyOption) (*HMACKey, error) {
240	if au.State != Active && au.State != Inactive {
241		return nil, fmt.Errorf("storage: invalid state %q for update, must be either %q or %q", au.State, Active, Inactive)
242	}
243
244	call := h.raw.Update(h.projectID, h.accessID, &raw.HmacKeyMetadata{
245		Etag:  au.Etag,
246		State: string(au.State),
247	})
248
249	desc := new(hmacKeyDesc)
250	for _, opt := range opts {
251		opt.withHMACKeyDesc(desc)
252	}
253	if desc.userProjectID != "" {
254		call = call.UserProject(desc.userProjectID)
255	}
256	setClientHeader(call.Header())
257
258	var metadata *raw.HmacKeyMetadata
259	var err error
260	err = runWithRetry(ctx, func() error {
261		metadata, err = call.Context(ctx).Do()
262		return err
263	})
264
265	if err != nil {
266		return nil, err
267	}
268	hkPb := &raw.HmacKey{
269		Metadata: metadata,
270	}
271	return pbHmacKeyToHMACKey(hkPb, false)
272}
273
274// An HMACKeysIterator is an iterator over HMACKeys.
275//
276// Note: This iterator is not safe for concurrent operations without explicit synchronization.
277//
278// This type is EXPERIMENTAL and subject to change or removal without notice.
279type HMACKeysIterator struct {
280	ctx       context.Context
281	raw       *raw.ProjectsHmacKeysService
282	projectID string
283	hmacKeys  []*HMACKey
284	pageInfo  *iterator.PageInfo
285	nextFunc  func() error
286	index     int
287	desc      hmacKeyDesc
288}
289
290// ListHMACKeys returns an iterator for listing HMACKeys.
291//
292// Note: This iterator is not safe for concurrent operations without explicit synchronization.
293//
294// This method is EXPERIMENTAL and subject to change or removal without notice.
295func (c *Client) ListHMACKeys(ctx context.Context, projectID string, opts ...HMACKeyOption) *HMACKeysIterator {
296	it := &HMACKeysIterator{
297		ctx:       ctx,
298		raw:       raw.NewProjectsHmacKeysService(c.raw),
299		projectID: projectID,
300	}
301
302	for _, opt := range opts {
303		opt.withHMACKeyDesc(&it.desc)
304	}
305
306	it.pageInfo, it.nextFunc = iterator.NewPageInfo(
307		it.fetch,
308		func() int { return len(it.hmacKeys) - it.index },
309		func() interface{} {
310			prev := it.hmacKeys
311			it.hmacKeys = it.hmacKeys[:0]
312			it.index = 0
313			return prev
314		})
315	return it
316}
317
318// Next returns the next result. Its second return value is iterator.Done if
319// there are no more results. Once Next returns iterator.Done, all subsequent
320// calls will return iterator.Done.
321//
322// Note: This iterator is not safe for concurrent operations without explicit synchronization.
323//
324// This method is EXPERIMENTAL and subject to change or removal without notice.
325func (it *HMACKeysIterator) Next() (*HMACKey, error) {
326	if err := it.nextFunc(); err != nil {
327		return nil, err
328	}
329
330	key := it.hmacKeys[it.index]
331	it.index++
332
333	return key, nil
334}
335
336// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
337//
338// Note: This iterator is not safe for concurrent operations without explicit synchronization.
339//
340// This method is EXPERIMENTAL and subject to change or removal without notice.
341func (it *HMACKeysIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
342
343func (it *HMACKeysIterator) fetch(pageSize int, pageToken string) (token string, err error) {
344	call := it.raw.List(it.projectID)
345	setClientHeader(call.Header())
346	if pageToken != "" {
347		call = call.PageToken(pageToken)
348	}
349	if it.desc.showDeletedKeys {
350		call = call.ShowDeletedKeys(true)
351	}
352	if it.desc.userProjectID != "" {
353		call = call.UserProject(it.desc.userProjectID)
354	}
355	if it.desc.forServiceAccountEmail != "" {
356		call = call.ServiceAccountEmail(it.desc.forServiceAccountEmail)
357	}
358	if pageSize > 0 {
359		call = call.MaxResults(int64(pageSize))
360	}
361
362	ctx := it.ctx
363	var resp *raw.HmacKeysMetadata
364	err = runWithRetry(it.ctx, func() error {
365		resp, err = call.Context(ctx).Do()
366		return err
367	})
368	if err != nil {
369		return "", err
370	}
371
372	for _, metadata := range resp.Items {
373		hkPb := &raw.HmacKey{
374			Metadata: metadata,
375		}
376		hkey, err := pbHmacKeyToHMACKey(hkPb, true)
377		if err != nil {
378			return "", err
379		}
380		it.hmacKeys = append(it.hmacKeys, hkey)
381	}
382	return resp.NextPageToken, nil
383}
384
385type hmacKeyDesc struct {
386	forServiceAccountEmail string
387	showDeletedKeys        bool
388	userProjectID          string
389}
390
391// HMACKeyOption configures the behavior of HMACKey related methods and actions.
392//
393// This interface is EXPERIMENTAL and subject to change or removal without notice.
394type HMACKeyOption interface {
395	withHMACKeyDesc(*hmacKeyDesc)
396}
397
398type hmacKeyDescFunc func(*hmacKeyDesc)
399
400func (hkdf hmacKeyDescFunc) withHMACKeyDesc(hkd *hmacKeyDesc) {
401	hkdf(hkd)
402}
403
404// ForHMACKeyServiceAccountEmail returns HMAC Keys that are
405// associated with the email address of a service account in the project.
406//
407// Only one service account email can be used as a filter, so if multiple
408// of these options are applied, the last email to be set will be used.
409//
410// This option is EXPERIMENTAL and subject to change or removal without notice.
411func ForHMACKeyServiceAccountEmail(serviceAccountEmail string) HMACKeyOption {
412	return hmacKeyDescFunc(func(hkd *hmacKeyDesc) {
413		hkd.forServiceAccountEmail = serviceAccountEmail
414	})
415}
416
417// ShowDeletedHMACKeys will also list keys whose state is "DELETED".
418//
419// This option is EXPERIMENTAL and subject to change or removal without notice.
420func ShowDeletedHMACKeys() HMACKeyOption {
421	return hmacKeyDescFunc(func(hkd *hmacKeyDesc) {
422		hkd.showDeletedKeys = true
423	})
424}
425
426// UserProjectForHMACKeys will bill the request against userProjectID
427// if userProjectID is non-empty.
428//
429// Note: This is a noop right now and only provided for API compatibility.
430//
431// This option is EXPERIMENTAL and subject to change or removal without notice.
432func UserProjectForHMACKeys(userProjectID string) HMACKeyOption {
433	return hmacKeyDescFunc(func(hkd *hmacKeyDesc) {
434		hkd.userProjectID = userProjectID
435	})
436}
437