// 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" storagepb "cloud.google.com/go/storage/internal/apiv2/stubs" "google.golang.org/api/iterator" raw "google.golang.org/api/storage/v1" ) // HMACState is the state of the HMAC key. // // This type is EXPERIMENTAL and subject to change or removal without notice. 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 or removal without notice. 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 or removal without notice. type HMACKeyHandle struct { projectID string accessID string retry *retryConfig raw *raw.ProjectsHmacKeysService } // HMACKeyHandle creates a handle that will be used for HMACKey operations. // // This method is EXPERIMENTAL and subject to change or removal without notice. func (c *Client) HMACKeyHandle(projectID, accessID string) *HMACKeyHandle { return &HMACKeyHandle{ projectID: projectID, accessID: accessID, retry: c.retry, raw: raw.NewProjectsHmacKeysService(c.raw), } } // Get invokes an RPC to retrieve the HMAC key referenced by the // HMACKeyHandle's accessID. // // Options such as UserProjectForHMACKeys can be used to set the // userProject to be billed against for operations. // // This method is EXPERIMENTAL and subject to change or removal without notice. func (hkh *HMACKeyHandle) Get(ctx context.Context, opts ...HMACKeyOption) (*HMACKey, error) { call := hkh.raw.Get(hkh.projectID, hkh.accessID) desc := new(hmacKeyDesc) for _, opt := range opts { opt.withHMACKeyDesc(desc) } if desc.userProjectID != "" { call = call.UserProject(desc.userProjectID) } setClientHeader(call.Header()) var metadata *raw.HmacKeyMetadata var err error err = run(ctx, func() error { metadata, err = call.Context(ctx).Do() return err }, hkh.retry, true, setRetryHeaderHTTP(call)) if err != nil { return nil, err } hk := &raw.HmacKey{ Metadata: metadata, } return toHMACKeyFromRaw(hk, 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. // // This method is EXPERIMENTAL and subject to change or removal without notice. func (hkh *HMACKeyHandle) Delete(ctx context.Context, opts ...HMACKeyOption) error { delCall := hkh.raw.Delete(hkh.projectID, hkh.accessID) desc := new(hmacKeyDesc) for _, opt := range opts { opt.withHMACKeyDesc(desc) } if desc.userProjectID != "" { delCall = delCall.UserProject(desc.userProjectID) } setClientHeader(delCall.Header()) return run(ctx, func() error { return delCall.Context(ctx).Do() }, hkh.retry, true, setRetryHeaderHTTP(delCall)) } func toHMACKeyFromRaw(hk *raw.HmacKey, updatedTimeCanBeNil bool) (*HMACKey, error) { hkmd := hk.Metadata if hkmd == nil { return nil, errors.New("field Metadata cannot be nil") } createdTime, err := time.Parse(time.RFC3339, hkmd.TimeCreated) if err != nil { return nil, fmt.Errorf("field CreatedTime: %v", err) } updatedTime, err := time.Parse(time.RFC3339, hkmd.Updated) if err != nil && !updatedTimeCanBeNil { return nil, fmt.Errorf("field UpdatedTime: %v", err) } hmKey := &HMACKey{ AccessID: hkmd.AccessId, Secret: hk.Secret, Etag: hkmd.Etag, ID: hkmd.Id, State: HMACState(hkmd.State), ProjectID: hkmd.ProjectId, CreatedTime: createdTime, UpdatedTime: updatedTime, ServiceAccountEmail: hkmd.ServiceAccountEmail, } return hmKey, nil } func toHMACKeyFromProto(pbmd *storagepb.HmacKeyMetadata) *HMACKey { if pbmd == nil { return nil } return &HMACKey{ AccessID: pbmd.GetAccessId(), ID: pbmd.GetId(), State: HMACState(pbmd.GetState()), ProjectID: pbmd.GetProject(), CreatedTime: convertProtoTime(pbmd.GetCreateTime()), UpdatedTime: convertProtoTime(pbmd.GetUpdateTime()), ServiceAccountEmail: pbmd.GetServiceAccountEmail(), } } // CreateHMACKey invokes an RPC for Google Cloud Storage to create a new HMACKey. // // This method is EXPERIMENTAL and subject to change or removal without notice. func (c *Client) CreateHMACKey(ctx context.Context, projectID, serviceAccountEmail string, opts ...HMACKeyOption) (*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) desc := new(hmacKeyDesc) for _, opt := range opts { opt.withHMACKeyDesc(desc) } if desc.userProjectID != "" { call = call.UserProject(desc.userProjectID) } setClientHeader(call.Header()) var hk *raw.HmacKey if err := run(ctx, func() error { h, err := call.Context(ctx).Do() hk = h return err }, c.retry, false, setRetryHeaderHTTP(call)); err != nil { return nil, err } return toHMACKeyFromRaw(hk, true) } // HMACKeyAttrsToUpdate defines the attributes of an HMACKey that will be updated. // // This type is EXPERIMENTAL and subject to change or removal without notice. 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. // // This method is EXPERIMENTAL and subject to change or removal without notice. func (h *HMACKeyHandle) Update(ctx context.Context, au HMACKeyAttrsToUpdate, opts ...HMACKeyOption) (*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), }) desc := new(hmacKeyDesc) for _, opt := range opts { opt.withHMACKeyDesc(desc) } if desc.userProjectID != "" { call = call.UserProject(desc.userProjectID) } setClientHeader(call.Header()) var metadata *raw.HmacKeyMetadata var err error isIdempotent := len(au.Etag) > 0 err = run(ctx, func() error { metadata, err = call.Context(ctx).Do() return err }, h.retry, isIdempotent, setRetryHeaderHTTP(call)) if err != nil { return nil, err } hk := &raw.HmacKey{ Metadata: metadata, } return toHMACKeyFromRaw(hk, false) } // An HMACKeysIterator is an iterator over HMACKeys. // // Note: This iterator is not safe for concurrent operations without explicit synchronization. // // This type is EXPERIMENTAL and subject to change or removal without notice. type HMACKeysIterator struct { ctx context.Context raw *raw.ProjectsHmacKeysService projectID string hmacKeys []*HMACKey pageInfo *iterator.PageInfo nextFunc func() error index int desc hmacKeyDesc retry *retryConfig } // ListHMACKeys returns an iterator for listing HMACKeys. // // Note: This iterator is not safe for concurrent operations without explicit synchronization. // // This method is EXPERIMENTAL and subject to change or removal without notice. func (c *Client) ListHMACKeys(ctx context.Context, projectID string, opts ...HMACKeyOption) *HMACKeysIterator { it := &HMACKeysIterator{ ctx: ctx, raw: raw.NewProjectsHmacKeysService(c.raw), projectID: projectID, retry: c.retry, } for _, opt := range opts { opt.withHMACKeyDesc(&it.desc) } it.pageInfo, it.nextFunc = iterator.NewPageInfo( it.fetch, func() int { return len(it.hmacKeys) - it.index }, func() interface{} { prev := it.hmacKeys it.hmacKeys = it.hmacKeys[:0] it.index = 0 return prev }) return it } // Next returns the next result. Its second return value is iterator.Done if // there are no more results. Once Next returns iterator.Done, all subsequent // calls will return iterator.Done. // // Note: This iterator is not safe for concurrent operations without explicit synchronization. // // This method is EXPERIMENTAL and subject to change or removal without notice. func (it *HMACKeysIterator) Next() (*HMACKey, error) { if err := it.nextFunc(); err != nil { return nil, err } key := it.hmacKeys[it.index] it.index++ return key, nil } // PageInfo supports pagination. See the google.golang.org/api/iterator package for details. // // Note: This iterator is not safe for concurrent operations without explicit synchronization. // // This method is EXPERIMENTAL and subject to change or removal without notice. func (it *HMACKeysIterator) PageInfo() *iterator.PageInfo { return it.pageInfo } func (it *HMACKeysIterator) fetch(pageSize int, pageToken string) (token string, err error) { // TODO: Remove fetch method upon integration. This method is internalized into // httpStorageClient.ListHMACKeys() as it is the only caller. call := it.raw.List(it.projectID) setClientHeader(call.Header()) if pageToken != "" { call = call.PageToken(pageToken) } if it.desc.showDeletedKeys { call = call.ShowDeletedKeys(true) } if it.desc.userProjectID != "" { call = call.UserProject(it.desc.userProjectID) } if it.desc.forServiceAccountEmail != "" { call = call.ServiceAccountEmail(it.desc.forServiceAccountEmail) } if pageSize > 0 { call = call.MaxResults(int64(pageSize)) } ctx := it.ctx var resp *raw.HmacKeysMetadata err = run(it.ctx, func() error { resp, err = call.Context(ctx).Do() return err }, it.retry, true, setRetryHeaderHTTP(call)) if err != nil { return "", err } for _, metadata := range resp.Items { hk := &raw.HmacKey{ Metadata: metadata, } hkey, err := toHMACKeyFromRaw(hk, true) if err != nil { return "", err } it.hmacKeys = append(it.hmacKeys, hkey) } return resp.NextPageToken, nil } type hmacKeyDesc struct { forServiceAccountEmail string showDeletedKeys bool userProjectID string } // HMACKeyOption configures the behavior of HMACKey related methods and actions. // // This interface is EXPERIMENTAL and subject to change or removal without notice. type HMACKeyOption interface { withHMACKeyDesc(*hmacKeyDesc) } type hmacKeyDescFunc func(*hmacKeyDesc) func (hkdf hmacKeyDescFunc) withHMACKeyDesc(hkd *hmacKeyDesc) { hkdf(hkd) } // ForHMACKeyServiceAccountEmail returns HMAC Keys that are // associated with the email address of a service account in the project. // // Only one service account email can be used as a filter, so if multiple // of these options are applied, the last email to be set will be used. // // This option is EXPERIMENTAL and subject to change or removal without notice. func ForHMACKeyServiceAccountEmail(serviceAccountEmail string) HMACKeyOption { return hmacKeyDescFunc(func(hkd *hmacKeyDesc) { hkd.forServiceAccountEmail = serviceAccountEmail }) } // ShowDeletedHMACKeys will also list keys whose state is "DELETED". // // This option is EXPERIMENTAL and subject to change or removal without notice. func ShowDeletedHMACKeys() HMACKeyOption { return hmacKeyDescFunc(func(hkd *hmacKeyDesc) { hkd.showDeletedKeys = true }) } // UserProjectForHMACKeys will bill the request against userProjectID // if userProjectID is non-empty. // // Note: This is a noop right now and only provided for API compatibility. // // This option is EXPERIMENTAL and subject to change or removal without notice. func UserProjectForHMACKeys(userProjectID string) HMACKeyOption { return hmacKeyDescFunc(func(hkd *hmacKeyDesc) { hkd.userProjectID = userProjectID }) }