client_get_all_secrets_test.go 12 KB

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