- add validation callback will enable the host service to validate the newly created key before the deleting the old key - wrapped both the call to validate permissions and validate callback in a retry because sometimes it takes some time for the newly created key to be active on GCP services. This throws an error and prevents host catalog creation
185 lines
5.8 KiB
Go
185 lines
5.8 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package credential
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary-plugin-gcp/internal/values"
|
|
"google.golang.org/api/option"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
type CredentialType int
|
|
|
|
// PersistedState is the persisted state for the GCP credential.
|
|
type PersistedState struct {
|
|
// CredentialsConfig is the credential configuration for the GCP credential.
|
|
CredentialsConfig *Config
|
|
// CredsLastRotatedTime is the last rotation of service account key for the GCP credential.
|
|
CredsLastRotatedTime time.Time
|
|
}
|
|
|
|
// NewPersistedState - create a new PersistedState
|
|
func NewPersistedState(opt ...Option) (*PersistedState, error) {
|
|
s := new(PersistedState)
|
|
opts, err := getOpts(opt...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.CredentialsConfig = opts.WithCredentialsConfig
|
|
s.CredsLastRotatedTime = opts.WithCredsLastRotatedTime
|
|
return s, nil
|
|
}
|
|
|
|
// RotateCreds rotates the credentials for the GCP catalog and updates the last rotated time.
|
|
func (s *PersistedState) RotateCreds(
|
|
ctx context.Context,
|
|
permissions []string,
|
|
validateCredsCallback ValidateCredsCallback,
|
|
opts ...option.ClientOption) error {
|
|
if s.CredentialsConfig == nil {
|
|
return status.Error(codes.InvalidArgument, "missing credentials config")
|
|
}
|
|
if !s.CredentialsConfig.IsRotatable() {
|
|
return status.Error(codes.InvalidArgument, "cannot rotate non-rotatable credentials")
|
|
}
|
|
|
|
err := s.CredentialsConfig.RotateServiceAccountKey(ctx, permissions, validateCredsCallback, opts...)
|
|
if err != nil {
|
|
return status.Errorf(codes.Internal, "failed to rotate credentials: %v", err)
|
|
}
|
|
s.CredsLastRotatedTime = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReplaceCreds replaces the private key in the state with a new key.
|
|
// If the existing key was rotated at any point in time, it is
|
|
// deleted first, otherwise it's left alone. This method returns a
|
|
// status error with PluginError details.
|
|
func (s *PersistedState) ReplaceCreds(ctx context.Context, newCreds *Config, opts ...option.ClientOption) error {
|
|
if newCreds == nil {
|
|
return status.Errorf(codes.InvalidArgument, "missing new credentials")
|
|
}
|
|
if s.CredentialsConfig == nil {
|
|
return status.Errorf(codes.InvalidArgument, "missing existing credentials")
|
|
}
|
|
|
|
// Delete old/existing credentials.
|
|
// This is done with the same credentials to ensure that it has the proper permissions to do it.
|
|
if !s.CredsLastRotatedTime.IsZero() && s.CredentialsConfig.IsRotatable() {
|
|
if err := s.DeleteCreds(ctx, opts...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Set the new attributes and clear the rotated time.
|
|
s.CredentialsConfig = newCreds
|
|
s.CredsLastRotatedTime = time.Time{}
|
|
return nil
|
|
}
|
|
|
|
// DeleteCreds deletes the credentials in the state. The access key
|
|
// ID, secret access key, and rotation time fields are zeroed out in
|
|
// the state just to ensure that they cannot be re-used after. This
|
|
// method returns a status error with PluginError details.
|
|
func (s *PersistedState) DeleteCreds(ctx context.Context, opts ...option.ClientOption) error {
|
|
if s.CredentialsConfig == nil {
|
|
return status.Errorf(codes.InvalidArgument, "missing credentials config")
|
|
}
|
|
if !s.CredentialsConfig.IsRotatable() {
|
|
return status.Errorf(codes.InvalidArgument, "cannot delete credentials for non-rotatable credentials")
|
|
}
|
|
|
|
err := s.CredentialsConfig.DeletePrivateKey(ctx, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.CredentialsConfig = nil
|
|
s.CredsLastRotatedTime = time.Time{}
|
|
return nil
|
|
}
|
|
|
|
// ToMap returns a map of the credentials stored in the persisted state.
|
|
// ToMap will return a map for long-term credentials with following keys:
|
|
// private_key_id, private_key & creds_last_rotated_time
|
|
func (s *PersistedState) ToMap() map[string]any {
|
|
if !s.CredentialsConfig.IsRotatable() {
|
|
return map[string]any{}
|
|
}
|
|
return map[string]any{
|
|
ConstPrivateKey: s.CredentialsConfig.PrivateKey,
|
|
ConstPrivateKeyId: s.CredentialsConfig.PrivateKeyId,
|
|
ConstCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
|
|
}
|
|
}
|
|
|
|
// PersistedStateFromProto parses values out of a protobuf struct input
|
|
// and returns a PersistedState used for GCP authentication.
|
|
func PersistedStateFromProto(secrets *structpb.Struct, attrs *CredentialAttributes, opts ...Option) (*PersistedState, error) {
|
|
if secrets == nil {
|
|
secrets = &structpb.Struct{
|
|
Fields: map[string]*structpb.Value{},
|
|
}
|
|
}
|
|
|
|
if attrs == nil {
|
|
return nil, fmt.Errorf("missing credential attributes")
|
|
}
|
|
|
|
privateKeyId, err := values.GetStringValue(secrets, ConstPrivateKeyId, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
|
}
|
|
|
|
privateKey, err := values.GetStringValue(secrets, ConstPrivateKey, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
|
}
|
|
|
|
credsLastRotatedTime, err := values.GetTimeValue(secrets, ConstCredsLastRotatedTime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("persisted state integrity error: %w", err)
|
|
}
|
|
|
|
s, err := NewPersistedState(opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfgOpts := []Option{}
|
|
if privateKeyId != "" && privateKey != "" {
|
|
cfgOpts = append(cfgOpts,
|
|
WithPrivateKey(privateKey),
|
|
WithPrivateKeyId(privateKeyId))
|
|
}
|
|
|
|
if attrs.Zone != "" {
|
|
cfgOpts = append(cfgOpts, WithZone(attrs.Zone))
|
|
}
|
|
if attrs.ProjectId != "" {
|
|
cfgOpts = append(cfgOpts, WithProjectId(attrs.ProjectId))
|
|
}
|
|
if attrs.ClientEmail != "" {
|
|
cfgOpts = append(cfgOpts, WithClientEmail(attrs.ClientEmail))
|
|
}
|
|
if attrs.TargetServiceAccountId != "" {
|
|
cfgOpts = append(cfgOpts, WithTargetServiceAccountId(attrs.TargetServiceAccountId))
|
|
}
|
|
credentialsConfig, err := NewConfig(cfgOpts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.CredentialsConfig = credentialsConfig
|
|
s.CredsLastRotatedTime = credsLastRotatedTime
|
|
|
|
return s, nil
|
|
}
|