vault_test.go 11 KB

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