client_get.go 9.6 KB

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