client_get_all_secrets_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  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. "errors"
  17. "strings"
  18. "testing"
  19. "github.com/google/go-cmp/cmp"
  20. vault "github.com/hashicorp/vault/api"
  21. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  22. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  23. "github.com/external-secrets/external-secrets/providers/v1/vault/fake"
  24. vaultutil "github.com/external-secrets/external-secrets/providers/v1/vault/util"
  25. )
  26. func TestGetAllSecrets(t *testing.T) {
  27. secret1Bytes := []byte("{\"access_key\":\"access_key\",\"access_secret\":\"access_secret\"}")
  28. secret2Bytes := []byte("{\"access_key\":\"access_key2\",\"access_secret\":\"access_secret2\"}")
  29. path1Bytes := []byte("{\"access_key\":\"path1\",\"access_secret\":\"path1\"}")
  30. path2Bytes := []byte("{\"access_key\":\"path2\",\"access_secret\":\"path2\"}")
  31. tagBytes := []byte("{\"access_key\":\"unfetched\",\"access_secret\":\"unfetched\"}")
  32. path := "path"
  33. kv2secret := map[string]any{
  34. "secret1": map[string]any{
  35. "metadata": map[string]any{
  36. "custom_metadata": map[string]any{
  37. "foo": "bar",
  38. },
  39. },
  40. "data": map[string]any{
  41. "access_key": "access_key",
  42. "access_secret": "access_secret",
  43. },
  44. },
  45. "secret2": map[string]any{
  46. "metadata": map[string]any{
  47. "custom_metadata": map[string]any{
  48. "foo": "baz",
  49. },
  50. },
  51. "data": map[string]any{
  52. "access_key": "access_key2",
  53. "access_secret": "access_secret2",
  54. },
  55. },
  56. "secret3": map[string]any{
  57. "metadata": map[string]any{
  58. "custom_metadata": map[string]any{
  59. "foo": "baz",
  60. },
  61. },
  62. "data": nil,
  63. },
  64. "tag": map[string]any{
  65. "metadata": map[string]any{
  66. "custom_metadata": map[string]any{
  67. "foo": "baz",
  68. },
  69. },
  70. "data": map[string]any{
  71. "access_key": "unfetched",
  72. "access_secret": "unfetched",
  73. },
  74. },
  75. "path/1": map[string]any{
  76. "metadata": map[string]any{
  77. "custom_metadata": map[string]any{
  78. "foo": "path",
  79. },
  80. },
  81. "data": map[string]any{
  82. "access_key": "path1",
  83. "access_secret": "path1",
  84. },
  85. },
  86. "path/2": map[string]any{
  87. "metadata": map[string]any{
  88. "custom_metadata": map[string]any{
  89. "foo": "path",
  90. },
  91. },
  92. "data": map[string]any{
  93. "access_key": "path2",
  94. "access_secret": "path2",
  95. },
  96. },
  97. "default": map[string]any{
  98. "data": map[string]any{
  99. "empty": "true",
  100. },
  101. "metadata": map[string]any{
  102. "keys": []any{"secret1", "secret2", "secret3", "tag", "path/"},
  103. },
  104. },
  105. "path/": map[string]any{
  106. "data": map[string]any{
  107. "empty": "true",
  108. },
  109. "metadata": map[string]any{
  110. "keys": []any{"1", "2"},
  111. },
  112. },
  113. }
  114. kv1secret := map[string]any{
  115. "secret1": map[string]any{
  116. "access_key": "access_key",
  117. "access_secret": "access_secret",
  118. },
  119. "secret2": map[string]any{
  120. "access_key": "access_key2",
  121. "access_secret": "access_secret2",
  122. },
  123. "tag": map[string]any{
  124. "access_key": "unfetched",
  125. "access_secret": "unfetched",
  126. },
  127. "path/1": map[string]any{
  128. "access_key": "path1",
  129. "access_secret": "path1",
  130. },
  131. "path/2": map[string]any{
  132. "access_key": "path2",
  133. "access_secret": "path2",
  134. },
  135. }
  136. type args struct {
  137. store *esv1.VaultProvider
  138. kube kclient.Client
  139. vLogical vaultutil.Logical
  140. ns string
  141. data esv1.ExternalSecretFind
  142. }
  143. type want struct {
  144. err error
  145. val map[string][]byte
  146. }
  147. cases := map[string]struct {
  148. reason string
  149. args args
  150. want want
  151. }{
  152. "FindByNameKv2": {
  153. reason: "should map multiple secrets matching name for kv2",
  154. args: args{
  155. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV2).Spec.Provider.Vault,
  156. vLogical: &fake.Logical{
  157. ListWithContextFn: newListWithContextFn(kv2secret),
  158. ReadWithDataWithContextFn: newReadtWithContextFn(kv2secret),
  159. },
  160. data: esv1.ExternalSecretFind{
  161. Name: &esv1.FindName{
  162. RegExp: "secret.*",
  163. },
  164. },
  165. },
  166. want: want{
  167. err: nil,
  168. val: map[string][]byte{
  169. "secret1": secret1Bytes,
  170. "secret2": secret2Bytes,
  171. },
  172. },
  173. },
  174. "FindByNameKv1": {
  175. reason: "should map multiple secrets matching name for kv1",
  176. args: args{
  177. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV1).Spec.Provider.Vault,
  178. vLogical: &fake.Logical{
  179. ListWithContextFn: newListWithContextKvv1Fn(kv1secret),
  180. ReadWithDataWithContextFn: newReadtWithContextKvv1Fn(kv1secret),
  181. },
  182. data: esv1.ExternalSecretFind{
  183. Name: &esv1.FindName{
  184. RegExp: "secret.*",
  185. },
  186. },
  187. },
  188. want: want{
  189. err: nil,
  190. val: map[string][]byte{
  191. "secret1": secret1Bytes,
  192. "secret2": secret2Bytes,
  193. },
  194. },
  195. },
  196. "FindByTagKv2": {
  197. reason: "should map multiple secrets matching tags",
  198. args: args{
  199. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV2).Spec.Provider.Vault,
  200. vLogical: &fake.Logical{
  201. ListWithContextFn: newListWithContextFn(kv2secret),
  202. ReadWithDataWithContextFn: newReadtWithContextFn(kv2secret),
  203. },
  204. data: esv1.ExternalSecretFind{
  205. Tags: map[string]string{
  206. "foo": "baz",
  207. },
  208. },
  209. },
  210. want: want{
  211. err: nil,
  212. val: map[string][]byte{
  213. "tag": tagBytes,
  214. "secret2": secret2Bytes,
  215. },
  216. },
  217. },
  218. "FindByTagKv1": {
  219. reason: "find by tag should not work if using kv1 store",
  220. args: args{
  221. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV1).Spec.Provider.Vault,
  222. vLogical: &fake.Logical{
  223. ListWithContextFn: newListWithContextKvv1Fn(kv1secret),
  224. ReadWithDataWithContextFn: newReadtWithContextKvv1Fn(kv1secret),
  225. },
  226. data: esv1.ExternalSecretFind{
  227. Tags: map[string]string{
  228. "foo": "baz",
  229. },
  230. },
  231. },
  232. want: want{
  233. err: errors.New(errUnsupportedKvVersion),
  234. },
  235. },
  236. "FilterByPathKv2WithTags": {
  237. reason: "should filter secrets based on path for kv2 with tags",
  238. args: args{
  239. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV2).Spec.Provider.Vault,
  240. vLogical: &fake.Logical{
  241. ListWithContextFn: newListWithContextFn(kv2secret),
  242. ReadWithDataWithContextFn: newReadtWithContextFn(kv2secret),
  243. },
  244. data: esv1.ExternalSecretFind{
  245. Path: &path,
  246. Tags: map[string]string{
  247. "foo": "path",
  248. },
  249. },
  250. },
  251. want: want{
  252. err: nil,
  253. val: map[string][]byte{
  254. "path/1": path1Bytes,
  255. "path/2": path2Bytes,
  256. },
  257. },
  258. },
  259. "FilterByPathKv2WithoutTags": {
  260. reason: "should filter secrets based on path for kv2 without tags",
  261. args: args{
  262. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV2).Spec.Provider.Vault,
  263. vLogical: &fake.Logical{
  264. ListWithContextFn: newListWithContextFn(kv2secret),
  265. ReadWithDataWithContextFn: newReadtWithContextFn(kv2secret),
  266. },
  267. data: esv1.ExternalSecretFind{
  268. Path: &path,
  269. },
  270. },
  271. want: want{
  272. err: nil,
  273. val: map[string][]byte{
  274. "path/1": path1Bytes,
  275. "path/2": path2Bytes,
  276. },
  277. },
  278. },
  279. "FilterByPathReturnsNotFound": {
  280. reason: "should return a not found error if there are no more secrets on the path",
  281. args: args{
  282. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV2).Spec.Provider.Vault,
  283. vLogical: &fake.Logical{
  284. ListWithContextFn: func(_ context.Context, _ string) (*vault.Secret, error) {
  285. return nil, nil
  286. },
  287. ReadWithDataWithContextFn: newReadtWithContextFn(map[string]any{}),
  288. },
  289. data: esv1.ExternalSecretFind{
  290. Path: &path,
  291. },
  292. },
  293. want: want{
  294. err: esv1.NoSecretError{},
  295. },
  296. },
  297. "FilterByPathKv1": {
  298. reason: "should filter secrets based on path for kv1",
  299. args: args{
  300. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV1).Spec.Provider.Vault,
  301. vLogical: &fake.Logical{
  302. ListWithContextFn: newListWithContextKvv1Fn(kv1secret),
  303. ReadWithDataWithContextFn: newReadtWithContextKvv1Fn(kv1secret),
  304. },
  305. data: esv1.ExternalSecretFind{
  306. Path: &path,
  307. },
  308. },
  309. want: want{
  310. err: nil,
  311. val: map[string][]byte{
  312. "path/1": path1Bytes,
  313. "path/2": path2Bytes,
  314. },
  315. },
  316. },
  317. "MetadataNotFound": {
  318. reason: "metadata secret not found",
  319. args: args{
  320. store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV2).Spec.Provider.Vault,
  321. vLogical: &fake.Logical{
  322. ListWithContextFn: newListWithContextFn(kv2secret),
  323. ReadWithDataWithContextFn: func(_ context.Context, _ string, _ map[string][]string) (*vault.Secret, error) {
  324. return nil, nil
  325. },
  326. },
  327. data: esv1.ExternalSecretFind{
  328. Tags: map[string]string{
  329. "foo": "baz",
  330. },
  331. },
  332. },
  333. want: want{
  334. err: errors.New(errNotFound),
  335. },
  336. },
  337. }
  338. for name, tc := range cases {
  339. t.Run(name, func(t *testing.T) {
  340. vStore := &client{
  341. kube: tc.args.kube,
  342. logical: tc.args.vLogical,
  343. store: tc.args.store,
  344. namespace: tc.args.ns,
  345. }
  346. val, err := vStore.GetAllSecrets(context.Background(), tc.args.data)
  347. if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
  348. t.Errorf("\n%s\nvault.GetAllSecrets(...): -want error, +got error:\n%s", tc.reason, diff)
  349. }
  350. if diff := cmp.Diff(tc.want.val, val); diff != "" {
  351. t.Errorf("\n%s\nvault.GetAllSecrets(...): -want val, +got val:\n%s", tc.reason, diff)
  352. }
  353. })
  354. }
  355. }
  356. func newListWithContextFn(secrets map[string]any) func(ctx context.Context, path string) (*vault.Secret, error) {
  357. return func(_ context.Context, path string) (*vault.Secret, error) {
  358. path = strings.TrimPrefix(path, "secret/metadata/") // kvv2
  359. if path == "" {
  360. path = "default"
  361. }
  362. data, ok := secrets[path]
  363. if !ok {
  364. return nil, errors.New("Secret not found")
  365. }
  366. meta := data.(map[string]any)
  367. ans := meta["metadata"].(map[string]any)
  368. secret := &vault.Secret{
  369. Data: map[string]any{
  370. "keys": ans["keys"],
  371. },
  372. }
  373. return secret, nil
  374. }
  375. }
  376. func newListWithContextKvv1Fn(secrets map[string]any) func(ctx context.Context, path string) (*vault.Secret, error) {
  377. return func(_ context.Context, path string) (*vault.Secret, error) {
  378. path = strings.TrimPrefix(path, "secret/")
  379. keys := make([]any, 0, len(secrets))
  380. for k := range secrets {
  381. if after, ok := strings.CutPrefix(k, path); ok {
  382. uniqueSuffix := after
  383. keys = append(keys, uniqueSuffix)
  384. }
  385. }
  386. if len(keys) == 0 {
  387. return nil, errors.New("secret not found")
  388. }
  389. secret := &vault.Secret{
  390. Data: map[string]any{
  391. "keys": keys,
  392. },
  393. }
  394. return secret, nil
  395. }
  396. }
  397. func newReadtWithContextFn(secrets map[string]any) func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
  398. return func(_ context.Context, path string, _ map[string][]string) (*vault.Secret, error) {
  399. path = strings.TrimPrefix(path, "secret/data/")
  400. path = strings.TrimPrefix(path, "secret/metadata/")
  401. data, ok := secrets[path]
  402. if !ok {
  403. return nil, errors.New("Secret not found")
  404. }
  405. meta := data.(map[string]any)
  406. metadata := meta["metadata"].(map[string]any)
  407. content := map[string]any{
  408. "data": meta["data"],
  409. "custom_metadata": metadata["custom_metadata"],
  410. }
  411. secret := &vault.Secret{
  412. Data: content,
  413. }
  414. return secret, nil
  415. }
  416. }
  417. func newReadtWithContextKvv1Fn(secrets map[string]any) func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
  418. return func(_ context.Context, path string, _ map[string][]string) (*vault.Secret, error) {
  419. path = strings.TrimPrefix(path, "secret/")
  420. data, ok := secrets[path]
  421. if !ok {
  422. return nil, errors.New("secret not found")
  423. }
  424. dataAsMap := data.(map[string]any)
  425. secret := &vault.Secret{
  426. Data: dataAsMap,
  427. }
  428. return secret, nil
  429. }
  430. }