client.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package dvls
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "net/http"
  19. "strings"
  20. "github.com/Devolutions/go-dvls"
  21. corev1 "k8s.io/api/core/v1"
  22. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  23. )
  24. const errFailedToGetEntry = "failed to get entry: %w"
  25. var errNotImplemented = errors.New("not implemented")
  26. var _ esv1.SecretsClient = &Client{}
  27. // Client implements the SecretsClient interface for DVLS.
  28. type Client struct {
  29. dvls credentialClient
  30. }
  31. type credentialClient interface {
  32. GetByID(ctx context.Context, vaultID, entryID string) (dvls.Entry, error)
  33. Update(ctx context.Context, entry dvls.Entry) (dvls.Entry, error)
  34. DeleteByID(ctx context.Context, vaultID, entryID string) error
  35. }
  36. type realCredentialClient struct {
  37. cred *dvls.EntryCredentialService
  38. }
  39. func (r *realCredentialClient) GetByID(ctx context.Context, vaultID, entryID string) (dvls.Entry, error) {
  40. return r.cred.GetByIdWithContext(ctx, vaultID, entryID)
  41. }
  42. func (r *realCredentialClient) Update(ctx context.Context, entry dvls.Entry) (dvls.Entry, error) {
  43. return r.cred.UpdateWithContext(ctx, entry)
  44. }
  45. func (r *realCredentialClient) DeleteByID(ctx context.Context, vaultID, entryID string) error {
  46. return r.cred.DeleteByIdWithContext(ctx, vaultID, entryID)
  47. }
  48. // NewClient creates a new DVLS secrets client.
  49. func NewClient(dvlsClient credentialClient) *Client {
  50. return &Client{dvls: dvlsClient}
  51. }
  52. // GetSecret retrieves a secret from DVLS.
  53. func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  54. vaultID, entryID, err := c.parseSecretRef(ref.Key)
  55. if err != nil {
  56. return nil, err
  57. }
  58. entry, err := c.dvls.GetByID(ctx, vaultID, entryID)
  59. if isNotFoundError(err) {
  60. return nil, esv1.NoSecretErr
  61. }
  62. if err != nil {
  63. return nil, fmt.Errorf(errFailedToGetEntry, err)
  64. }
  65. secretMap, err := c.entryToSecretMap(entry)
  66. if err != nil {
  67. return nil, err
  68. }
  69. // Default to "password" when no property specified (consistent with 1Password provider).
  70. property := ref.Property
  71. if property == "" {
  72. property = "password"
  73. }
  74. value, ok := secretMap[property]
  75. if !ok {
  76. return nil, fmt.Errorf("property %q not found in entry", property)
  77. }
  78. return value, nil
  79. }
  80. // GetSecretMap retrieves all fields from a DVLS entry.
  81. func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  82. vaultID, entryID, err := c.parseSecretRef(ref.Key)
  83. if err != nil {
  84. return nil, err
  85. }
  86. entry, err := c.dvls.GetByID(ctx, vaultID, entryID)
  87. if isNotFoundError(err) {
  88. return nil, esv1.NoSecretErr
  89. }
  90. if err != nil {
  91. return nil, fmt.Errorf(errFailedToGetEntry, err)
  92. }
  93. return c.entryToSecretMap(entry)
  94. }
  95. // GetAllSecrets is not implemented for DVLS.
  96. func (c *Client) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  97. return nil, errNotImplemented
  98. }
  99. // PushSecret updates an existing entry's password field.
  100. func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  101. if secret == nil {
  102. return errors.New("secret is required for DVLS push")
  103. }
  104. vaultID, entryID, err := c.parseSecretRef(data.GetRemoteKey())
  105. if err != nil {
  106. return err
  107. }
  108. value, err := extractPushValue(secret, data)
  109. if err != nil {
  110. return err
  111. }
  112. existingEntry, err := c.dvls.GetByID(ctx, vaultID, entryID)
  113. if isNotFoundError(err) {
  114. return fmt.Errorf("entry %s not found in vault %s: entry must exist before pushing secrets", entryID, vaultID)
  115. }
  116. if err != nil {
  117. return fmt.Errorf(errFailedToGetEntry, err)
  118. }
  119. // SetCredentialSecret only updates the password/secret field.
  120. if err := existingEntry.SetCredentialSecret(string(value)); err != nil {
  121. return err
  122. }
  123. _, err = c.dvls.Update(ctx, existingEntry)
  124. if err != nil {
  125. return fmt.Errorf("failed to update entry: %w", err)
  126. }
  127. return nil
  128. }
  129. // DeleteSecret deletes a secret from DVLS.
  130. func (c *Client) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
  131. vaultID, entryID, err := c.parseSecretRef(ref.GetRemoteKey())
  132. if err != nil {
  133. return err
  134. }
  135. return c.dvls.DeleteByID(ctx, vaultID, entryID)
  136. }
  137. // SecretExists checks if a secret exists in DVLS.
  138. func (c *Client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  139. vaultID, entryID, err := c.parseSecretRef(ref.GetRemoteKey())
  140. if err != nil {
  141. return false, err
  142. }
  143. _, err = c.dvls.GetByID(ctx, vaultID, entryID)
  144. if isNotFoundError(err) {
  145. return false, nil
  146. }
  147. if err != nil {
  148. return false, err
  149. }
  150. return true, nil
  151. }
  152. // Validate checks if the client is properly configured.
  153. func (c *Client) Validate() (esv1.ValidationResult, error) {
  154. if c.dvls == nil {
  155. return esv1.ValidationResultError, errors.New("DVLS client is not initialized")
  156. }
  157. return esv1.ValidationResultReady, nil
  158. }
  159. // Close is a no-op for the DVLS client.
  160. func (c *Client) Close(_ context.Context) error {
  161. return nil
  162. }
  163. // parseSecretRef parses the secret reference key.
  164. // Format: "<vault-id>/<entry-id>".
  165. func (c *Client) parseSecretRef(key string) (vaultID, entryID string, err error) {
  166. parts := strings.SplitN(key, "/", 2)
  167. if len(parts) != 2 {
  168. return "", "", fmt.Errorf("invalid key format: expected '<vault-id>/<entry-id>', got %q", key)
  169. }
  170. vaultID = strings.TrimSpace(parts[0])
  171. entryID = strings.TrimSpace(parts[1])
  172. if vaultID == "" {
  173. return "", "", errors.New("vault ID cannot be empty")
  174. }
  175. if entryID == "" {
  176. return "", "", errors.New("entry ID cannot be empty")
  177. }
  178. return vaultID, entryID, nil
  179. }
  180. // entryToSecretMap converts a DVLS entry to a map of secret values.
  181. func (c *Client) entryToSecretMap(entry dvls.Entry) (map[string][]byte, error) {
  182. secretMap, err := entry.ToCredentialMap()
  183. if err != nil {
  184. return nil, err
  185. }
  186. result := make(map[string][]byte, len(secretMap))
  187. for k, v := range secretMap {
  188. result[k] = []byte(v)
  189. }
  190. return result, nil
  191. }
  192. func extractPushValue(secret *corev1.Secret, data esv1.PushSecretData) ([]byte, error) {
  193. if data.GetSecretKey() == "" {
  194. return nil, fmt.Errorf("secretKey is required for DVLS push")
  195. }
  196. if secret.Data == nil {
  197. return nil, fmt.Errorf("secret %q has no data", secret.Name)
  198. }
  199. value, ok := secret.Data[data.GetSecretKey()]
  200. if !ok {
  201. return nil, fmt.Errorf("key %q not found in secret %q", data.GetSecretKey(), secret.Name)
  202. }
  203. if len(value) == 0 {
  204. return nil, fmt.Errorf("key %q in secret %q is empty", data.GetSecretKey(), secret.Name)
  205. }
  206. return value, nil
  207. }
  208. func isNotFoundError(err error) bool {
  209. if err == nil {
  210. return false
  211. }
  212. if dvls.IsNotFound(err) {
  213. return true
  214. }
  215. var reqErr dvls.RequestError
  216. return errors.As(err, &reqErr) && reqErr.StatusCode == http.StatusNotFound
  217. }