provider_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  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 conjur
  13. import (
  14. "context"
  15. "errors"
  16. "fmt"
  17. "reflect"
  18. "testing"
  19. "time"
  20. "github.com/cyberark/conjur-api-go/conjurapi"
  21. "github.com/cyberark/conjur-api-go/conjurapi/authn"
  22. "github.com/golang-jwt/jwt/v5"
  23. "github.com/google/go-cmp/cmp"
  24. corev1 "k8s.io/api/core/v1"
  25. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  26. typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
  27. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  28. clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  29. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  30. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  31. "github.com/external-secrets/external-secrets/pkg/provider/conjur/fake"
  32. utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
  33. )
  34. var (
  35. svcURL = "https://example.com"
  36. svcUser = "user"
  37. svcApikey = "apikey"
  38. svcAccount = "account1"
  39. )
  40. func makeValidRef(k string) *esv1beta1.ExternalSecretDataRemoteRef {
  41. return &esv1beta1.ExternalSecretDataRemoteRef{
  42. Key: k,
  43. Version: "default",
  44. }
  45. }
  46. type ValidateStoreTestCase struct {
  47. store *esv1beta1.SecretStore
  48. err error
  49. }
  50. func TestValidateStore(t *testing.T) {
  51. testCases := []ValidateStoreTestCase{
  52. {
  53. store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
  54. err: nil,
  55. },
  56. {
  57. store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
  58. err: fmt.Errorf("conjur URL cannot be empty"),
  59. },
  60. {
  61. store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
  62. err: fmt.Errorf("missing Auth.Apikey.UserRef"),
  63. },
  64. {
  65. store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
  66. err: fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
  67. },
  68. {
  69. store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
  70. err: fmt.Errorf("missing Auth.ApiKey.Account"),
  71. },
  72. {
  73. store: makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", "myconjuraccount"),
  74. err: nil,
  75. },
  76. {
  77. store: makeJWTSecretStore(svcURL, "", "jwt-secret", "jwt-auth-service", "myconjuraccount"),
  78. err: nil,
  79. },
  80. {
  81. store: makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", ""),
  82. err: fmt.Errorf("missing Auth.Jwt.Account"),
  83. },
  84. {
  85. store: makeJWTSecretStore(svcURL, "conjur", "", "", "myconjuraccount"),
  86. err: fmt.Errorf("missing Auth.Jwt.ServiceID"),
  87. },
  88. {
  89. store: makeJWTSecretStore("", "conjur", "", "jwt-auth-service", "myconjuraccount"),
  90. err: fmt.Errorf("conjur URL cannot be empty"),
  91. },
  92. {
  93. store: makeJWTSecretStore(svcURL, "", "", "jwt-auth-service", "myconjuraccount"),
  94. err: fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
  95. },
  96. {
  97. store: makeNoAuthSecretStore(svcURL),
  98. err: fmt.Errorf("missing Auth.* configuration"),
  99. },
  100. }
  101. c := Provider{}
  102. for _, tc := range testCases {
  103. _, err := c.ValidateStore(tc.store)
  104. if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
  105. t.Errorf("test failed! want %v, got %v", tc.err, err)
  106. } else if tc.err == nil && err != nil {
  107. t.Errorf("want nil got err %v", err)
  108. } else if tc.err != nil && err == nil {
  109. t.Errorf("want err %v got nil", tc.err)
  110. }
  111. }
  112. }
  113. func TestGetSecret(t *testing.T) {
  114. type args struct {
  115. store esv1beta1.GenericStore
  116. kube kclient.Client
  117. corev1 typedcorev1.CoreV1Interface
  118. namespace string
  119. secretPath string
  120. }
  121. type want struct {
  122. err error
  123. value string
  124. }
  125. type testCase struct {
  126. reason string
  127. args args
  128. want want
  129. }
  130. cases := map[string]testCase{
  131. "ApiKeyReadSecretSuccess": {
  132. reason: "Should read a secret successfully using an ApiKey auth secret store.",
  133. args: args{
  134. store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
  135. kube: clientfake.NewClientBuilder().
  136. WithObjects(makeFakeAPIKeySecrets()...).Build(),
  137. namespace: "default",
  138. secretPath: "path/to/secret",
  139. },
  140. want: want{
  141. err: nil,
  142. value: "secret",
  143. },
  144. },
  145. "ApiKeyReadSecretFailure": {
  146. reason: "Should fail to read secret using ApiKey auth secret store.",
  147. args: args{
  148. store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
  149. kube: clientfake.NewClientBuilder().
  150. WithObjects(makeFakeAPIKeySecrets()...).Build(),
  151. namespace: "default",
  152. secretPath: "error",
  153. },
  154. want: want{
  155. err: errors.New("error"),
  156. value: "",
  157. },
  158. },
  159. "JwtWithServiceAccountRefReadSecretSuccess": {
  160. reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
  161. args: args{
  162. store: makeJWTSecretStore(svcURL, "my-service-account", "", "jwt-authenticator", "myconjuraccount"),
  163. kube: clientfake.NewClientBuilder().
  164. WithObjects().Build(),
  165. namespace: "default",
  166. secretPath: "path/to/secret",
  167. corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
  168. },
  169. want: want{
  170. err: nil,
  171. value: "secret",
  172. },
  173. },
  174. "JwtWithSecretRefReadSecretSuccess": {
  175. reason: "Should read a secret successfully using an JWT auth secret store that references a k8s secret.",
  176. args: args{
  177. store: makeJWTSecretStore(svcURL, "", "jwt-secret", "jwt-authenticator", "myconjuraccount"),
  178. kube: clientfake.NewClientBuilder().
  179. WithObjects(&corev1.Secret{
  180. ObjectMeta: metav1.ObjectMeta{
  181. Name: "jwt-secret",
  182. Namespace: "default",
  183. },
  184. Data: map[string][]byte{
  185. "token": []byte(createFakeJwtToken(true)),
  186. },
  187. }).Build(),
  188. namespace: "default",
  189. secretPath: "path/to/secret",
  190. },
  191. want: want{
  192. err: nil,
  193. value: "secret",
  194. },
  195. },
  196. "JwtWithCABundleSuccess": {
  197. reason: "Should read a secret successfully using a JWT auth secret store that references a k8s service account.",
  198. args: args{
  199. store: makeJWTSecretStore(svcURL, "my-service-account", "", "jwt-authenticator", "myconjuraccount"),
  200. kube: clientfake.NewClientBuilder().
  201. WithObjects().Build(),
  202. namespace: "default",
  203. secretPath: "path/to/secret",
  204. corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
  205. },
  206. want: want{
  207. err: nil,
  208. value: "secret",
  209. },
  210. },
  211. }
  212. runTest := func(t *testing.T, _ string, tc testCase) {
  213. provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
  214. ref := makeValidRef(tc.args.secretPath)
  215. secret, err := provider.GetSecret(context.Background(), *ref)
  216. if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
  217. t.Errorf("\n%s\nconjur.GetSecret(...): -want error, +got error:\n%s", tc.reason, diff)
  218. }
  219. secretString := string(secret)
  220. if secretString != tc.want.value {
  221. t.Errorf("\n%s\nconjur.GetSecret(...): want value %v got %v", tc.reason, tc.want.value, secretString)
  222. }
  223. }
  224. for name, tc := range cases {
  225. t.Run(name, func(t *testing.T) {
  226. runTest(t, name, tc)
  227. })
  228. }
  229. }
  230. func TestGetCA(t *testing.T) {
  231. type args struct {
  232. store esv1beta1.GenericStore
  233. kube kclient.Client
  234. corev1 typedcorev1.CoreV1Interface
  235. namespace string
  236. }
  237. type want struct {
  238. err error
  239. cert string
  240. }
  241. type testCase struct {
  242. reason string
  243. args args
  244. want want
  245. }
  246. certData := "mycertdata"
  247. certDataEncoded := "bXljZXJ0ZGF0YQo="
  248. cases := map[string]testCase{
  249. "UseCABundleSuccess": {
  250. reason: "Should read a caBundle successfully.",
  251. args: args{
  252. store: makeStoreWithCA("cabundle", certDataEncoded),
  253. kube: clientfake.NewClientBuilder().
  254. WithObjects().Build(),
  255. namespace: "default",
  256. corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
  257. },
  258. want: want{
  259. err: nil,
  260. cert: certDataEncoded,
  261. },
  262. },
  263. "UseCAProviderConfigMapSuccess": {
  264. reason: "Should read a ca from a ConfigMap successfully.",
  265. args: args{
  266. store: makeStoreWithCA("configmap", ""),
  267. kube: clientfake.NewClientBuilder().
  268. WithObjects(makeFakeCASource("configmap", certData)).Build(),
  269. namespace: "default",
  270. corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
  271. },
  272. want: want{
  273. err: nil,
  274. cert: certDataEncoded,
  275. },
  276. },
  277. "UseCAProviderSecretSuccess": {
  278. reason: "Should read a ca from a Secret successfully.",
  279. args: args{
  280. store: makeStoreWithCA("secret", ""),
  281. kube: clientfake.NewClientBuilder().
  282. WithObjects(makeFakeCASource("secret", certData)).Build(),
  283. namespace: "default",
  284. corev1: utilfake.NewCreateTokenMock().WithToken(createFakeJwtToken(true)),
  285. },
  286. want: want{
  287. err: nil,
  288. cert: certDataEncoded,
  289. },
  290. },
  291. }
  292. runTest := func(t *testing.T, _ string, tc testCase) {
  293. provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
  294. _, err := provider.GetSecret(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
  295. Key: "path/to/secret",
  296. })
  297. if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
  298. t.Errorf("\n%s\nconjur.GetCA(...): -want error, +got error:\n%s", tc.reason, diff)
  299. }
  300. }
  301. for name, tc := range cases {
  302. t.Run(name, func(t *testing.T) {
  303. runTest(t, name, tc)
  304. })
  305. }
  306. }
  307. func makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount string) *esv1beta1.SecretStore {
  308. uref := &esmeta.SecretKeySelector{
  309. Name: "user",
  310. Key: "conjur-hostid",
  311. }
  312. if svcUser == "" {
  313. uref = nil
  314. }
  315. aref := &esmeta.SecretKeySelector{
  316. Name: "apikey",
  317. Key: "conjur-apikey",
  318. }
  319. if svcApikey == "" {
  320. aref = nil
  321. }
  322. store := &esv1beta1.SecretStore{
  323. Spec: esv1beta1.SecretStoreSpec{
  324. Provider: &esv1beta1.SecretStoreProvider{
  325. Conjur: &esv1beta1.ConjurProvider{
  326. URL: svcURL,
  327. Auth: esv1beta1.ConjurAuth{
  328. APIKey: &esv1beta1.ConjurAPIKey{
  329. Account: svcAccount,
  330. UserRef: uref,
  331. APIKeyRef: aref,
  332. },
  333. },
  334. },
  335. },
  336. },
  337. }
  338. return store
  339. }
  340. func makeJWTSecretStore(svcURL, serviceAccountName, secretName, jwtServiceID, conjurAccount string) *esv1beta1.SecretStore {
  341. serviceAccountRef := &esmeta.ServiceAccountSelector{
  342. Name: serviceAccountName,
  343. Audiences: []string{"conjur"},
  344. }
  345. if serviceAccountName == "" {
  346. serviceAccountRef = nil
  347. }
  348. secretRef := &esmeta.SecretKeySelector{
  349. Name: secretName,
  350. Key: "token",
  351. }
  352. if secretName == "" {
  353. secretRef = nil
  354. }
  355. store := &esv1beta1.SecretStore{
  356. Spec: esv1beta1.SecretStoreSpec{
  357. Provider: &esv1beta1.SecretStoreProvider{
  358. Conjur: &esv1beta1.ConjurProvider{
  359. URL: svcURL,
  360. Auth: esv1beta1.ConjurAuth{
  361. Jwt: &esv1beta1.ConjurJWT{
  362. Account: conjurAccount,
  363. ServiceID: jwtServiceID,
  364. ServiceAccountRef: serviceAccountRef,
  365. SecretRef: secretRef,
  366. },
  367. },
  368. },
  369. },
  370. },
  371. }
  372. return store
  373. }
  374. func makeStoreWithCA(caSource, caData string) *esv1beta1.SecretStore {
  375. store := makeJWTSecretStore(svcURL, "conjur", "", "jwt-auth-service", "myconjuraccount")
  376. if caSource == "secret" {
  377. store.Spec.Provider.Conjur.CAProvider = &esv1beta1.CAProvider{
  378. Type: esv1beta1.CAProviderTypeSecret,
  379. Name: "conjur-cert",
  380. Key: "ca",
  381. }
  382. } else if caSource == "configmap" {
  383. store.Spec.Provider.Conjur.CAProvider = &esv1beta1.CAProvider{
  384. Type: esv1beta1.CAProviderTypeConfigMap,
  385. Name: "conjur-cert",
  386. Key: "ca",
  387. }
  388. } else {
  389. store.Spec.Provider.Conjur.CABundle = caData
  390. }
  391. return store
  392. }
  393. func makeNoAuthSecretStore(svcURL string) *esv1beta1.SecretStore {
  394. store := &esv1beta1.SecretStore{
  395. Spec: esv1beta1.SecretStoreSpec{
  396. Provider: &esv1beta1.SecretStoreProvider{
  397. Conjur: &esv1beta1.ConjurProvider{
  398. URL: svcURL,
  399. },
  400. },
  401. },
  402. }
  403. return store
  404. }
  405. func makeFakeAPIKeySecrets() []kclient.Object {
  406. return []kclient.Object{
  407. &corev1.Secret{
  408. ObjectMeta: metav1.ObjectMeta{
  409. Name: "user",
  410. Namespace: "default",
  411. },
  412. Data: map[string][]byte{
  413. "conjur-hostid": []byte("myhostid"),
  414. },
  415. },
  416. &corev1.Secret{
  417. ObjectMeta: metav1.ObjectMeta{
  418. Name: "apikey",
  419. Namespace: "default",
  420. },
  421. Data: map[string][]byte{
  422. "conjur-apikey": []byte("apikey"),
  423. },
  424. },
  425. }
  426. }
  427. func makeFakeCASource(kind, caData string) kclient.Object {
  428. if kind == "secret" {
  429. return &corev1.Secret{
  430. ObjectMeta: metav1.ObjectMeta{
  431. Name: "conjur-cert",
  432. Namespace: "default",
  433. },
  434. Data: map[string][]byte{
  435. "ca": []byte(caData),
  436. },
  437. }
  438. }
  439. return &corev1.ConfigMap{
  440. ObjectMeta: metav1.ObjectMeta{
  441. Name: "conjur-cert",
  442. Namespace: "default",
  443. },
  444. Data: map[string]string{
  445. "ca": caData,
  446. },
  447. }
  448. }
  449. func createFakeJwtToken(expires bool) string {
  450. signingKey := []byte("fakekey")
  451. token := jwt.New(jwt.SigningMethodHS256)
  452. claims := token.Claims.(jwt.MapClaims)
  453. if expires {
  454. claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
  455. }
  456. jwtTokenString, err := token.SignedString(signingKey)
  457. if err != nil {
  458. panic(err)
  459. }
  460. return jwtTokenString
  461. }
  462. // ConjurMockAPIClient is a mock implementation of the ApiClient interface.
  463. type ConjurMockAPIClient struct {
  464. }
  465. func (c *ConjurMockAPIClient) NewClientFromKey(_ conjurapi.Config, _ authn.LoginPair) (SecretsClient, error) {
  466. return &fake.ConjurMockClient{}, nil
  467. }
  468. func (c *ConjurMockAPIClient) NewClientFromJWT(_ conjurapi.Config, _, _ string) (SecretsClient, error) {
  469. return &fake.ConjurMockClient{}, nil
  470. }
  471. // EquateErrors returns true if the supplied errors are of the same type and
  472. // produce identical strings. This mirrors the error comparison behavior of
  473. // https://github.com/go-test/deep, which most Crossplane tests targeted before
  474. // we switched to go-cmp.
  475. //
  476. // This differs from cmpopts.EquateErrors, which does not test for error strings
  477. // and instead returns whether one error 'is' (in the errors.Is sense) the
  478. // other.
  479. func EquateErrors() cmp.Option {
  480. return cmp.Comparer(func(a, b error) bool {
  481. if a == nil || b == nil {
  482. return a == nil && b == nil
  483. }
  484. av := reflect.ValueOf(a)
  485. bv := reflect.ValueOf(b)
  486. if av.Type() != bv.Type() {
  487. return false
  488. }
  489. return a.Error() == b.Error()
  490. })
  491. }