Files
threshold-plugin-gcp/internal/credential/state.go
T
Elim Tsiagbey de9ee3c408 feat: enhance service account key rotation with validation callback
- 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
2024-12-17 11:22:55 -05:00

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
}