| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- /*
- 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 secretmanager
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "strconv"
- "strings"
- secretmanager "cloud.google.com/go/secretmanager/apiv1"
- "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
- "github.com/googleapis/gax-go/v2"
- "github.com/googleapis/gax-go/v2/apierror"
- "github.com/tidwall/gjson"
- "google.golang.org/api/iterator"
- "google.golang.org/grpc/codes"
- ctrl "sigs.k8s.io/controller-runtime"
- kclient "sigs.k8s.io/controller-runtime/pkg/client"
- esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
- "github.com/external-secrets/external-secrets/pkg/constants"
- "github.com/external-secrets/external-secrets/pkg/find"
- "github.com/external-secrets/external-secrets/pkg/metrics"
- "github.com/external-secrets/external-secrets/pkg/utils"
- )
- const (
- CloudPlatformRole = "https://www.googleapis.com/auth/cloud-platform"
- defaultVersion = "latest"
- errGCPSMStore = "received invalid GCPSM SecretStore resource"
- errUnableGetCredentials = "unable to get credentials: %w"
- errClientClose = "unable to close SecretManager client: %w"
- errMissingStoreSpec = "invalid: missing store spec"
- errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
- errMissingSAK = "missing SecretAccessKey"
- errUnableProcessJSONCredentials = "failed to process the provided JSON credentials: %w"
- errUnableCreateGCPSMClient = "failed to create GCP secretmanager client: %w"
- errUninitalizedGCPProvider = "provider GCP is not initialized"
- errClientGetSecretAccess = "unable to access Secret from SecretManager Client: %w"
- errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
- errInvalidStore = "invalid store"
- errInvalidStoreSpec = "invalid store spec"
- errInvalidStoreProv = "invalid store provider"
- errInvalidGCPProv = "invalid gcp secrets manager provider"
- errInvalidAuthSecretRef = "invalid auth secret ref: %w"
- errInvalidWISARef = "invalid workload identity service account reference: %w"
- errUnexpectedFindOperator = "unexpected find operator"
- )
- type Client struct {
- smClient GoogleSecretManagerClient
- kube kclient.Client
- store *esv1beta1.GCPSMProvider
- storeKind string
- // namespace of the external secret
- namespace string
- workloadIdentity *workloadIdentity
- }
- type GoogleSecretManagerClient interface {
- DeleteSecret(ctx context.Context, req *secretmanagerpb.DeleteSecretRequest, opts ...gax.CallOption) error
- AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error)
- ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator
- AddSecretVersion(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error)
- CreateSecret(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
- Close() error
- GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
- }
- var log = ctrl.Log.WithName("provider").WithName("gcp").WithName("secretsmanager")
- func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
- var gcpSecret *secretmanagerpb.Secret
- var err error
- gcpSecret, err = c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
- Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
- })
- metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGetSecret, err)
- var gErr *apierror.APIError
- if errors.As(err, &gErr) {
- if gErr.GRPCStatus().Code() == codes.NotFound {
- return nil
- }
- return err
- }
- if err != nil {
- return err
- }
- manager, ok := gcpSecret.Labels["managed-by"]
- if !ok || manager != "external-secrets" {
- return nil
- }
- deleteSecretVersionReq := &secretmanagerpb.DeleteSecretRequest{
- Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
- }
- err = c.smClient.DeleteSecret(ctx, deleteSecretVersionReq)
- metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMDeleteSecret, err)
- return err
- }
- func parseError(err error) error {
- var gerr *apierror.APIError
- if errors.As(err, &gerr) && gerr.GRPCStatus().Code() == codes.NotFound {
- return esv1beta1.NoSecretError{}
- }
- return err
- }
- // PushSecret pushes a kubernetes secret key into gcp provider Secret.
- func (c *Client) PushSecret(ctx context.Context, payload []byte, remoteRef esv1beta1.PushRemoteRef) error {
- createSecretReq := &secretmanagerpb.CreateSecretRequest{
- Parent: fmt.Sprintf("projects/%s", c.store.ProjectID),
- SecretId: remoteRef.GetRemoteKey(),
- Secret: &secretmanagerpb.Secret{
- Labels: map[string]string{
- "managed-by": "external-secrets",
- },
- Replication: &secretmanagerpb.Replication{
- Replication: &secretmanagerpb.Replication_Automatic_{
- Automatic: &secretmanagerpb.Replication_Automatic{},
- },
- },
- },
- }
- var gcpSecret *secretmanagerpb.Secret
- var err error
- gcpSecret, err = c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
- Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
- })
- metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGetSecret, err)
- var gErr *apierror.APIError
- if err != nil && errors.As(err, &gErr) {
- if gErr.GRPCStatus().Code() == codes.NotFound {
- gcpSecret, err = c.smClient.CreateSecret(ctx, createSecretReq)
- metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMCreateSecret, err)
- if err != nil {
- return err
- }
- } else {
- return err
- }
- }
- manager, ok := gcpSecret.Labels["managed-by"]
- if !ok || manager != "external-secrets" {
- return fmt.Errorf("secret %v is not managed by external secrets", remoteRef.GetRemoteKey())
- }
- gcpVersion, err := c.smClient.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
- Name: fmt.Sprintf("projects/%s/secrets/%s/versions/latest", c.store.ProjectID, remoteRef.GetRemoteKey()),
- })
- metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAccessSecretVersion, err)
- if errors.As(err, &gErr) {
- if err != nil && gErr.GRPCStatus().Code() != codes.NotFound {
- return err
- }
- } else if err != nil {
- return err
- }
- if gcpVersion != nil && gcpVersion.Payload != nil && bytes.Equal(payload, gcpVersion.Payload.Data) {
- return nil
- }
- addSecretVersionReq := &secretmanagerpb.AddSecretVersionRequest{
- Parent: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
- Payload: &secretmanagerpb.SecretPayload{
- Data: payload,
- },
- }
- _, err = c.smClient.AddSecretVersion(ctx, addSecretVersionReq)
- metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAddSecretVersion, err)
- if err != nil {
- return err
- }
- return nil
- }
- // GetAllSecrets syncs multiple secrets from gcp provider into a single Kubernetes Secret.
- func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
- if ref.Name != nil {
- return c.findByName(ctx, ref)
- }
- if len(ref.Tags) > 0 {
- return c.findByTags(ctx, ref)
- }
- return nil, errors.New(errUnexpectedFindOperator)
- }
- func (c *Client) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
- // regex matcher
- matcher, err := find.New(*ref.Name)
- if err != nil {
- return nil, err
- }
- req := &secretmanagerpb.ListSecretsRequest{
- Parent: fmt.Sprintf("projects/%s", c.store.ProjectID),
- }
- if ref.Path != nil {
- req.Filter = fmt.Sprintf("name:%s", *ref.Path)
- }
- // Call the API.
- it := c.smClient.ListSecrets(ctx, req)
- secretMap := make(map[string][]byte)
- var resp *secretmanagerpb.Secret
- defer metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMListSecrets, err)
- for {
- resp, err = it.Next()
- if errors.Is(err, iterator.Done) {
- break
- }
- if err != nil {
- return nil, fmt.Errorf("failed to list secrets: %w", err)
- }
- log.V(1).Info("gcp sm findByName found", "secrets", strconv.Itoa(it.PageInfo().Remaining()))
- key := c.trimName(resp.Name)
- // If we don't match we skip.
- // Also, if we have path, and it is not at the beguining we skip.
- // We have to check if path is at the beguining of the key because
- // there is no way to create a `name:%s*` (starts with) filter
- // At https://cloud.google.com/secret-manager/docs/filtering you can use `*`
- // but not like that it seems.
- if !matcher.MatchName(key) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
- continue
- }
- log.V(1).Info("gcp sm findByName matches", "name", resp.Name)
- secretMap[key], err = c.getData(ctx, key)
- if err != nil {
- return nil, err
- }
- }
- return utils.ConvertKeys(ref.ConversionStrategy, secretMap)
- }
- func (c *Client) getData(ctx context.Context, key string) ([]byte, error) {
- dataRef := esv1beta1.ExternalSecretDataRemoteRef{
- Key: key,
- }
- data, err := c.GetSecret(ctx, dataRef)
- if err != nil {
- return []byte(""), err
- }
- return data, nil
- }
- func (c *Client) findByTags(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
- var tagFilter string
- for k, v := range ref.Tags {
- tagFilter = fmt.Sprintf("%slabels.%s=%s ", tagFilter, k, v)
- }
- tagFilter = strings.TrimSuffix(tagFilter, " ")
- if ref.Path != nil {
- tagFilter = fmt.Sprintf("%s name:%s", tagFilter, *ref.Path)
- }
- req := &secretmanagerpb.ListSecretsRequest{
- Parent: fmt.Sprintf("projects/%s", c.store.ProjectID),
- }
- log.V(1).Info("gcp sm findByTags", "tagFilter", tagFilter)
- req.Filter = tagFilter
- // Call the API.
- it := c.smClient.ListSecrets(ctx, req)
- var resp *secretmanagerpb.Secret
- var err error
- defer metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMListSecrets, err)
- secretMap := make(map[string][]byte)
- for {
- resp, err = it.Next()
- if errors.Is(err, iterator.Done) {
- break
- }
- if err != nil {
- return nil, fmt.Errorf("failed to list secrets: %w", err)
- }
- key := c.trimName(resp.Name)
- if ref.Path != nil && !strings.HasPrefix(key, *ref.Path) {
- continue
- }
- log.V(1).Info("gcp sm findByTags matches tags", "name", resp.Name)
- secretMap[key], err = c.getData(ctx, key)
- if err != nil {
- return nil, err
- }
- }
- return utils.ConvertKeys(ref.ConversionStrategy, secretMap)
- }
- func (c *Client) trimName(name string) string {
- projectIDNumuber := c.extractProjectIDNumber(name)
- key := strings.TrimPrefix(name, fmt.Sprintf("projects/%s/secrets/", projectIDNumuber))
- return key
- }
- // extractProjectIDNumber grabs the project id from the full name returned by gcp api
- // gcp api seems to always return the number and not the project name
- // (and users would always use the name, while requests accept both).
- func (c *Client) extractProjectIDNumber(secretFullName string) string {
- s := strings.Split(secretFullName, "/")
- ProjectIDNumuber := s[1]
- return ProjectIDNumuber
- }
- // GetSecret returns a single secret from the provider.
- func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
- if utils.IsNil(c.smClient) || c.store.ProjectID == "" {
- return nil, fmt.Errorf(errUninitalizedGCPProvider)
- }
- if ref.MetadataPolicy == esv1beta1.ExternalSecretMetadataPolicyFetch {
- return c.getSecretMetadata(ctx, ref)
- }
- version := ref.Version
- if version == "" {
- version = defaultVersion
- }
- req := &secretmanagerpb.AccessSecretVersionRequest{
- Name: fmt.Sprintf("projects/%s/secrets/%s/versions/%s", c.store.ProjectID, ref.Key, version),
- }
- result, err := c.smClient.AccessSecretVersion(ctx, req)
- metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAccessSecretVersion, err)
- err = parseError(err)
- if err != nil {
- return nil, fmt.Errorf(errClientGetSecretAccess, err)
- }
- if ref.Property == "" {
- if result.Payload.Data != nil {
- return result.Payload.Data, nil
- }
- return nil, fmt.Errorf("invalid secret received. no secret string for key: %s", ref.Key)
- }
- var payload string
- if result.Payload.Data != nil {
- payload = string(result.Payload.Data)
- }
- idx := strings.Index(ref.Property, ".")
- refProperty := ref.Property
- if idx > 0 {
- refProperty = strings.ReplaceAll(refProperty, ".", "\\.")
- val := gjson.Get(payload, refProperty)
- if val.Exists() {
- return []byte(val.String()), nil
- }
- }
- val := gjson.Get(payload, ref.Property)
- if !val.Exists() {
- return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
- }
- return []byte(val.String()), nil
- }
- func (c *Client) getSecretMetadata(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
- secret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
- Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, ref.Key),
- })
- err = parseError(err)
- if err != nil {
- return nil, fmt.Errorf(errClientGetSecretAccess, err)
- }
- const (
- annotations = "annotations"
- labels = "labels"
- )
- extractMetadataKey := func(s string, p string) string {
- prefix := p + "."
- if !strings.HasPrefix(s, prefix) {
- return ""
- }
- return strings.TrimPrefix(s, prefix)
- }
- if annotation := extractMetadataKey(ref.Property, annotations); annotation != "" {
- v, ok := secret.GetAnnotations()[annotation]
- if !ok {
- return nil, fmt.Errorf("annotation with key %s does not exist in secret %s", annotation, ref.Key)
- }
- return []byte(v), nil
- }
- if label := extractMetadataKey(ref.Property, labels); label != "" {
- v, ok := secret.GetLabels()[label]
- if !ok {
- return nil, fmt.Errorf("label with key %s does not exist in secret %s", label, ref.Key)
- }
- return []byte(v), nil
- }
- if ref.Property == annotations {
- j, err := json.Marshal(secret.GetAnnotations())
- if err != nil {
- return nil, fmt.Errorf("faild marshaling annotations into json: %w", err)
- }
- return j, nil
- }
- if ref.Property == labels {
- j, err := json.Marshal(secret.GetLabels())
- if err != nil {
- return nil, fmt.Errorf("faild marshaling labels into json: %w", err)
- }
- return j, nil
- }
- if ref.Property != "" {
- return nil, fmt.Errorf("invalid property %s: metadata property should start with either %s or %s", ref.Property, annotations, labels)
- }
- j, err := json.Marshal(map[string]map[string]string{
- "annotations": secret.GetAnnotations(),
- "labels": secret.GetLabels(),
- })
- if err != nil {
- return nil, fmt.Errorf("faild marshaling metadata map into json: %w", err)
- }
- return j, nil
- }
- // GetSecretMap returns multiple k/v pairs from the provider.
- func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
- if c.smClient == nil || c.store.ProjectID == "" {
- return nil, fmt.Errorf(errUninitalizedGCPProvider)
- }
- data, err := c.GetSecret(ctx, ref)
- if err != nil {
- return nil, err
- }
- kv := make(map[string]json.RawMessage)
- err = json.Unmarshal(data, &kv)
- if err != nil {
- return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
- }
- secretData := make(map[string][]byte)
- for k, v := range kv {
- var strVal string
- err = json.Unmarshal(v, &strVal)
- if err == nil {
- secretData[k] = []byte(strVal)
- } else {
- secretData[k] = v
- }
- }
- return secretData, nil
- }
- func (c *Client) Close(_ context.Context) error {
- var err error
- if c.smClient != nil {
- err = c.smClient.Close()
- }
- if c.workloadIdentity != nil {
- err = c.workloadIdentity.Close()
- }
- useMu.Unlock()
- if err != nil {
- return fmt.Errorf(errClientClose, err)
- }
- return nil
- }
- func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
- if c.storeKind == esv1beta1.ClusterSecretStoreKind && isReferentSpec(c.store) {
- return esv1beta1.ValidationResultUnknown, nil
- }
- return esv1beta1.ValidationResultReady, nil
- }
|