provider_test.go 15 KB

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