client_get.go 9.2 KB

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