...

Source file src/edge-infra.dev/pkg/lib/gcp/secretmanager/secretmanager.go

Documentation: edge-infra.dev/pkg/lib/gcp/secretmanager

     1  // Package secretmanager provides a set of functions to facilitate easily
     2  // working with the Secret Manager API without all of the gory details
     3  // of request handling
     4  package secretmanager
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"reflect"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	secretmanager "cloud.google.com/go/secretmanager/apiv1"
    16  	secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
    17  	"google.golang.org/api/iterator"
    18  	"google.golang.org/api/option"
    19  	"google.golang.org/grpc/codes"
    20  	"google.golang.org/grpc/status"
    21  	fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb"
    22  	"google.golang.org/protobuf/types/known/timestamppb"
    23  )
    24  
    25  // SecretManager contains a Client and a Project ID to make requests against
    26  type SecretManager struct {
    27  	Client    *secretmanager.Client
    28  	projectID string
    29  }
    30  
    31  // New creates a new SecretManager for a given project id, with the provided
    32  // googleAppCredsPath. if one isn't provided, it lets the google libraries
    33  // resolve auth
    34  func New(ctx context.Context, googleAppCredsPath string, projectID string) (SecretManager, error) {
    35  	var opts option.ClientOption
    36  	if googleAppCredsPath != "" {
    37  		opts = option.WithCredentialsFile(googleAppCredsPath)
    38  	}
    39  
    40  	client, err := secretmanager.NewClient(ctx, opts)
    41  	if err != nil {
    42  		return SecretManager{}, err
    43  	}
    44  
    45  	return SecretManager{
    46  		projectID: projectID,
    47  		Client:    client,
    48  	}, nil
    49  }
    50  
    51  // NewWithOptions creates a new SecretManager for a given project id, with the provided
    52  // options.
    53  func NewWithOptions(ctx context.Context, projectID string, opts ...option.ClientOption) (SecretManager, error) {
    54  	client, err := secretmanager.NewClient(ctx, opts...)
    55  	if err != nil {
    56  		return SecretManager{}, err
    57  	}
    58  
    59  	return SecretManager{
    60  		projectID: projectID,
    61  		Client:    client,
    62  	}, nil
    63  }
    64  
    65  // DecodeSecretManagerSecretVersion will attempt to resolve the true
    66  // version for the secret manager secret using the path /versions/<version>
    67  func DecodeSecretManagerSecretVersion(name string) (int, error) {
    68  	path := strings.Split(name, "/")
    69  	versionParsed := path[len(path)-1] // get version from secret manager secret path
    70  	return strconv.Atoi(versionParsed)
    71  }
    72  
    73  // GetSecret gets a secret by its name
    74  func (s SecretManager) GetSecret(ctx context.Context, secretID string) (*secretmanagerpb.Secret, error) {
    75  	req := &secretmanagerpb.GetSecretRequest{Name: s.secretRef(secretID)}
    76  
    77  	return s.Client.GetSecret(ctx, req)
    78  }
    79  
    80  // ListSecrets lists the secrets
    81  func (s SecretManager) ListSecrets(ctx context.Context, pageToken string) ([]*secretmanagerpb.Secret, error) {
    82  	var results []*secretmanagerpb.Secret
    83  	req := &secretmanagerpb.ListSecretsRequest{
    84  		Parent:    s.projectRef(),
    85  		PageToken: pageToken,
    86  	}
    87  	t1 := s.Client.ListSecrets(ctx, req)
    88  	for {
    89  		secret, err := t1.Next()
    90  		if err == iterator.Done {
    91  			return results, nil
    92  		}
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		results = append(results, secret)
    97  	}
    98  }
    99  
   100  // GetLatestSecretValue gets the latest version of a secret value
   101  func (s SecretManager) GetLatestSecretValue(ctx context.Context, secretID string) ([]byte, error) {
   102  	return s.GetSecretVersionValue(ctx, secretID, "latest")
   103  }
   104  
   105  // GetSecretVersionValue gets the latest version of a secret value
   106  func (s SecretManager) GetSecretVersionValue(ctx context.Context, secretID string, version string) ([]byte, error) {
   107  	getVersionReq := &secretmanagerpb.AccessSecretVersionRequest{
   108  		Name: fmt.Sprintf("%s/versions/%s", s.secretRef(secretID), version),
   109  	}
   110  	latestVersion, err := s.Client.AccessSecretVersion(ctx, getVersionReq)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	return latestVersion.Payload.Data, nil
   115  }
   116  
   117  // GetVersionSecretValueInfo gets the latest information about a secret versions value
   118  func (s SecretManager) GetSecretVersionValueInfo(ctx context.Context, secretID, version string) (*secretmanagerpb.SecretVersion, error) {
   119  	getVersionReq := &secretmanagerpb.GetSecretVersionRequest{
   120  		Name: fmt.Sprintf("%s/versions/%s", s.secretRef(secretID), version),
   121  	}
   122  	latestVersion, err := s.Client.GetSecretVersion(ctx, getVersionReq)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  	return latestVersion, nil
   127  }
   128  
   129  // GetLatestSecretValueInfo gets the latest information about a secret value
   130  func (s SecretManager) GetLatestSecretValueInfo(ctx context.Context, secretID string) (*secretmanagerpb.SecretVersion, error) {
   131  	return s.GetSecretVersionValueInfo(ctx, secretID, "latest")
   132  }
   133  
   134  // AddSecret adds secret values into Secret Manager:
   135  //   - will create the SecretManagerSecret if it doesnt exist
   136  //   - will add a new version if it does exist
   137  //   - will _not_ add a new version if provided secretValue matches the current
   138  //     active versions value
   139  func (s SecretManager) AddSecret(ctx context.Context, secretID string, secretValue []byte, labels map[string]string, forceLabelsUpdate bool, expireAt *time.Time, versionAlias string) error {
   140  	// first try to fetch the secret so we can determine what needs to be done
   141  	secret, err := s.GetSecret(ctx, secretID)
   142  	code := status.Code(err)
   143  	// create it if it wasnt found
   144  	if code == codes.NotFound {
   145  		secret, err = s.createNewSecret(ctx, secretID, labels, expireAt)
   146  		if err != nil {
   147  			return err
   148  		}
   149  
   150  		if err := s.addSecretVersion(ctx, secret, secretValue); err != nil {
   151  			return err
   152  		}
   153  		// update version alias if one is set
   154  		if versionAlias == "" {
   155  			return nil
   156  		}
   157  		return s.updateSecretVersionAlias(ctx, secret, secretID, versionAlias)
   158  	} else if code != codes.OK {
   159  		// if request failed for any other reason than not finding the secret,
   160  		// return error
   161  		return err
   162  	}
   163  
   164  	if !reflect.DeepEqual(secret.Labels, labels) {
   165  		if !forceLabelsUpdate {
   166  			return fmt.Errorf("this secret is out of date, please delete and recreate it")
   167  		}
   168  		secret.Labels = labels
   169  		updateSecretReq := &secretmanagerpb.UpdateSecretRequest{
   170  			Secret: secret,
   171  			UpdateMask: &fieldmaskpb.FieldMask{
   172  				Paths: []string{"labels"},
   173  			},
   174  		}
   175  		secret, err = s.Client.UpdateSecret(ctx, updateSecretReq)
   176  		if err != nil {
   177  			return err
   178  		}
   179  	}
   180  
   181  	// attempt to access version of resolved secret
   182  	getVersionReq := &secretmanagerpb.AccessSecretVersionRequest{
   183  		Name: fmt.Sprintf("%s/versions/latest", s.secretRef(secretID)),
   184  	}
   185  	latestVersion, err := s.Client.AccessSecretVersion(ctx, getVersionReq)
   186  	if err != nil {
   187  		return err
   188  	}
   189  	// check retrieved secret payload against secret value
   190  	if !bytes.Equal(secretValue, latestVersion.Payload.Data) {
   191  		if err = s.addSecretVersion(ctx, secret, secretValue); err != nil {
   192  			return err
   193  		}
   194  	}
   195  
   196  	// update version alias if one is set
   197  	if versionAlias == "" {
   198  		return nil
   199  	}
   200  	return s.updateSecretVersionAlias(ctx, secret, secretID, versionAlias)
   201  }
   202  
   203  // AddSecrets calls AddSecret for every secret:value pair in secrets. It attempts to add all given
   204  // secrets even if one fails, returning an error containing information about which secrets failed
   205  // to add
   206  func (s SecretManager) AddSecrets(ctx context.Context, secrets map[string][]byte) error {
   207  	var errs []string
   208  	for k, v := range secrets {
   209  		if err := s.AddSecret(ctx, k, v, nil, false, nil, ""); err != nil {
   210  			errs = append(errs, fmt.Sprintf("error adding secret '%s': %s", k, err.Error()))
   211  		}
   212  	}
   213  	if len(errs) > 0 {
   214  		return fmt.Errorf("adding secrets failed with error(s): %s", strings.Join(errs, ", "))
   215  	}
   216  	return nil
   217  }
   218  
   219  // DeleteSecret deletes the secret for the given ID
   220  func (s SecretManager) DeleteSecret(ctx context.Context, secretID string) error {
   221  	// Create the request to delete the secret.
   222  	deleteSecretReq := &secretmanagerpb.DeleteSecretRequest{
   223  		Name: s.secretRef(secretID),
   224  	}
   225  
   226  	return s.Client.DeleteSecret(ctx, deleteSecretReq)
   227  }
   228  
   229  func (s SecretManager) GetProjectID() string { return s.projectID }
   230  
   231  func (s *SecretManager) SetProjectID(newProjectID string) { s.projectID = newProjectID }
   232  
   233  func (s SecretManager) addSecretVersion(ctx context.Context, secret *secretmanagerpb.Secret, secretValue []byte) error {
   234  	// Build the request.
   235  	addSecretVersionReq := &secretmanagerpb.AddSecretVersionRequest{
   236  		Parent: secret.Name,
   237  		Payload: &secretmanagerpb.SecretPayload{
   238  			Data: secretValue,
   239  		},
   240  	}
   241  
   242  	// Call the API.
   243  	_, err := s.Client.AddSecretVersion(ctx, addSecretVersionReq)
   244  	if err != nil {
   245  		return err
   246  	}
   247  
   248  	return nil
   249  }
   250  
   251  func (s SecretManager) secretRef(secretID string) string {
   252  	return fmt.Sprintf("projects/%s/secrets/%s", s.projectID, secretID)
   253  }
   254  
   255  func (s SecretManager) projectRef() string {
   256  	return fmt.Sprintf("projects/%s", s.projectID)
   257  }
   258  
   259  func (s SecretManager) createNewSecret(ctx context.Context, secretID string, labels map[string]string, expireAt *time.Time) (*secretmanagerpb.Secret, error) {
   260  	createSecretReq := &secretmanagerpb.CreateSecretRequest{
   261  		Parent:   fmt.Sprintf("projects/%s", s.projectID),
   262  		SecretId: secretID,
   263  		Secret: &secretmanagerpb.Secret{
   264  			Replication: &secretmanagerpb.Replication{
   265  				Replication: &secretmanagerpb.Replication_Automatic_{
   266  					Automatic: &secretmanagerpb.Replication_Automatic{},
   267  				},
   268  			},
   269  			Labels: labels,
   270  		},
   271  	}
   272  
   273  	if expireAt != nil {
   274  		createSecretReq.Secret.Expiration = &secretmanagerpb.Secret_ExpireTime{
   275  			ExpireTime: timestamppb.New(*expireAt),
   276  		}
   277  	}
   278  	return s.Client.CreateSecret(ctx, createSecretReq)
   279  }
   280  
   281  func (s SecretManager) updateSecretVersionAlias(ctx context.Context, secret *secretmanagerpb.Secret, secretID, versionAlias string) error {
   282  	trueVersion, err := s.trueSecretVersion(ctx, secretID)
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	if secret.VersionAliases == nil {
   288  		secret.VersionAliases = map[string]int64{}
   289  	}
   290  	secret.VersionAliases[versionAlias] = int64(trueVersion)
   291  	updateSecretReq := &secretmanagerpb.UpdateSecretRequest{
   292  		Secret: secret,
   293  		UpdateMask: &fieldmaskpb.FieldMask{
   294  			Paths: []string{"version_aliases"},
   295  		},
   296  	}
   297  	_, err = s.Client.UpdateSecret(ctx, updateSecretReq)
   298  	return err
   299  }
   300  
   301  func (s SecretManager) trueSecretVersion(ctx context.Context, secretID string) (int, error) {
   302  	latestSecretInfo, err := s.GetLatestSecretValueInfo(ctx, secretID)
   303  	if err != nil && status.Code(err) != codes.NotFound {
   304  		return 1, err
   305  	} else if err != nil && status.Code(err) == codes.NotFound {
   306  		return 1, nil
   307  	}
   308  	// retrieve the true version for the secret and not its aliases i.e. latest
   309  	return DecodeSecretManagerSecretVersion(latestSecretInfo.Name)
   310  }
   311  

View as plain text