client_get.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "strings"
  20. "github.com/tidwall/gjson"
  21. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  22. "github.com/external-secrets/external-secrets/pkg/constants"
  23. "github.com/external-secrets/external-secrets/pkg/esutils"
  24. "github.com/external-secrets/external-secrets/pkg/metrics"
  25. )
  26. const (
  27. errReadSecret = "cannot read secret data from Vault: %w"
  28. errDataField = "failed to find data field"
  29. errJSONUnmarshall = "failed to unmarshall JSON"
  30. errPathInvalid = "provided Path isn't a valid kv v2 path"
  31. errUnsupportedMetadataKvVersion = "cannot perform metadata fetch operations with kv version v1"
  32. errNotFound = "secret not found"
  33. errSecretKeyFmt = "cannot find secret data for key: %q"
  34. )
  35. // GetSecret supports two types:
  36. // 1. get the full secret as json-encoded value
  37. // by leaving the ref.Property empty.
  38. // 2. get a key from the secret.
  39. // Nested values are supported by specifying a gjson expression
  40. func (c *client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  41. var data map[string]any
  42. var err error
  43. if ref.MetadataPolicy == esv1.ExternalSecretMetadataPolicyFetch {
  44. if c.store.Version == esv1.VaultKVStoreV1 {
  45. return nil, errors.New(errUnsupportedMetadataKvVersion)
  46. }
  47. metadata, err := c.readSecretMetadata(ctx, ref.Key)
  48. if err != nil {
  49. return nil, err
  50. }
  51. if len(metadata) == 0 {
  52. return nil, nil
  53. }
  54. data = make(map[string]any, len(metadata))
  55. for k, v := range metadata {
  56. data[k] = v
  57. }
  58. } else {
  59. data, err = c.readSecret(ctx, ref.Key, ref.Version)
  60. if err != nil {
  61. return nil, err
  62. }
  63. }
  64. return getSecretValue(data, ref.Property)
  65. }
  66. // GetSecretMap supports two modes of operation:
  67. // 1. get the full secret from the vault data payload (by leaving .property empty).
  68. // 2. extract key/value pairs from a (nested) object.
  69. func (c *client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  70. data, err := c.GetSecret(ctx, ref)
  71. if err != nil {
  72. return nil, err
  73. }
  74. var secretData map[string]any
  75. err = json.Unmarshal(data, &secretData)
  76. if err != nil {
  77. return nil, err
  78. }
  79. byteMap := make(map[string][]byte, len(secretData))
  80. for k := range secretData {
  81. byteMap[k], err = esutils.GetByteValueFromMap(secretData, k)
  82. if err != nil {
  83. return nil, err
  84. }
  85. }
  86. return byteMap, nil
  87. }
  88. func (c *client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  89. path := c.buildPath(ref.GetRemoteKey())
  90. data, err := c.readSecret(ctx, path, "")
  91. if err != nil {
  92. if errors.Is(err, esv1.NoSecretError{}) {
  93. return false, nil
  94. }
  95. return false, err
  96. }
  97. value, err := getSecretValue(data, ref.GetProperty())
  98. if err != nil {
  99. if errors.Is(err, esv1.NoSecretError{}) || err.Error() == fmt.Sprintf(errSecretKeyFmt, ref.GetProperty()) {
  100. return false, nil
  101. }
  102. return false, err
  103. }
  104. return value != nil, nil
  105. }
  106. func (c *client) readSecret(ctx context.Context, path, version string) (map[string]any, error) {
  107. dataPath := c.buildPath(path)
  108. // path formated according to vault docs for v1 and v2 API
  109. // v1: https://www.vaultproject.io/api-docs/secret/kv/kv-v1#read-secret
  110. // v2: https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
  111. var params map[string][]string
  112. if version != "" {
  113. params = make(map[string][]string)
  114. params["version"] = []string{version}
  115. }
  116. vaultSecret, err := c.logical.ReadWithDataWithContext(ctx, dataPath, params)
  117. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultReadSecretData, err)
  118. if err != nil {
  119. return nil, fmt.Errorf(errReadSecret, err)
  120. }
  121. if vaultSecret == nil {
  122. return nil, esv1.NoSecretError{}
  123. }
  124. secretData := vaultSecret.Data
  125. if c.store.Version == esv1.VaultKVStoreV2 {
  126. // Vault KV2 has data embedded within sub-field
  127. // reference - https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
  128. dataInt, ok := vaultSecret.Data["data"]
  129. if !ok {
  130. return nil, errors.New(errDataField)
  131. }
  132. if dataInt == nil {
  133. return nil, esv1.NoSecretError{}
  134. }
  135. secretData, ok = dataInt.(map[string]any)
  136. if !ok {
  137. return nil, errors.New(errJSONUnmarshall)
  138. }
  139. }
  140. return secretData, nil
  141. }
  142. func getSecretValue(data map[string]any, property string) ([]byte, error) {
  143. if data == nil {
  144. return nil, esv1.NoSecretError{}
  145. }
  146. jsonStr, err := json.Marshal(data)
  147. if err != nil {
  148. return nil, err
  149. }
  150. // (1): return raw json if no property is defined
  151. if property == "" {
  152. return jsonStr, nil
  153. }
  154. // For backwards compatibility we want the
  155. // actual keys to take precedence over gjson syntax
  156. // (2): extract key from secret with property
  157. if _, ok := data[property]; ok {
  158. return esutils.GetByteValueFromMap(data, property)
  159. }
  160. // (3): extract key from secret using gjson
  161. val := gjson.Get(string(jsonStr), property)
  162. if !val.Exists() {
  163. return nil, fmt.Errorf(errSecretKeyFmt, property)
  164. }
  165. return []byte(val.String()), nil
  166. }
  167. func (c *client) readSecretMetadata(ctx context.Context, path string) (map[string]string, error) {
  168. metadata := make(map[string]string)
  169. url, err := c.buildMetadataPath(path)
  170. if err != nil {
  171. return nil, err
  172. }
  173. secret, err := c.logical.ReadWithDataWithContext(ctx, url, nil)
  174. metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultReadSecretData, err)
  175. if err != nil {
  176. return nil, fmt.Errorf(errReadSecret, err)
  177. }
  178. if secret == nil {
  179. return nil, errors.New(errNotFound)
  180. }
  181. t, ok := secret.Data["custom_metadata"]
  182. if !ok {
  183. return nil, nil
  184. }
  185. d, ok := t.(map[string]any)
  186. if !ok {
  187. return metadata, nil
  188. }
  189. for k, v := range d {
  190. metadata[k] = v.(string)
  191. }
  192. return metadata, nil
  193. }
  194. func (c *client) buildMetadataPath(path string) (string, error) {
  195. var url string
  196. if c.store.Version == esv1.VaultKVStoreV1 {
  197. url = fmt.Sprintf("%s/%s", *c.store.Path, path)
  198. } else { // KV v2 is used
  199. if c.store.Path == nil && !strings.Contains(path, "data") {
  200. return "", errors.New(errPathInvalid)
  201. }
  202. if c.store.Path == nil {
  203. path = strings.Replace(path, "/data/", "/metadata/", 1)
  204. url = path
  205. } else {
  206. url = fmt.Sprintf("%s/metadata/%s", *c.store.Path, path)
  207. }
  208. }
  209. return url, nil
  210. }
  211. /*
  212. buildPath is a helper method to build the vault equivalent path
  213. from ExternalSecrets and SecretStore manifests. the path build logic
  214. varies depending on the SecretStore KV version:
  215. Example inputs/outputs:
  216. # simple build:
  217. kv version == "v2":
  218. provider_path: "secret/path"
  219. input: "foo"
  220. output: "secret/path/data/foo" # provider_path and data are prepended
  221. kv version == "v1":
  222. provider_path: "secret/path"
  223. input: "foo"
  224. output: "secret/path/foo" # provider_path is prepended
  225. # inheriting paths:
  226. kv version == "v2":
  227. provider_path: "secret/path"
  228. input: "secret/path/foo"
  229. output: "secret/path/data/foo" #data is prepended
  230. kv version == "v2":
  231. provider_path: "secret/path"
  232. input: "secret/path/data/foo"
  233. output: "secret/path/data/foo" #noop
  234. kv version == "v1":
  235. provider_path: "secret/path"
  236. input: "secret/path/foo"
  237. output: "secret/path/foo" #noop
  238. # provider path not defined:
  239. kv version == "v2":
  240. provider_path: nil
  241. input: "secret/path/foo"
  242. output: "secret/data/path/foo" # data is prepended to secret/
  243. kv version == "v2":
  244. provider_path: nil
  245. input: "secret/path/data/foo"
  246. output: "secret/path/data/foo" #noop
  247. kv version == "v1":
  248. provider_path: nil
  249. input: "secret/path/foo"
  250. output: "secret/path/foo" #noop
  251. */
  252. func (c *client) buildPath(path string) string {
  253. optionalMount := c.store.Path
  254. out := path
  255. // if optionalMount is Set, remove it from path if its there
  256. if optionalMount != nil {
  257. cut := *optionalMount + "/"
  258. if strings.HasPrefix(out, cut) {
  259. // This current logic induces a bug when the actual secret resides on same path names as the mount path.
  260. _, out, _ = strings.Cut(out, cut)
  261. // if data succeeds optionalMount on v2 store, we should remove it as well
  262. if strings.HasPrefix(out, "data/") && c.store.Version == esv1.VaultKVStoreV2 {
  263. _, out, _ = strings.Cut(out, "data/")
  264. }
  265. }
  266. buildPath := strings.Split(out, "/")
  267. buildMount := strings.Split(*optionalMount, "/")
  268. if c.store.Version == esv1.VaultKVStoreV2 {
  269. buildMount = append(buildMount, "data")
  270. }
  271. buildMount = append(buildMount, buildPath...)
  272. out = strings.Join(buildMount, "/")
  273. return out
  274. }
  275. if !strings.Contains(out, "/data/") && c.store.Version == esv1.VaultKVStoreV2 {
  276. buildPath := strings.Split(out, "/")
  277. buildMount := []string{buildPath[0], "data"}
  278. buildMount = append(buildMount, buildPath[1:]...)
  279. out = strings.Join(buildMount, "/")
  280. return out
  281. }
  282. return out
  283. }