storage: add HMACKey management
Adds methods:
(*Client) CreateHMACKey(context.Context, projectID, serviceAccount)
(*Client) HMACKeyHandle(projectID, accessKeyId)
(*HMACKeyHandle) Delete(context.Context) error
(*HMACKeyHandle) Get(context.Context) (*HMACKey, error)
(*HMACKeyHandle) Update(context.Context) (*HMACKey, error)
An HMACKey will be commonly referred to
by an accessId such as:
GOOGTS7C7FUP3AIRVJTE2BCD
while a Secret will look for example like:
bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ
One can enable "HMACKey interoperability" by visiting
https://cloud.google.com/storage/docs/migrating
Another CL will follow up to add Listing capabilities.
Updates #1486
Change-Id: I2e5e5d597265eb5b747bed250695d7ddd5df4dad
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/42550
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Lui <jonathanlui@google.com>
Reviewed-by: Frank Natividad <franknatividad@google.com>
Reviewed-by: Jean de Klerk <deklerk@google.com>
diff --git a/storage/example_test.go b/storage/example_test.go
index 5162f95..235130d 100644
--- a/storage/example_test.go
+++ b/storage/example_test.go
@@ -663,3 +663,63 @@
// TODO: handle error.
}
}
+
+func ExampleClient_CreateHMACKey() {
+ ctx := context.Background()
+ client, err := storage.NewClient(ctx)
+ if err != nil {
+ // TODO: handle error.
+ }
+
+ hkey, err := client.CreateHMACKey(ctx, "project-id", "service-account-email")
+ if err != nil {
+ // TODO: handle error.
+ }
+ _ = hkey // TODO: Use the HMAC Key.
+}
+
+func ExampleHMACKeyHandle_Delete() {
+ ctx := context.Background()
+ client, err := storage.NewClient(ctx)
+ if err != nil {
+ // TODO: handle error.
+ }
+
+ hkh := client.HMACKeyHandle("project-id", "access-key-id")
+ // Make sure that the HMACKey being deleted has a status of inactive.
+ if err := hkh.Delete(ctx); err != nil {
+ // TODO: handle error.
+ }
+}
+
+func ExampleHMACKeyHandle_Get() {
+ ctx := context.Background()
+ client, err := storage.NewClient(ctx)
+ if err != nil {
+ // TODO: handle error.
+ }
+
+ hkh := client.HMACKeyHandle("project-id", "access-key-id")
+ hkey, err := hkh.Get(ctx)
+ if err != nil {
+ // TODO: handle error.
+ }
+ _ = hkey // TODO: Use the HMAC Key.
+}
+
+func ExampleHMACKeyHandle_Update() {
+ ctx := context.Background()
+ client, err := storage.NewClient(ctx)
+ if err != nil {
+ // TODO: handle error.
+ }
+
+ hkh := client.HMACKeyHandle("project-id", "access-key-id")
+ ukey, err := hkh.Update(ctx, storage.HMACKeyAttrsToUpdate{
+ State: storage.Inactive,
+ })
+ if err != nil {
+ // TODO: handle error.
+ }
+ _ = ukey // TODO: Use the HMAC Key.
+}
diff --git a/storage/hmac.go b/storage/hmac.go
new file mode 100644
index 0000000..a906d44
--- /dev/null
+++ b/storage/hmac.go
@@ -0,0 +1,227 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package storage
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ raw "google.golang.org/api/storage/v1"
+)
+
+// HMACState is the state of the HMAC key.
+type HMACState string
+
+const (
+ // Active is the status for an active key that can be used to sign
+ // requests.
+ Active HMACState = "ACTIVE"
+
+ // Inactive is the status for an inactive key thus requests signed by
+ // this key will be denied.
+ Inactive HMACState = "INACTIVE"
+
+ // Deleted is the status for a key that is deleted.
+ // Once in this state the key cannot key cannot be recovered
+ // and does not count towards key limits. Deleted keys will be cleaned
+ // up later.
+ Deleted HMACState = "DELETED"
+)
+
+// HMACKey is the representation of a Google Cloud Storage HMAC key.
+//
+// HMAC keys are used to authenticate signed access to objects. To enable HMAC key
+// authentication, please visit https://cloud.google.com/storage/docs/migrating.
+//
+// This type is experimental and subject to change.
+type HMACKey struct {
+ // The HMAC's secret key.
+ Secret string
+
+ // AccessID is the ID of the HMAC key.
+ AccessID string
+
+ // Etag is the HTTP/1.1 Entity tag.
+ Etag string
+
+ // ID is the ID of the HMAC key, including the ProjectID and AccessID.
+ ID string
+
+ // ProjectID is the ID of the project that owns the
+ // service account to which the key authenticates.
+ ProjectID string
+
+ // ServiceAccountEmail is the email address
+ // of the key's associated service account.
+ ServiceAccountEmail string
+
+ // CreatedTime is the creation time of the HMAC key.
+ CreatedTime time.Time
+
+ // UpdatedTime is the last modification time of the HMAC key metadata.
+ UpdatedTime time.Time
+
+ // State is the state of the HMAC key.
+ // It can be one of StateActive, StateInactive or StateDeleted.
+ State HMACState
+}
+
+// HMACKeyHandle helps provide access and management for HMAC keys.
+//
+// This type is experimental and subject to change.
+type HMACKeyHandle struct {
+ projectID string
+ accessID string
+
+ raw *raw.ProjectsHmacKeysService
+}
+
+// HMACKeyHandle creates a handle that will be used for HMACKey operations.
+func (c *Client) HMACKeyHandle(projectID, accessID string) *HMACKeyHandle {
+ return &HMACKeyHandle{
+ projectID: projectID,
+ accessID: accessID,
+ raw: raw.NewProjectsHmacKeysService(c.raw),
+ }
+}
+
+// Get invokes an RPC to retrieve the HMAC key referenced by the
+// HMACKeyHandle's accessID.
+func (hkh *HMACKeyHandle) Get(ctx context.Context) (*HMACKey, error) {
+ call := hkh.raw.Get(hkh.projectID, hkh.accessID)
+ setClientHeader(call.Header())
+
+ var metadata *raw.HmacKeyMetadata
+ var err error
+ err = runWithRetry(ctx, func() error {
+ metadata, err = call.Context(ctx).Do()
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ hkPb := &raw.HmacKey{
+ Metadata: metadata,
+ }
+ return pbHmacKeyToHMACKey(hkPb, false)
+}
+
+// Delete invokes an RPC to delete the key referenced by accessID, on Google Cloud Storage.
+// Only inactive HMAC keys can be deleted.
+// After deletion, a key cannot be used to authenticate requests.
+func (hkh *HMACKeyHandle) Delete(ctx context.Context) error {
+ delCall := hkh.raw.Delete(hkh.projectID, hkh.accessID)
+ setClientHeader(delCall.Header())
+
+ return runWithRetry(ctx, func() error {
+ return delCall.Context(ctx).Do()
+ })
+}
+
+func pbHmacKeyToHMACKey(pb *raw.HmacKey, isCreate bool) (*HMACKey, error) {
+ pbmd := pb.Metadata
+ if pbmd == nil {
+ return nil, errors.New("field Metadata cannot be nil")
+ }
+ createdTime, err := time.Parse(time.RFC3339, pbmd.TimeCreated)
+ if err != nil {
+ return nil, fmt.Errorf("field CreatedTime: %v", err)
+ }
+ updatedTime, err := time.Parse(time.RFC3339, pbmd.Updated)
+ if err != nil && !isCreate {
+ return nil, fmt.Errorf("field UpdatedTime: %v", err)
+ }
+
+ hmk := &HMACKey{
+ AccessID: pbmd.AccessId,
+ Secret: pb.Secret,
+ Etag: pbmd.Etag,
+ ID: pbmd.Id,
+ State: HMACState(pbmd.State),
+ ProjectID: pbmd.ProjectId,
+ CreatedTime: createdTime,
+ UpdatedTime: updatedTime,
+
+ ServiceAccountEmail: pbmd.ServiceAccountEmail,
+ }
+
+ return hmk, nil
+}
+
+// CreateHMACKey invokes an RPC for Google Cloud Storage to create a new HMACKey.
+func (c *Client) CreateHMACKey(ctx context.Context, projectID, serviceAccountEmail string) (*HMACKey, error) {
+ if projectID == "" {
+ return nil, errors.New("storage: expecting a non-blank projectID")
+ }
+ if serviceAccountEmail == "" {
+ return nil, errors.New("storage: expecting a non-blank service account email")
+ }
+
+ svc := raw.NewProjectsHmacKeysService(c.raw)
+ call := svc.Create(projectID, serviceAccountEmail)
+ setClientHeader(call.Header())
+
+ var hkPb *raw.HmacKey
+ var err error
+ err = runWithRetry(ctx, func() error {
+ hkPb, err = call.Context(ctx).Do()
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return pbHmacKeyToHMACKey(hkPb, true)
+}
+
+// HMACKeyAttrsToUpdate defines the attributes of an HMACKey that will be updated.
+type HMACKeyAttrsToUpdate struct {
+ // State is required and must be either StateActive or StateInactive.
+ State HMACState
+
+ // Etag is an optional field and it is the HTTP/1.1 Entity tag.
+ Etag string
+}
+
+// Update mutates the HMACKey referred to by accessID.
+func (h *HMACKeyHandle) Update(ctx context.Context, au HMACKeyAttrsToUpdate) (*HMACKey, error) {
+ if au.State != Active && au.State != Inactive {
+ return nil, fmt.Errorf("storage: invalid state %q for update, must be either %q or %q", au.State, Active, Inactive)
+ }
+
+ call := h.raw.Update(h.projectID, h.accessID, &raw.HmacKeyMetadata{
+ Etag: au.Etag,
+ State: string(au.State),
+ })
+ setClientHeader(call.Header())
+
+ var metadata *raw.HmacKeyMetadata
+ var err error
+ err = runWithRetry(ctx, func() error {
+ metadata, err = call.Context(ctx).Do()
+ return err
+ })
+
+ if err != nil {
+ return nil, err
+ }
+ hkPb := &raw.HmacKey{
+ Metadata: metadata,
+ }
+ return pbHmacKeyToHMACKey(hkPb, false)
+}
diff --git a/storage/hmac_test.go b/storage/hmac_test.go
new file mode 100644
index 0000000..b53651f
--- /dev/null
+++ b/storage/hmac_test.go
@@ -0,0 +1,380 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package storage
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+
+ "cloud.google.com/go/internal/testutil"
+ "google.golang.org/api/googleapi"
+)
+
+func TestHMACKeyHandle_GetParsing(t *testing.T) {
+ mt := &mockTransport{}
+ client := mockClient(t, mt)
+ projectID := "hmackey-project-id"
+ ctx := context.Background()
+
+ tests := []struct {
+ res string
+ want *HMACKey
+ wantErr string
+ }{
+ {
+ res: fmt.Sprintf(`
+ {
+ "kind": "storage#hmacKeyMetadata",
+ "projectId":%q,"state":"ACTIVE",
+ "timeCreated": "2019-07-06T11:21:58+00:00",
+ "updated": "2019-07-06T11:22:18+00:00"
+ }`, projectID),
+ want: &HMACKey{
+ State: Active,
+ ProjectID: projectID,
+ UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
+ CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
+ },
+ },
+ {
+ res: fmt.Sprintf(`
+ {
+ "kind": "storage#hmacKeyMetadata",
+ "projectId":%q,"state":"ACTIVE",
+ "timeCreated": "2019-07-06T11:21:58+00:00",
+ "updated": "2019-07-06T11:22:18+00:00"
+ }`, projectID),
+ want: &HMACKey{
+ State: Active,
+ ProjectID: projectID,
+ UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
+ CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
+ },
+ },
+ {
+ res: `{}`,
+ // CreatedTime must be formatted in RFC 3339.
+ wantErr: `CreatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
+ },
+ {
+ res: `{"timeCreated": "2019-07-foo"}`,
+ // CreatedTime must be formatted in RFC 3339.
+ wantErr: `CreatedTime: parsing time "2019-07-foo" as "2006-01-02T15:04:05Z07:00"`,
+ },
+ {
+ res: `{
+ "kind": "storage#hmacKeyMetadata",
+ "state":"INACTIVE",
+ "timeCreated": "2019-07-06T11:21:58+00:00"
+ }`,
+ // UpdatedTime must be formatted in RFC 3339.
+ wantErr: `UpdatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
+ },
+ }
+
+ for i, tt := range tests {
+ mt.addResult(&http.Response{
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ ContentLength: int64(len(tt.res)),
+ Status: "OK",
+ StatusCode: 200,
+ Body: bodyReader(tt.res),
+ }, nil)
+ hkh := client.HMACKeyHandle(projectID, "some-access-key-id")
+ got, err := hkh.Get(ctx)
+ if tt.wantErr != "" {
+ if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
+ t.Errorf("#%d: failed to match errors:\ngot: %q\nwant: %q", i, err, tt.wantErr)
+ }
+ if got != nil {
+ t.Errorf("#%d: unexpectedly got a non-nil result: %#v\n", i, got)
+ }
+ continue
+ }
+
+ if err != nil {
+ t.Errorf("#%d: got an unexpected error: %v", i, err)
+ continue
+ }
+
+ if diff := testutil.Diff(got, tt.want); diff != "" {
+ t.Errorf("#%d: got - want +\n\n%s", i, diff)
+ }
+ }
+}
+
+func TestHMACKeyHandle_Get_NotFound(t *testing.T) {
+ mt := &mockTransport{}
+ client := mockClient(t, mt)
+ ctx := context.Background()
+
+ mt.addResult(&http.Response{
+ ProtoMajor: 2,
+ ProtoMinor: 0,
+ Status: "OK",
+ StatusCode: http.StatusNotFound,
+ Body: bodyReader("Access ID not found in project"),
+ }, nil)
+
+ hkh := client.HMACKeyHandle("project-id", "some-access-key-id")
+ _, gotErr := hkh.Get(ctx)
+
+ wantErr := &googleapi.Error{
+ Body: "Access ID not found in project",
+ Code: http.StatusNotFound,
+ Message: "",
+ }
+ if diff := testutil.Diff(gotErr, wantErr); diff != "" {
+ t.Fatalf("Error mismatch, got - want +\n%s", diff)
+ }
+}
+
+func TestHMACKeyHandle_Delete(t *testing.T) {
+ mt := &mockTransport{}
+ client := mockClient(t, mt)
+ ctx := context.Background()
+
+ tests := []struct {
+ statusCode int
+ msg string
+ wantErr error
+ }{
+ {
+ statusCode: http.StatusBadRequest,
+ msg: "Cannot delete keys in 'ACTIVE' state",
+ wantErr: &googleapi.Error{
+ Code: http.StatusBadRequest, Message: "Cannot delete keys in 'ACTIVE' state",
+ Body: `{"error":{"message":"Cannot delete keys in 'ACTIVE' state"}}`,
+ },
+ },
+ {
+ statusCode: http.StatusNotFound,
+ msg: "random message",
+ wantErr: &googleapi.Error{
+ Code: http.StatusNotFound, Message: "random message",
+ Body: `{"error":{"message":"random message"}}`,
+ },
+ },
+ {
+ statusCode: http.StatusNotFound,
+ msg: "Access ID not found in project",
+ wantErr: &googleapi.Error{
+ Code: http.StatusNotFound, Message: "Access ID not found in project",
+ Body: `{"error":{"message":"Access ID not found in project"}}`,
+ },
+ },
+ }
+
+ for i, tt := range tests {
+ mt.addResult(&http.Response{
+ ProtoMajor: 2,
+ ProtoMinor: 0,
+ Status: tt.msg,
+ StatusCode: tt.statusCode,
+ Body: bodyReader(fmt.Sprintf(`{"error":{"message":%q}}`, tt.msg)),
+ }, nil)
+
+ hkh := client.HMACKeyHandle("project", "access-key-id")
+ err := hkh.Delete(ctx)
+
+ if diff := testutil.Diff(err, tt.wantErr); diff != "" {
+ t.Errorf("#%d: error mismatch got - want +\n%s", i, diff)
+ }
+ }
+}
+
+func TestHMACKeyHandle_Create(t *testing.T) {
+ mt := &mockTransport{}
+ client := mockClient(t, mt)
+ projectID := "hmackey-project-id"
+ serviceAccountEmail := "service-account-email-1"
+ ctx := context.Background()
+
+ tests := []struct {
+ res string
+ want *HMACKey
+ wantErr string
+ }{
+ {
+ res: `
+ {
+ "kind": "storage#hmackey",
+ "secret":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
+ "metadata": {
+ "projectId":"project-id","state":"ACTIVE",
+ "timeCreated": "2019-07-06T11:21:58+00:00",
+ "updated": "2019-07-06T11:22:18+00:00"
+ }
+ }`,
+ want: &HMACKey{
+ Secret: "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
+ State: Active,
+ ProjectID: "project-id",
+ UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
+ CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
+ },
+ },
+ {
+ res: `{}`,
+ wantErr: "Metadata cannot be nil",
+ },
+ {
+ res: `{"metadata":{}}`,
+ // CreatedTime must be non-empty and it must formatted in RFC 3339.
+ wantErr: `CreatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
+ },
+ {
+ res: `{"metadata":{"timeCreated": "2019-07-foo"}}`,
+ // CreatedTime must be formatted in RFC 3339.
+ wantErr: `CreatedTime: parsing time "2019-07-foo" as "2006-01-02T15:04:05Z07:00"`,
+ },
+ {
+ res: `{
+ "kind": "storage#hmackey",
+ "secret":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
+ "metadata":{
+ "kind": "storage#hmacKeyMetadata",
+ "state":"ACTIVE",
+ "timeCreated": "2019-07-06T12:11:33+00:00",
+ "projectId": "project-id",
+ "updated": ""
+ }
+ }`,
+ // ONLY during creation is it okay for UpdatedTime to not be set.
+ want: &HMACKey{
+ Secret: "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
+ State: Active,
+ ProjectID: "project-id",
+ CreatedTime: time.Date(2019, 07, 06, 12, 11, 33, 0, time.UTC),
+ },
+ },
+ }
+
+ for i, tt := range tests {
+ mt.addResult(&http.Response{
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ ContentLength: int64(len(tt.res)),
+ Status: "OK",
+ StatusCode: 200,
+ Body: bodyReader(tt.res),
+ }, nil)
+ got, err := client.CreateHMACKey(ctx, projectID, serviceAccountEmail)
+ if tt.wantErr != "" {
+ if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
+ t.Errorf("#%d: failed to match errors:\ngot: %q\nwant: %q", i, err, tt.wantErr)
+ }
+ if got != nil {
+ t.Errorf("#%d: unexpectedly got a non-nil result: %#v\n", i, got)
+ }
+ continue
+ }
+
+ if err != nil {
+ t.Errorf("#%d: got an unexpected error: %v", i, err)
+ continue
+ }
+
+ if diff := testutil.Diff(got, tt.want); diff != "" {
+ t.Errorf("#%d: got - want +\n\n%s", i, diff)
+ }
+ }
+
+ // Lastly ensure that a blank service account will return an error.
+ mt.addResult(&http.Response{
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ Status: "OK",
+ StatusCode: 200,
+ Body: bodyReader("{}"),
+ }, nil)
+ hk, err := client.CreateHMACKey(ctx, projectID, "")
+ if err == nil {
+ t.Fatal("Unexpectedly succeeded in creating a key using a blank service account email")
+ }
+ if !strings.Contains(err.Error(), "non-blank service account email") {
+ t.Fatalf("Expected an error about a non-blank service account email: %v", err)
+ }
+ if hk != nil {
+ t.Fatalf("Unexpectedly got back a created HMACKey: %#v", hk)
+ }
+}
+
+func TestHMACKey_UpdateState(t *testing.T) {
+ // This test ensures that updating the state can only
+ // happen with either of Active or Inactive.
+
+ mt := &mockTransport{}
+ client := mockClient(t, mt)
+ projectID := "hmackey-project-id"
+ ctx := context.Background()
+
+ hkh := client.HMACKeyHandle(projectID, "some-access-id")
+
+ // 1. Ensure that invalid states are NOT accepted for an Update.
+ invalidStates := []HMACState{"", Deleted, "active", "inactive", "foo_bar"}
+ for _, invalidState := range invalidStates {
+ t.Run("invalid-"+string(invalidState), func(t *testing.T) {
+ _, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
+ State: invalidState,
+ })
+ if err == nil {
+ t.Fatal("Unexpectedly succeeded")
+ }
+ invalidStateMsg := fmt.Sprintf(`storage: invalid state %q for update, must be either "ACTIVE" or "INACTIVE"`, invalidState)
+ if err.Error() != invalidStateMsg {
+ t.Fatalf("Mismatched error: got: %q\nwant: %q", err, invalidStateMsg)
+ }
+ })
+ }
+
+ // 2. Ensure that valid states for Update are accepted.
+ validStates := []HMACState{Active, Inactive}
+ for _, validState := range validStates {
+ t.Run("valid-"+string(validState), func(t *testing.T) {
+ resBody := fmt.Sprintf(`{
+ "kind": "storage#hmacKeyMetadata",
+ "state":%q,
+ "timeCreated": "2019-07-11T12:11:33+00:00",
+ "projectId": "project-id",
+ "updated": "2019-07-11T12:13:33+00:00"
+ }
+ }`, validState)
+ mt.addResult(&http.Response{
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ ContentLength: int64(len(resBody)),
+ Status: "OK",
+ StatusCode: 200,
+ Body: bodyReader(resBody),
+ }, nil)
+
+ hu, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
+ State: validState,
+ })
+ if err != nil {
+ t.Fatalf("Unexpected failure: %v", err)
+ }
+ if hu.State != validState {
+ t.Fatalf("Unexpected updated state %q, expected %q", hu.State, validState)
+ }
+ })
+ }
+}
diff --git a/storage/integration_test.go b/storage/integration_test.go
index 1b25041..e4a92bc 100644
--- a/storage/integration_test.go
+++ b/storage/integration_test.go
@@ -2760,6 +2760,94 @@
}
}
+func TestIntegration_HMACKey_Update(t *testing.T) {
+ ctx := context.Background()
+ client := testConfig(ctx, t)
+ defer client.Close()
+
+ projectID := testutil.ProjID()
+ serviceAccountEmail, err := client.ServiceAccount(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Failed to get service account: %v", err)
+ }
+ hmacKey, err := client.CreateHMACKey(ctx, projectID, serviceAccountEmail)
+ if err != nil {
+ t.Fatalf("Failed to create HMACKey: %v", err)
+ }
+ if hmacKey == nil {
+ t.Fatal("Unexpectedly got back a nil HMAC key")
+ }
+
+ if hmacKey.State != Active {
+ t.Fatalf("Unexpected state %q, expected %q", hmacKey.State, Active)
+ }
+
+ hkh := client.HMACKeyHandle(projectID, hmacKey.AccessID)
+ // 1. Ensure that we CANNOT delete an ACTIVE key.
+ if err := hkh.Delete(ctx); err == nil {
+ t.Fatalf("Unexpectedly deleted key whose state is ACTIVE: %v", err)
+ }
+
+ invalidStates := []HMACState{"", Deleted, "active", "inactive", "foo_bar"}
+ for _, invalidState := range invalidStates {
+ t.Run("invalid-"+string(invalidState), func(t *testing.T) {
+ _, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
+ State: invalidState,
+ })
+ if err == nil {
+ t.Fatal("Unexpectedly succeeded")
+ }
+ invalidStateMsg := fmt.Sprintf(`storage: invalid state %q for update, must be either "ACTIVE" or "INACTIVE"`, invalidState)
+ if err.Error() != invalidStateMsg {
+ t.Fatalf("Mismatched error: got: %q\nwant: %q", err, invalidStateMsg)
+ }
+ })
+ }
+
+ // 2.1. Setting the State to Inactive should succeed.
+ hu, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
+ State: Inactive,
+ })
+ if err != nil {
+ t.Fatalf("Unexpected Update failure: %v", err)
+ }
+ if got, want := hu.State, Inactive; got != want {
+ t.Fatalf("Unexpected updated state %q, expected %q", got, want)
+ }
+
+ // 2.2. Setting the State back to Active should succeed.
+ hu, err = hkh.Update(ctx, HMACKeyAttrsToUpdate{
+ State: Active,
+ })
+ if err != nil {
+ t.Fatalf("Unexpected Update failure: %v", err)
+ }
+ if got, want := hu.State, Active; got != want {
+ t.Fatalf("Unexpected updated state %q, expected %q", got, want)
+ }
+
+ // 3. Finally set it to back to Inactive and
+ // then retry the deletion which should now succeed.
+ _, _ = hkh.Update(ctx, HMACKeyAttrsToUpdate{
+ State: Inactive,
+ })
+ if err := hkh.Delete(ctx); err != nil {
+ t.Fatalf("Unexpected deletion failure: %v", err)
+ }
+
+ hk, err := hkh.Get(ctx)
+ switch {
+ case err == nil:
+ // If the err == nil, then the returned HMACKey's state MUST be Deleted.
+ if hk == nil || hk.State != Deleted {
+ t.Fatalf("After deletion\nGot %#v\nWanted state %q", hk, Deleted)
+ }
+
+ case err.Error() != "foo":
+ t.Fatalf("Unexpected error: %v", err)
+ }
+}
+
type testHelper struct {
t *testing.T
}