| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- /*
- Copyright © The ESO Authors
- 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
- https://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 bitwarden
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "gopkg.in/yaml.v3"
- corev1 "k8s.io/api/core/v1"
- "k8s.io/kube-openapi/pkg/validation/strfmt"
- "k8s.io/utils/ptr"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- "github.com/external-secrets/external-secrets/runtime/esutils"
- )
- const (
- // NoteMetadataKey defines the note for the pushed secret.
- NoteMetadataKey = "note"
- errNoProvider = "store does not have a provider"
- )
- var (
- errFailedToGetAllSecrets = "failed to get all secrets: %w"
- errFailedToGetSecret = "failed to get secret: %w"
- )
- // PushSecret will write a single secret into the provider.
- // Note: We will refuse to overwrite ANY secrets, because we can never be completely
- // sure if it's the same secret we are trying to push. We only have the Name and the value
- // could be different. Therefore, we will always create a new secret. Except if, the value
- // the key, the note, and organization ID all match.
- // We only allow to push to a single project, because GET returns a single project ID
- // the secret belongs to even though technically Create allows multiple projects. This is
- // to ensure that we push to the same project always, and so we can determine reliably that
- // we don't need to push again.
- func (p *Provider) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
- spec := p.store.GetSpec()
- if spec == nil || spec.Provider == nil {
- return errors.New(errNoProvider)
- }
- if data.GetRemoteKey() == "" {
- return errors.New("remote key must be defined")
- }
- value, err := esutils.ExtractSecretData(data, secret)
- if err != nil {
- return fmt.Errorf("failed to extract secret data: %w", err)
- }
- note, err := esutils.FetchValueFromMetadata(NoteMetadataKey, data.GetMetadata(), "")
- if err != nil {
- return fmt.Errorf("failed to fetch note from metadata: %w", err)
- }
- // ListAll Secrets for an organization. If the key matches our key, we GetSecret that and do a compare.
- remoteSecrets, err := p.bitwardenSdkClient.ListSecrets(ctx, spec.Provider.BitwardenSecretsManager.OrganizationID)
- if err != nil {
- return fmt.Errorf(errFailedToGetAllSecrets, err)
- }
- for _, d := range remoteSecrets.Data {
- if d.Key != data.GetRemoteKey() {
- continue
- }
- sec, err := p.bitwardenSdkClient.GetSecret(ctx, d.ID)
- if err != nil {
- return fmt.Errorf("failed to get secret: %w", err)
- }
- // If all pushed data matches, we won't push this secret.
- if p.isExactlySameSecret(sec, data.GetRemoteKey(), note, spec.Provider.BitwardenSecretsManager.ProjectID, value) {
- // we have a complete match, skip pushing.
- return nil
- } else if p.isOnlyValueDifferent(sec, data.GetRemoteKey(), note, spec.Provider.BitwardenSecretsManager.ProjectID, value) {
- // only the value is different, update the existing secret.
- _, err = p.bitwardenSdkClient.UpdateSecret(ctx, SecretPutRequest{
- ID: sec.ID,
- Key: data.GetRemoteKey(),
- Note: note,
- OrganizationID: spec.Provider.BitwardenSecretsManager.OrganizationID,
- ProjectIDs: []string{spec.Provider.BitwardenSecretsManager.ProjectID},
- Value: string(value),
- })
- return err
- }
- }
- // no matching secret found, let's create it
- _, err = p.bitwardenSdkClient.CreateSecret(ctx, SecretCreateRequest{
- Key: data.GetRemoteKey(),
- Note: note,
- OrganizationID: spec.Provider.BitwardenSecretsManager.OrganizationID,
- ProjectIDs: []string{spec.Provider.BitwardenSecretsManager.ProjectID},
- Value: string(value),
- })
- return err
- }
- func (p *Provider) isExactlySameSecret(sec *SecretResponse, remoteKey, note, projectID string, value []byte) bool {
- return sec.Key == remoteKey &&
- sec.Value == string(value) &&
- sec.Note == note &&
- ptr.Deref(sec.ProjectID, "") == projectID
- }
- func (p *Provider) isOnlyValueDifferent(sec *SecretResponse, remoteKey, note, projectID string, value []byte) bool {
- return sec.Key == remoteKey &&
- sec.Value != string(value) &&
- sec.Note == note &&
- ptr.Deref(sec.ProjectID, "") == projectID
- }
- // GetSecret returns a single secret from the provider.
- func (p *Provider) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
- if strfmt.IsUUID(ref.Key) {
- resp, err := p.bitwardenSdkClient.GetSecret(ctx, ref.Key)
- if err != nil {
- return nil, fmt.Errorf(errFailedToGetSecret, err)
- }
- return []byte(resp.Value), nil
- }
- spec := p.store.GetSpec()
- if spec == nil || spec.Provider == nil {
- return nil, errors.New(errNoProvider)
- }
- secret, err := p.findSecretByRef(ctx, ref.Key, spec.Provider.BitwardenSecretsManager.ProjectID)
- if err != nil {
- return nil, fmt.Errorf(errFailedToGetSecret, err)
- }
- if secret == nil {
- return nil, fmt.Errorf("no secret found for project id %s and name %s", spec.Provider.BitwardenSecretsManager.ProjectID, ref.Key)
- }
- // we found our secret, return the value for it
- return []byte(secret.Value), nil
- }
- // DeleteSecret deletes a secret from Bitwarden.
- func (p *Provider) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
- if strfmt.IsUUID(ref.GetRemoteKey()) {
- return p.deleteSecret(ctx, ref.GetRemoteKey())
- }
- spec := p.store.GetSpec()
- if spec == nil || spec.Provider == nil {
- return errors.New(errNoProvider)
- }
- secret, err := p.findSecretByRef(ctx, ref.GetRemoteKey(), spec.Provider.BitwardenSecretsManager.ProjectID)
- if err != nil {
- return fmt.Errorf(errFailedToGetSecret, err)
- }
- if secret == nil {
- return fmt.Errorf("no secret found for project id %s and name %s", spec.Provider.BitwardenSecretsManager.ProjectID, ref.GetRemoteKey())
- }
- return p.deleteSecret(ctx, secret.ID)
- }
- func (p *Provider) deleteSecret(ctx context.Context, id string) error {
- resp, err := p.bitwardenSdkClient.DeleteSecret(ctx, []string{id})
- if err != nil {
- return fmt.Errorf("error deleting secret: %w", err)
- }
- var errs error
- for _, data := range resp.Data {
- if data.Error != nil {
- errs = errors.Join(errs, fmt.Errorf("error deleting secret with id %s: %s", data.ID, *data.Error))
- }
- }
- if errs != nil {
- return fmt.Errorf("there were one or more errors deleting secrets: %w", errs)
- }
- return nil
- }
- // SecretExists checks if a secret exists in Bitwarden.
- func (p *Provider) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
- if strfmt.IsUUID(ref.GetRemoteKey()) {
- _, err := p.bitwardenSdkClient.GetSecret(ctx, ref.GetRemoteKey())
- if err != nil {
- return false, fmt.Errorf(errFailedToGetSecret, err)
- }
- return true, nil
- }
- spec := p.store.GetSpec()
- if spec == nil || spec.Provider == nil {
- return false, errors.New(errNoProvider)
- }
- secret, err := p.findSecretByRef(ctx, ref.GetRemoteKey(), spec.Provider.BitwardenSecretsManager.ProjectID)
- if err != nil {
- return false, fmt.Errorf(errFailedToGetSecret, err)
- }
- if secret == nil {
- return false, nil
- }
- return true, nil
- }
- // GetSecretMap returns multiple k/v pairs from the provider.
- func (p *Provider) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
- data, err := p.GetSecret(ctx, ref)
- if err != nil {
- return nil, err
- }
- if err := yaml.Unmarshal(data, map[string]any{}); err == nil {
- return p.parseYamlSecretData(data)
- }
- kv := make(map[string]json.RawMessage)
- if err := json.Unmarshal(data, &kv); err != nil {
- return nil, fmt.Errorf("error unmarshalling secret: %w", 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 (p *Provider) parseYamlSecretData(data []byte) (map[string][]byte, error) {
- kv := make(map[string]any)
- if err := yaml.Unmarshal(data, &kv); err != nil {
- return nil, fmt.Errorf("error unmarshalling secret: %w", err)
- }
- secretData := make(map[string][]byte)
- for k, v := range kv {
- switch t := v.(type) {
- case string:
- secretData[k] = []byte(t)
- case []byte:
- secretData[k] = t
- case map[string]any:
- d, err := yaml.Marshal(t)
- if err != nil {
- return nil, fmt.Errorf("error marshaling secret: %w", err)
- }
- secretData[k] = bytes.TrimSpace(d)
- default:
- secretData[k] = fmt.Appendf(nil, "%v", t) // Convert to string and then []byte
- }
- }
- return secretData, nil
- }
- // GetAllSecrets gets multiple secrets from the provider and loads into a kubernetes secret.
- // First load all secrets from secretStore path configuration
- // Then, gets secrets from a matching name or matching custom_metadata.
- func (p *Provider) GetAllSecrets(ctx context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
- spec := p.store.GetSpec()
- if spec == nil {
- return nil, errors.New(errNoProvider)
- }
- secrets, err := p.bitwardenSdkClient.ListSecrets(ctx, spec.Provider.BitwardenSecretsManager.OrganizationID)
- if err != nil {
- return nil, fmt.Errorf(errFailedToGetAllSecrets, err)
- }
- result := map[string][]byte{}
- for _, d := range secrets.Data {
- sec, err := p.bitwardenSdkClient.GetSecret(ctx, d.ID)
- if err != nil {
- return nil, fmt.Errorf(errFailedToGetSecret, err)
- }
- result[d.ID] = []byte(sec.Value)
- }
- return result, nil
- }
- // Validate validates the provider.
- func (p *Provider) Validate() (esv1.ValidationResult, error) {
- return esv1.ValidationResultReady, nil
- }
- // Close closes the provider.
- func (p *Provider) Close(_ context.Context) error {
- return nil
- }
- func (p *Provider) findSecretByRef(ctx context.Context, key, projectID string) (*SecretResponse, error) {
- spec := p.store.GetSpec()
- if spec == nil || spec.Provider == nil {
- return nil, errors.New(errNoProvider)
- }
- // ListAll Secrets for an organization. If the key matches our key, we GetSecret that and do a compare.
- secrets, err := p.bitwardenSdkClient.ListSecrets(ctx, spec.Provider.BitwardenSecretsManager.OrganizationID)
- if err != nil {
- return nil, fmt.Errorf(errFailedToGetAllSecrets, err)
- }
- var remoteSecret *SecretResponse
- for _, d := range secrets.Data {
- if d.Key != key {
- continue
- }
- sec, err := p.bitwardenSdkClient.GetSecret(ctx, d.ID)
- if err != nil {
- return nil, fmt.Errorf(errFailedToGetSecret, err)
- }
- if sec.ProjectID != nil && *sec.ProjectID == projectID {
- if remoteSecret != nil {
- return nil, fmt.Errorf("more than one secret found for project %s with key %s", projectID, key)
- }
- // We don't break here because we WANT TO MAKE SURE that there is ONLY ONE
- // such secret.
- remoteSecret = sec
- }
- }
- return remoteSecret, nil
- }
|