// Package secretmanager provides a set of functions to facilitate easily // working with the Secret Manager API without all of the gory details // of request handling package secretmanager import ( "bytes" "context" "fmt" "reflect" "strconv" "strings" "time" secretmanager "cloud.google.com/go/secretmanager/apiv1" secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "google.golang.org/api/iterator" "google.golang.org/api/option" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" "google.golang.org/protobuf/types/known/timestamppb" ) // SecretManager contains a Client and a Project ID to make requests against type SecretManager struct { Client *secretmanager.Client projectID string } // New creates a new SecretManager for a given project id, with the provided // googleAppCredsPath. if one isn't provided, it lets the google libraries // resolve auth func New(ctx context.Context, googleAppCredsPath string, projectID string) (SecretManager, error) { var opts option.ClientOption if googleAppCredsPath != "" { opts = option.WithCredentialsFile(googleAppCredsPath) } client, err := secretmanager.NewClient(ctx, opts) if err != nil { return SecretManager{}, err } return SecretManager{ projectID: projectID, Client: client, }, nil } // NewWithOptions creates a new SecretManager for a given project id, with the provided // options. func NewWithOptions(ctx context.Context, projectID string, opts ...option.ClientOption) (SecretManager, error) { client, err := secretmanager.NewClient(ctx, opts...) if err != nil { return SecretManager{}, err } return SecretManager{ projectID: projectID, Client: client, }, nil } // DecodeSecretManagerSecretVersion will attempt to resolve the true // version for the secret manager secret using the path /versions/ func DecodeSecretManagerSecretVersion(name string) (int, error) { path := strings.Split(name, "/") versionParsed := path[len(path)-1] // get version from secret manager secret path return strconv.Atoi(versionParsed) } // GetSecret gets a secret by its name func (s SecretManager) GetSecret(ctx context.Context, secretID string) (*secretmanagerpb.Secret, error) { req := &secretmanagerpb.GetSecretRequest{Name: s.secretRef(secretID)} return s.Client.GetSecret(ctx, req) } // ListSecrets lists the secrets func (s SecretManager) ListSecrets(ctx context.Context, pageToken string) ([]*secretmanagerpb.Secret, error) { var results []*secretmanagerpb.Secret req := &secretmanagerpb.ListSecretsRequest{ Parent: s.projectRef(), PageToken: pageToken, } t1 := s.Client.ListSecrets(ctx, req) for { secret, err := t1.Next() if err == iterator.Done { return results, nil } if err != nil { return nil, err } results = append(results, secret) } } // GetLatestSecretValue gets the latest version of a secret value func (s SecretManager) GetLatestSecretValue(ctx context.Context, secretID string) ([]byte, error) { return s.GetSecretVersionValue(ctx, secretID, "latest") } // GetSecretVersionValue gets the latest version of a secret value func (s SecretManager) GetSecretVersionValue(ctx context.Context, secretID string, version string) ([]byte, error) { getVersionReq := &secretmanagerpb.AccessSecretVersionRequest{ Name: fmt.Sprintf("%s/versions/%s", s.secretRef(secretID), version), } latestVersion, err := s.Client.AccessSecretVersion(ctx, getVersionReq) if err != nil { return nil, err } return latestVersion.Payload.Data, nil } // GetVersionSecretValueInfo gets the latest information about a secret versions value func (s SecretManager) GetSecretVersionValueInfo(ctx context.Context, secretID, version string) (*secretmanagerpb.SecretVersion, error) { getVersionReq := &secretmanagerpb.GetSecretVersionRequest{ Name: fmt.Sprintf("%s/versions/%s", s.secretRef(secretID), version), } latestVersion, err := s.Client.GetSecretVersion(ctx, getVersionReq) if err != nil { return nil, err } return latestVersion, nil } // GetLatestSecretValueInfo gets the latest information about a secret value func (s SecretManager) GetLatestSecretValueInfo(ctx context.Context, secretID string) (*secretmanagerpb.SecretVersion, error) { return s.GetSecretVersionValueInfo(ctx, secretID, "latest") } // AddSecret adds secret values into Secret Manager: // - will create the SecretManagerSecret if it doesnt exist // - will add a new version if it does exist // - will _not_ add a new version if provided secretValue matches the current // active versions value func (s SecretManager) AddSecret(ctx context.Context, secretID string, secretValue []byte, labels map[string]string, forceLabelsUpdate bool, expireAt *time.Time, versionAlias string) error { // first try to fetch the secret so we can determine what needs to be done secret, err := s.GetSecret(ctx, secretID) code := status.Code(err) // create it if it wasnt found if code == codes.NotFound { secret, err = s.createNewSecret(ctx, secretID, labels, expireAt) if err != nil { return err } if err := s.addSecretVersion(ctx, secret, secretValue); err != nil { return err } // update version alias if one is set if versionAlias == "" { return nil } return s.updateSecretVersionAlias(ctx, secret, secretID, versionAlias) } else if code != codes.OK { // if request failed for any other reason than not finding the secret, // return error return err } if !reflect.DeepEqual(secret.Labels, labels) { if !forceLabelsUpdate { return fmt.Errorf("this secret is out of date, please delete and recreate it") } secret.Labels = labels updateSecretReq := &secretmanagerpb.UpdateSecretRequest{ Secret: secret, UpdateMask: &fieldmaskpb.FieldMask{ Paths: []string{"labels"}, }, } secret, err = s.Client.UpdateSecret(ctx, updateSecretReq) if err != nil { return err } } // attempt to access version of resolved secret getVersionReq := &secretmanagerpb.AccessSecretVersionRequest{ Name: fmt.Sprintf("%s/versions/latest", s.secretRef(secretID)), } latestVersion, err := s.Client.AccessSecretVersion(ctx, getVersionReq) if err != nil { return err } // check retrieved secret payload against secret value if !bytes.Equal(secretValue, latestVersion.Payload.Data) { if err = s.addSecretVersion(ctx, secret, secretValue); err != nil { return err } } // update version alias if one is set if versionAlias == "" { return nil } return s.updateSecretVersionAlias(ctx, secret, secretID, versionAlias) } // AddSecrets calls AddSecret for every secret:value pair in secrets. It attempts to add all given // secrets even if one fails, returning an error containing information about which secrets failed // to add func (s SecretManager) AddSecrets(ctx context.Context, secrets map[string][]byte) error { var errs []string for k, v := range secrets { if err := s.AddSecret(ctx, k, v, nil, false, nil, ""); err != nil { errs = append(errs, fmt.Sprintf("error adding secret '%s': %s", k, err.Error())) } } if len(errs) > 0 { return fmt.Errorf("adding secrets failed with error(s): %s", strings.Join(errs, ", ")) } return nil } // DeleteSecret deletes the secret for the given ID func (s SecretManager) DeleteSecret(ctx context.Context, secretID string) error { // Create the request to delete the secret. deleteSecretReq := &secretmanagerpb.DeleteSecretRequest{ Name: s.secretRef(secretID), } return s.Client.DeleteSecret(ctx, deleteSecretReq) } func (s SecretManager) GetProjectID() string { return s.projectID } func (s *SecretManager) SetProjectID(newProjectID string) { s.projectID = newProjectID } func (s SecretManager) addSecretVersion(ctx context.Context, secret *secretmanagerpb.Secret, secretValue []byte) error { // Build the request. addSecretVersionReq := &secretmanagerpb.AddSecretVersionRequest{ Parent: secret.Name, Payload: &secretmanagerpb.SecretPayload{ Data: secretValue, }, } // Call the API. _, err := s.Client.AddSecretVersion(ctx, addSecretVersionReq) if err != nil { return err } return nil } func (s SecretManager) secretRef(secretID string) string { return fmt.Sprintf("projects/%s/secrets/%s", s.projectID, secretID) } func (s SecretManager) projectRef() string { return fmt.Sprintf("projects/%s", s.projectID) } func (s SecretManager) createNewSecret(ctx context.Context, secretID string, labels map[string]string, expireAt *time.Time) (*secretmanagerpb.Secret, error) { createSecretReq := &secretmanagerpb.CreateSecretRequest{ Parent: fmt.Sprintf("projects/%s", s.projectID), SecretId: secretID, Secret: &secretmanagerpb.Secret{ Replication: &secretmanagerpb.Replication{ Replication: &secretmanagerpb.Replication_Automatic_{ Automatic: &secretmanagerpb.Replication_Automatic{}, }, }, Labels: labels, }, } if expireAt != nil { createSecretReq.Secret.Expiration = &secretmanagerpb.Secret_ExpireTime{ ExpireTime: timestamppb.New(*expireAt), } } return s.Client.CreateSecret(ctx, createSecretReq) } func (s SecretManager) updateSecretVersionAlias(ctx context.Context, secret *secretmanagerpb.Secret, secretID, versionAlias string) error { trueVersion, err := s.trueSecretVersion(ctx, secretID) if err != nil { return err } if secret.VersionAliases == nil { secret.VersionAliases = map[string]int64{} } secret.VersionAliases[versionAlias] = int64(trueVersion) updateSecretReq := &secretmanagerpb.UpdateSecretRequest{ Secret: secret, UpdateMask: &fieldmaskpb.FieldMask{ Paths: []string{"version_aliases"}, }, } _, err = s.Client.UpdateSecret(ctx, updateSecretReq) return err } func (s SecretManager) trueSecretVersion(ctx context.Context, secretID string) (int, error) { latestSecretInfo, err := s.GetLatestSecretValueInfo(ctx, secretID) if err != nil && status.Code(err) != codes.NotFound { return 1, err } else if err != nil && status.Code(err) == codes.NotFound { return 1, nil } // retrieve the true version for the secret and not its aliases i.e. latest return DecodeSecretManagerSecretVersion(latestSecretInfo.Name) }