vault_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  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 vaultdynamic
  14. import (
  15. "context"
  16. "errors"
  17. "testing"
  18. "github.com/google/go-cmp/cmp"
  19. vaultapi "github.com/hashicorp/vault/api"
  20. corev1 "k8s.io/api/core/v1"
  21. apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
  24. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  25. clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  26. provider "github.com/external-secrets/external-secrets/providers/v1/vault"
  27. "github.com/external-secrets/external-secrets/providers/v1/vault/fake"
  28. vaultutil "github.com/external-secrets/external-secrets/providers/v1/vault/util"
  29. utilfake "github.com/external-secrets/external-secrets/runtime/util/fake"
  30. )
  31. type args struct {
  32. jsonSpec *apiextensions.JSON
  33. kube kclient.Client
  34. corev1 typedcorev1.CoreV1Interface
  35. vaultClientFn func(config *vaultapi.Config) (vaultutil.Client, error)
  36. }
  37. type want struct {
  38. val map[string][]byte
  39. partialVal map[string][]byte
  40. err error
  41. }
  42. type testCase struct {
  43. reason string
  44. args args
  45. want want
  46. }
  47. func TestVaultDynamicSecretGenerator(t *testing.T) {
  48. cases := map[string]testCase{
  49. "NilSpec": {
  50. reason: "Raise an error with empty spec.",
  51. args: args{
  52. jsonSpec: nil,
  53. },
  54. want: want{
  55. err: errors.New("no config spec provided"),
  56. },
  57. },
  58. "InvalidSpec": {
  59. reason: "Raise an error with invalid spec.",
  60. args: args{
  61. jsonSpec: &apiextensions.JSON{
  62. Raw: []byte(``),
  63. },
  64. },
  65. want: want{
  66. err: errors.New("no Vault provider config in spec"),
  67. },
  68. },
  69. "MissingRoleName": {
  70. reason: "Raise error if incomplete k8s auth config is provided.",
  71. args: args{
  72. corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
  73. jsonSpec: &apiextensions.JSON{
  74. Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
  75. kind: VaultDynamicSecret
  76. spec:
  77. provider:
  78. auth:
  79. kubernetes:
  80. serviceAccountRef:
  81. name: "testing"
  82. method: GET
  83. path: "github/token/example"`),
  84. },
  85. kube: clientfake.NewClientBuilder().Build(),
  86. },
  87. want: want{
  88. err: errors.New("unable to setup Vault client: no role name was provided"),
  89. },
  90. },
  91. "EmptyVaultResponse": {
  92. reason: "Fail on empty response from Vault.",
  93. args: args{
  94. corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
  95. jsonSpec: &apiextensions.JSON{
  96. Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
  97. kind: VaultDynamicSecret
  98. spec:
  99. provider:
  100. auth:
  101. kubernetes:
  102. role: test
  103. serviceAccountRef:
  104. name: "testing"
  105. method: GET
  106. path: "github/token/example"`),
  107. },
  108. kube: clientfake.NewClientBuilder().WithObjects(&corev1.ServiceAccount{
  109. ObjectMeta: metav1.ObjectMeta{
  110. Name: "testing",
  111. Namespace: "testing",
  112. },
  113. Secrets: []corev1.ObjectReference{
  114. {
  115. Name: "test",
  116. },
  117. },
  118. }).Build(),
  119. },
  120. want: want{
  121. err: errors.New("unable to get dynamic secret: empty response from Vault"),
  122. },
  123. },
  124. "EmptyVaultPOST": {
  125. reason: "Fail on empty response from Vault.",
  126. args: args{
  127. corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
  128. jsonSpec: &apiextensions.JSON{
  129. Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
  130. kind: VaultDynamicSecret
  131. spec:
  132. provider:
  133. auth:
  134. kubernetes:
  135. role: test
  136. serviceAccountRef:
  137. name: "testing"
  138. method: POST
  139. parameters:
  140. foo: "bar"
  141. path: "github/token/example"`),
  142. },
  143. kube: clientfake.NewClientBuilder().WithObjects(&corev1.ServiceAccount{
  144. ObjectMeta: metav1.ObjectMeta{
  145. Name: "testing",
  146. Namespace: "testing",
  147. },
  148. Secrets: []corev1.ObjectReference{
  149. {
  150. Name: "test",
  151. },
  152. },
  153. }).Build(),
  154. },
  155. want: want{
  156. err: errors.New("unable to get dynamic secret: empty response from Vault"),
  157. },
  158. },
  159. "AllowEmptyVaultPOST": {
  160. reason: "Allow empty response from Vault POST.",
  161. args: args{
  162. corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
  163. jsonSpec: &apiextensions.JSON{
  164. Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
  165. kind: VaultDynamicSecret
  166. spec:
  167. provider:
  168. auth:
  169. kubernetes:
  170. role: test
  171. serviceAccountRef:
  172. name: "testing"
  173. method: POST
  174. parameters:
  175. foo: "bar"
  176. path: "github/token/example"
  177. allowEmptyResponse: true`),
  178. },
  179. kube: clientfake.NewClientBuilder().WithObjects(&corev1.ServiceAccount{
  180. ObjectMeta: metav1.ObjectMeta{
  181. Name: "testing",
  182. Namespace: "testing",
  183. },
  184. Secrets: []corev1.ObjectReference{
  185. {
  186. Name: "test",
  187. },
  188. },
  189. }).Build(),
  190. },
  191. want: want{
  192. err: nil,
  193. val: nil,
  194. },
  195. },
  196. "AllowEmptyVaultGET": {
  197. reason: "Allow empty response from Vault GET.",
  198. args: args{
  199. corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
  200. jsonSpec: &apiextensions.JSON{
  201. Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
  202. kind: VaultDynamicSecret
  203. spec:
  204. provider:
  205. auth:
  206. kubernetes:
  207. role: test
  208. serviceAccountRef:
  209. name: "testing"
  210. method: GET
  211. parameters:
  212. foo: "bar"
  213. path: "github/token/example"
  214. allowEmptyResponse: true`),
  215. },
  216. kube: clientfake.NewClientBuilder().WithObjects(&corev1.ServiceAccount{
  217. ObjectMeta: metav1.ObjectMeta{
  218. Name: "testing",
  219. Namespace: "testing",
  220. },
  221. Secrets: []corev1.ObjectReference{
  222. {
  223. Name: "test",
  224. },
  225. },
  226. }).Build(),
  227. },
  228. want: want{
  229. err: nil,
  230. val: nil,
  231. },
  232. },
  233. "DataResultType": {
  234. reason: "Allow accessing the data section of the response from Vault API.",
  235. args: args{
  236. corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
  237. jsonSpec: &apiextensions.JSON{
  238. Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
  239. kind: VaultDynamicSecret
  240. spec:
  241. provider:
  242. auth:
  243. kubernetes:
  244. role: test
  245. serviceAccountRef:
  246. name: "testing"
  247. path: "github/token/example"`),
  248. },
  249. kube: clientfake.NewClientBuilder().WithObjects(&corev1.ServiceAccount{
  250. ObjectMeta: metav1.ObjectMeta{
  251. Name: "testing",
  252. Namespace: "testing",
  253. },
  254. Secrets: []corev1.ObjectReference{
  255. {
  256. Name: "test",
  257. },
  258. },
  259. }).Build(),
  260. vaultClientFn: fake.ModifiableClientWithLoginMock(
  261. func(cl *fake.VaultClient) {
  262. cl.MockLogical.ReadWithDataWithContextFn = func(ctx context.Context, path string, data map[string][]string) (*vaultapi.Secret, error) {
  263. return &vaultapi.Secret{
  264. Data: map[string]interface{}{
  265. "key": "value",
  266. },
  267. }, nil
  268. }
  269. },
  270. ),
  271. },
  272. want: want{
  273. err: nil,
  274. val: map[string][]byte{"key": []byte("value")},
  275. },
  276. },
  277. "AuthResultType": {
  278. reason: "Allow accessing auth section of the response from Vault API.",
  279. args: args{
  280. corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
  281. jsonSpec: &apiextensions.JSON{
  282. Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
  283. kind: VaultDynamicSecret
  284. spec:
  285. provider:
  286. auth:
  287. kubernetes:
  288. role: test
  289. serviceAccountRef:
  290. name: "testing"
  291. path: "github/token/example"
  292. resultType: "Auth"`),
  293. },
  294. kube: clientfake.NewClientBuilder().WithObjects(&corev1.ServiceAccount{
  295. ObjectMeta: metav1.ObjectMeta{
  296. Name: "testing",
  297. Namespace: "testing",
  298. },
  299. Secrets: []corev1.ObjectReference{
  300. {
  301. Name: "test",
  302. },
  303. },
  304. }).Build(),
  305. vaultClientFn: fake.ModifiableClientWithLoginMock(
  306. func(cl *fake.VaultClient) {
  307. cl.MockLogical.ReadWithDataWithContextFn = func(ctx context.Context, path string, data map[string][]string) (*vaultapi.Secret, error) {
  308. return &vaultapi.Secret{
  309. Auth: &vaultapi.SecretAuth{
  310. EntityID: "123",
  311. },
  312. }, nil
  313. }
  314. },
  315. ),
  316. },
  317. want: want{
  318. err: nil,
  319. partialVal: map[string][]byte{"entity_id": []byte("123")},
  320. },
  321. },
  322. "RawResultType": {
  323. reason: "Allow accessing auth section of the response from Vault API.",
  324. args: args{
  325. corev1: utilfake.NewCreateTokenMock().WithToken("ok"),
  326. jsonSpec: &apiextensions.JSON{
  327. Raw: []byte(`apiVersion: generators.external-secrets.io/v1alpha1
  328. kind: VaultDynamicSecret
  329. spec:
  330. provider:
  331. auth:
  332. kubernetes:
  333. role: test
  334. serviceAccountRef:
  335. name: "testing"
  336. path: "github/token/example"
  337. resultType: "Raw"`),
  338. },
  339. kube: clientfake.NewClientBuilder().WithObjects(&corev1.ServiceAccount{
  340. ObjectMeta: metav1.ObjectMeta{
  341. Name: "testing",
  342. Namespace: "testing",
  343. },
  344. Secrets: []corev1.ObjectReference{
  345. {
  346. Name: "test",
  347. },
  348. },
  349. }).Build(),
  350. vaultClientFn: fake.ModifiableClientWithLoginMock(
  351. func(cl *fake.VaultClient) {
  352. cl.MockLogical.ReadWithDataWithContextFn = func(ctx context.Context, path string, data map[string][]string) (*vaultapi.Secret, error) {
  353. return &vaultapi.Secret{
  354. LeaseID: "123",
  355. Data: map[string]interface{}{
  356. "key": "value",
  357. },
  358. }, nil
  359. }
  360. },
  361. ),
  362. },
  363. want: want{
  364. err: nil,
  365. partialVal: map[string][]byte{
  366. "lease_id": []byte("123"),
  367. "data": []byte(`{"key":"value"}`),
  368. },
  369. },
  370. },
  371. }
  372. for name, tc := range cases {
  373. t.Run(name, func(t *testing.T) {
  374. newClientFn := fake.ClientWithLoginMock
  375. if tc.args.vaultClientFn != nil {
  376. newClientFn = tc.args.vaultClientFn
  377. }
  378. c := &provider.Provider{NewVaultClient: newClientFn}
  379. gen := &Generator{}
  380. val, _, err := gen.generate(context.Background(), c, tc.args.jsonSpec, tc.args.kube, tc.args.corev1, "testing")
  381. if tc.want.err != nil {
  382. if err != nil {
  383. if diff := cmp.Diff(tc.want.err.Error(), err.Error()); diff != "" {
  384. t.Errorf("\n%s\nvault.GetSecret(...): -want error, +got error:\n%s", tc.reason, diff)
  385. }
  386. } else {
  387. t.Errorf("\n%s\nvault.GetSecret(...): -want error, +got val:\n%s", tc.reason, val)
  388. }
  389. } else if tc.want.partialVal != nil {
  390. for k, v := range tc.want.partialVal {
  391. if diff := cmp.Diff(v, val[k]); diff != "" {
  392. t.Errorf("\n%s\nvault.GetSecret(...) -> %s: -want partial, +got partial:\n%s", k, tc.reason, diff)
  393. }
  394. }
  395. } else {
  396. if diff := cmp.Diff(tc.want.val, val); diff != "" {
  397. t.Errorf("\n%s\nvault.GetSecret(...): -want val, +got val:\n%s", tc.reason, diff)
  398. }
  399. }
  400. })
  401. }
  402. }