client_push.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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 vault
  14. import (
  15. "bytes"
  16. "context"
  17. "encoding/json"
  18. "errors"
  19. "fmt"
  20. "maps"
  21. corev1 "k8s.io/api/core/v1"
  22. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  23. "github.com/external-secrets/external-secrets/pkg/constants"
  24. "github.com/external-secrets/external-secrets/pkg/esutils"
  25. "github.com/external-secrets/external-secrets/pkg/metrics"
  26. )
  27. func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  28. var (
  29. value []byte
  30. err error
  31. )
  32. key := data.GetSecretKey()
  33. if key == "" {
  34. // Must convert secret values to string, otherwise data will be sent as base64 to Vault
  35. secretStringVal := make(map[string]string)
  36. for k, v := range secret.Data {
  37. secretStringVal[k] = string(v)
  38. }
  39. value, err = esutils.JSONMarshal(secretStringVal)
  40. if err != nil {
  41. return fmt.Errorf("failed to serialize secret content as JSON: %w", err)
  42. }
  43. } else {
  44. value = secret.Data[key]
  45. }
  46. label := map[string]any{
  47. "custom_metadata": map[string]string{
  48. "managed-by": "external-secrets",
  49. },
  50. }
  51. secretVal := make(map[string]any)
  52. path := c.buildPath(data.GetRemoteKey())
  53. metaPath, err := c.buildMetadataPath(data.GetRemoteKey())
  54. if err != nil {
  55. return err
  56. }
  57. // Retrieve the secret map from vault and convert the secret value in string form.
  58. vaultSecret, err := c.readSecret(ctx, path, "")
  59. // If error is not of type secret not found, we should error
  60. if err != nil && !errors.Is(err, esv1.NoSecretError{}) {
  61. return err
  62. }
  63. secretExists := err == nil
  64. // If the secret exists, we should check if it is managed by external-secrets
  65. if secretExists {
  66. metadata, err := c.readSecretMetadata(ctx, data.GetRemoteKey())
  67. if err != nil {
  68. return err
  69. }
  70. manager, ok := metadata["managed-by"]
  71. if !ok || manager != "external-secrets" {
  72. return errors.New("secret not managed by external-secrets")
  73. }
  74. // Remove the metadata map to check the reconcile difference
  75. if c.store.Version == esv1.VaultKVStoreV1 {
  76. delete(vaultSecret, "custom_metadata")
  77. }
  78. // Only compare the entire secret if we're pushing the whole secret (not a single property)
  79. if data.GetProperty() == "" {
  80. // Convert incoming value to map for proper JSON comparison
  81. var incomingSecretMap map[string]any
  82. err = json.Unmarshal(value, &incomingSecretMap)
  83. if err != nil {
  84. return fmt.Errorf("error unmarshalling incoming secret value: %w", err)
  85. }
  86. // Compare maps instead of raw bytes to handle JSON field ordering and formatting
  87. if maps.Equal(vaultSecret, incomingSecretMap) {
  88. return nil
  89. }
  90. }
  91. }
  92. // If a Push of a property only, we should merge and add/update the property
  93. if data.GetProperty() != "" {
  94. if _, ok := vaultSecret[data.GetProperty()]; ok {
  95. d, ok := vaultSecret[data.GetProperty()].(string)
  96. if !ok {
  97. return fmt.Errorf("error converting %s to string", data.GetProperty())
  98. }
  99. // If the property has the same value, don't update the secret
  100. if bytes.Equal([]byte(d), value) {
  101. return nil
  102. }
  103. }
  104. maps.Insert(secretVal, maps.All(vaultSecret))
  105. // Secret got from vault is already on map[string]string format
  106. secretVal[data.GetProperty()] = string(value)
  107. } else {
  108. err = json.Unmarshal(value, &secretVal)
  109. if err != nil {
  110. return fmt.Errorf("error unmarshalling vault secret: %w", err)
  111. }
  112. }
  113. secretToPush := secretVal
  114. // Adding custom_metadata to the secret for KV v1
  115. if c.store.Version == esv1.VaultKVStoreV1 {
  116. secretToPush["custom_metadata"] = label["custom_metadata"]
  117. }
  118. if c.store.Version == esv1.VaultKVStoreV2 {
  119. secretToPush = map[string]any{
  120. "data": secretVal,
  121. }
  122. // Add CAS options if required
  123. if c.store.CheckAndSet != nil && c.store.CheckAndSet.Required {
  124. casVersion, casErr := c.getCASVersion(ctx, data.GetRemoteKey(), secretExists)
  125. if casErr != nil {
  126. return fmt.Errorf("failed to get CAS version: %w", casErr)
  127. }
  128. secretToPush["options"] = map[string]any{
  129. "cas": casVersion,
  130. }
  131. }
  132. }
  133. if err != nil {
  134. return fmt.Errorf("failed to convert value to a valid JSON: %w", err)
  135. }
  136. // Secret metadata should be pushed separately only for KV2
  137. if c.store.Version == esv1.VaultKVStoreV2 {
  138. _, err = c.logical.WriteWithContext(ctx, metaPath, label)
  139. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultWriteSecretData, err)
  140. if err != nil {
  141. return err
  142. }
  143. }
  144. // Otherwise, create or update the version.
  145. _, err = c.logical.WriteWithContext(ctx, path, secretToPush)
  146. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultWriteSecretData, err)
  147. return err
  148. }
  149. func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
  150. path := c.buildPath(remoteRef.GetRemoteKey())
  151. metaPath, err := c.buildMetadataPath(remoteRef.GetRemoteKey())
  152. if err != nil {
  153. return err
  154. }
  155. // Retrieve the secret map from vault and convert the secret value in string form.
  156. secretVal, err := c.readSecret(ctx, path, "")
  157. // If error is not of type secret not found, we should error
  158. if err != nil && errors.Is(err, esv1.NoSecretError{}) {
  159. return nil
  160. }
  161. if err != nil {
  162. return err
  163. }
  164. metadata, err := c.readSecretMetadata(ctx, remoteRef.GetRemoteKey())
  165. if err != nil {
  166. return err
  167. }
  168. manager, ok := metadata["managed-by"]
  169. if !ok || manager != "external-secrets" {
  170. return nil
  171. }
  172. // If Push for a Property, we need to delete the property and update the secret
  173. if remoteRef.GetProperty() != "" {
  174. delete(secretVal, remoteRef.GetProperty())
  175. // If the only key left in the remote secret is the reference of the metadata.
  176. if c.store.Version == esv1.VaultKVStoreV1 && len(secretVal) == 1 {
  177. delete(secretVal, "custom_metadata")
  178. }
  179. if len(secretVal) > 0 {
  180. secretToPush := secretVal
  181. if c.store.Version == esv1.VaultKVStoreV2 {
  182. secretToPush = map[string]any{
  183. "data": secretVal,
  184. }
  185. }
  186. _, err = c.logical.WriteWithContext(ctx, path, secretToPush)
  187. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultDeleteSecret, err)
  188. return err
  189. }
  190. }
  191. _, err = c.logical.DeleteWithContext(ctx, path)
  192. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultDeleteSecret, err)
  193. if err != nil {
  194. return fmt.Errorf("could not delete secret %v: %w", remoteRef.GetRemoteKey(), err)
  195. }
  196. if c.store.Version == esv1.VaultKVStoreV2 {
  197. _, err = c.logical.DeleteWithContext(ctx, metaPath)
  198. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultDeleteSecret, err)
  199. if err != nil {
  200. return fmt.Errorf("could not delete secret metadata %v: %w", remoteRef.GetRemoteKey(), err)
  201. }
  202. }
  203. return nil
  204. }
  205. // getCASVersion retrieves the current version of the secret for check-and-set operations.
  206. // Returns:
  207. // - 0 for new secrets (CAS version 0 means "create only if doesn't exist")
  208. // - N for existing secrets (CAS version N means "update only if current version is N")
  209. func (c *client) getCASVersion(ctx context.Context, remoteKey string, secretExists bool) (int, error) {
  210. // For new secrets, use CAS version 0 (create only if doesn't exist)
  211. if !secretExists {
  212. return 0, nil
  213. }
  214. // For existing secrets, read the full metadata to get current version
  215. metaPath, err := c.buildMetadataPath(remoteKey)
  216. if err != nil {
  217. return 0, fmt.Errorf("failed to build metadata path: %w", err)
  218. }
  219. secret, err := c.logical.ReadWithDataWithContext(ctx, metaPath, nil)
  220. if err != nil {
  221. return 0, fmt.Errorf("failed to read metadata: %w", err)
  222. }
  223. if secret == nil || secret.Data == nil {
  224. // If no metadata found for an existing secret, assume this is version 1.
  225. // This can happen with older secrets that were created before version tracking.
  226. // Vault KV v2 secrets start at version 1 (not 0) when first created.
  227. return 1, nil
  228. }
  229. return getCurrentVersionFromMetadata(secret.Data)
  230. }
  231. func getCurrentVersionFromMetadata(data map[string]any) (int, error) {
  232. var err error
  233. if currentVersion, ok := data["current_version"]; ok {
  234. switch v := currentVersion.(type) {
  235. case int:
  236. return v, nil
  237. case float64:
  238. return int(v), nil
  239. case json.Number:
  240. if intVal, err := v.Int64(); err == nil {
  241. return int(intVal), nil
  242. }
  243. return 0, fmt.Errorf("failed to convert json.Number to int: %w", err)
  244. default:
  245. return 0, fmt.Errorf("unexpected type for current_version: %T", currentVersion)
  246. }
  247. }
  248. // If metadata exists but no current_version found, assume this is version 1.
  249. // This handles edge cases with legacy secrets or incomplete metadata.
  250. // Vault KV v2 secrets start at version 1, so this is the safest assumption.
  251. return 1, nil
  252. }