secretsmanager_workload_identity_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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 secretmanager
  13. import (
  14. "context"
  15. "encoding/json"
  16. "io"
  17. "net/http"
  18. "net/http/httptest"
  19. "testing"
  20. "github.com/googleapis/gax-go/v2"
  21. "github.com/stretchr/testify/assert"
  22. "golang.org/x/oauth2"
  23. credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
  24. authv1 "k8s.io/api/authentication/v1"
  25. v1 "k8s.io/api/core/v1"
  26. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  27. k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
  28. "sigs.k8s.io/controller-runtime/pkg/client"
  29. clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  30. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  31. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  32. )
  33. type workloadIdentityTest struct {
  34. name string
  35. expTS bool
  36. expToken *oauth2.Token
  37. expErr string
  38. genAccessToken func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
  39. genIDBindToken func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
  40. genSAToken func(c context.Context, s1, s2, s3 string) (*authv1.TokenRequest, error)
  41. store esv1beta1.GenericStore
  42. kubeObjects []client.Object
  43. }
  44. func TestWorkloadIdentity(t *testing.T) {
  45. clusterSANamespace := "foobar"
  46. tbl := []*workloadIdentityTest{
  47. composeTestcase(
  48. defaultTestCase("missing store spec should result in error"),
  49. withErr("invalid: missing store spec"),
  50. withStore(&esv1beta1.SecretStore{}),
  51. ),
  52. composeTestcase(
  53. defaultTestCase("should skip when no workload identity is configured: TokenSource and error must be nil"),
  54. withStore(&esv1beta1.SecretStore{
  55. Spec: esv1beta1.SecretStoreSpec{
  56. Provider: &esv1beta1.SecretStoreProvider{
  57. GCPSM: &esv1beta1.GCPSMProvider{},
  58. },
  59. },
  60. }),
  61. ),
  62. composeTestcase(
  63. defaultTestCase("return access token from GenerateAccessTokenRequest with SecretStore"),
  64. withStore(defaultStore()),
  65. expTokenSource(),
  66. expectToken(defaultGenAccessToken),
  67. ),
  68. composeTestcase(
  69. defaultTestCase("return idBindToken when no annotation is set with SecretStore"),
  70. expTokenSource(),
  71. expectToken(defaultIDBindToken),
  72. withStore(defaultStore()),
  73. withK8sResources([]client.Object{
  74. &v1.ServiceAccount{
  75. ObjectMeta: metav1.ObjectMeta{
  76. Name: "example",
  77. Namespace: "default",
  78. Annotations: map[string]string{},
  79. },
  80. },
  81. }),
  82. ),
  83. composeTestcase(
  84. defaultTestCase("invalid ClusterSecretStore: missing service account namespace"),
  85. expErr("invalid ClusterSecretStore: missing GCP Service Account Namespace"),
  86. withStore(
  87. composeStore(defaultClusterStore()),
  88. ),
  89. withK8sResources([]client.Object{
  90. &v1.ServiceAccount{
  91. ObjectMeta: metav1.ObjectMeta{
  92. Name: "example",
  93. Namespace: "default",
  94. Annotations: map[string]string{},
  95. },
  96. },
  97. }),
  98. ),
  99. composeTestcase(
  100. defaultTestCase("return access token from GenerateAccessTokenRequest with ClusterSecretStore"),
  101. expTokenSource(),
  102. expectToken(defaultGenAccessToken),
  103. withStore(
  104. composeStore(defaultClusterStore(), withSANamespace(clusterSANamespace)),
  105. ),
  106. withK8sResources([]client.Object{
  107. &v1.ServiceAccount{
  108. ObjectMeta: metav1.ObjectMeta{
  109. Name: "example",
  110. Namespace: clusterSANamespace,
  111. Annotations: map[string]string{
  112. gcpSAAnnotation: "example",
  113. },
  114. },
  115. },
  116. }),
  117. ),
  118. }
  119. for _, row := range tbl {
  120. t.Run(row.name, func(t *testing.T) {
  121. fakeIam := &fakeIAMClient{generateAccessTokenFunc: row.genAccessToken}
  122. fakeIDBGen := &fakeIDBindTokenGen{generateFunc: row.genIDBindToken}
  123. fakeSATG := &fakeSATokenGen{GenerateFunc: row.genSAToken}
  124. w := &workloadIdentity{
  125. iamClient: fakeIam,
  126. idBindTokenGenerator: fakeIDBGen,
  127. saTokenGenerator: fakeSATG,
  128. }
  129. cb := clientfake.NewClientBuilder()
  130. cb.WithObjects(row.kubeObjects...)
  131. client := cb.Build()
  132. ts, err := w.TokenSource(context.Background(), row.store, client, "default")
  133. // assert err
  134. if row.expErr == "" {
  135. assert.NoError(t, err)
  136. } else {
  137. assert.Error(t, err, row.expErr)
  138. }
  139. // assert ts
  140. if row.expTS {
  141. assert.NotNil(t, ts)
  142. if row.expToken != nil {
  143. tk, err := ts.Token()
  144. assert.NoError(t, err)
  145. assert.EqualValues(t, tk, row.expToken)
  146. }
  147. } else {
  148. assert.Nil(t, ts)
  149. }
  150. })
  151. }
  152. }
  153. func TestClusterProjectID(t *testing.T) {
  154. clusterID, err := clusterProjectID(defaultStore().GetSpec())
  155. assert.Nil(t, err)
  156. assert.Equal(t, clusterID, "1234")
  157. externalClusterID, err := clusterProjectID(defaultExternalStore().GetSpec())
  158. assert.Nil(t, err)
  159. assert.Equal(t, externalClusterID, "5678")
  160. }
  161. func TestSATokenGen(t *testing.T) {
  162. corev1 := &fakeK8sV1{}
  163. g := &k8sSATokenGenerator{
  164. corev1: corev1,
  165. }
  166. token, err := g.Generate(context.Background(), "my-fake-audience", "bar", "default")
  167. assert.Nil(t, err)
  168. assert.Equal(t, token.Status.Token, defaultSAToken)
  169. assert.Equal(t, token.Spec.Audiences[0], "my-fake-audience")
  170. }
  171. func TestIDBTokenGen(t *testing.T) {
  172. srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  173. payload := make(map[string]string)
  174. rb, err := io.ReadAll(r.Body)
  175. assert.Nil(t, err)
  176. err = json.Unmarshal(rb, &payload)
  177. assert.Nil(t, err)
  178. assert.Equal(t, payload["audience"], "identitynamespace:some-idpool:some-id-provider")
  179. bt, err := json.Marshal(&oauth2.Token{
  180. AccessToken: "12345",
  181. })
  182. assert.Nil(t, err)
  183. rw.WriteHeader(http.StatusOK)
  184. rw.Write(bt)
  185. }))
  186. defer srv.Close()
  187. gen := &gcpIDBindTokenGenerator{
  188. targetURL: srv.URL,
  189. }
  190. token, err := gen.Generate(context.Background(), http.DefaultClient, "some-token", "some-idpool", "some-id-provider")
  191. assert.Nil(t, err)
  192. assert.Equal(t, token.AccessToken, "12345")
  193. }
  194. type testCaseMutator func(tc *workloadIdentityTest)
  195. func composeTestcase(tc *workloadIdentityTest, mutators ...testCaseMutator) *workloadIdentityTest {
  196. for _, m := range mutators {
  197. m(tc)
  198. }
  199. return tc
  200. }
  201. func withErr(err string) testCaseMutator {
  202. return func(tc *workloadIdentityTest) {
  203. tc.expErr = err
  204. }
  205. }
  206. func withStore(store esv1beta1.GenericStore) testCaseMutator {
  207. return func(tc *workloadIdentityTest) {
  208. tc.store = store
  209. }
  210. }
  211. func expTokenSource() testCaseMutator {
  212. return func(tc *workloadIdentityTest) {
  213. tc.expTS = true
  214. }
  215. }
  216. func expectToken(token string) testCaseMutator {
  217. return func(tc *workloadIdentityTest) {
  218. tc.expToken = &oauth2.Token{
  219. AccessToken: token,
  220. }
  221. }
  222. }
  223. func expErr(err string) testCaseMutator {
  224. return func(tc *workloadIdentityTest) {
  225. tc.expErr = err
  226. }
  227. }
  228. func withK8sResources(objs []client.Object) testCaseMutator {
  229. return func(tc *workloadIdentityTest) {
  230. tc.kubeObjects = objs
  231. }
  232. }
  233. var (
  234. defaultGenAccessToken = "default-gen-access-token"
  235. defaultIDBindToken = "default-id-bind-token"
  236. defaultSAToken = "default-k8s-sa-token"
  237. )
  238. func defaultTestCase(name string) *workloadIdentityTest {
  239. return &workloadIdentityTest{
  240. name: name,
  241. genAccessToken: func(c context.Context, gatr *credentialspb.GenerateAccessTokenRequest, co ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
  242. return &credentialspb.GenerateAccessTokenResponse{
  243. AccessToken: defaultGenAccessToken,
  244. }, nil
  245. },
  246. genIDBindToken: func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
  247. return &oauth2.Token{
  248. AccessToken: defaultIDBindToken,
  249. }, nil
  250. },
  251. genSAToken: func(c context.Context, s1, s2, s3 string) (*authv1.TokenRequest, error) {
  252. return &authv1.TokenRequest{
  253. Status: authv1.TokenRequestStatus{
  254. Token: defaultSAToken,
  255. },
  256. }, nil
  257. },
  258. kubeObjects: []client.Object{
  259. &v1.ServiceAccount{
  260. ObjectMeta: metav1.ObjectMeta{
  261. Name: "example",
  262. Namespace: "default",
  263. Annotations: map[string]string{
  264. gcpSAAnnotation: "example",
  265. },
  266. },
  267. },
  268. },
  269. }
  270. }
  271. func defaultStore() *esv1beta1.SecretStore {
  272. return &esv1beta1.SecretStore{
  273. ObjectMeta: metav1.ObjectMeta{
  274. Name: "foobar",
  275. Namespace: "default",
  276. },
  277. Spec: defaultStoreSpec(),
  278. }
  279. }
  280. func defaultExternalStore() *esv1beta1.SecretStore {
  281. return &esv1beta1.SecretStore{
  282. ObjectMeta: metav1.ObjectMeta{
  283. Name: "foobar",
  284. Namespace: "default",
  285. },
  286. Spec: defaultExternalStoreSpec(),
  287. }
  288. }
  289. func defaultClusterStore() *esv1beta1.ClusterSecretStore {
  290. return &esv1beta1.ClusterSecretStore{
  291. TypeMeta: metav1.TypeMeta{
  292. Kind: esv1beta1.ClusterSecretStoreKind,
  293. },
  294. ObjectMeta: metav1.ObjectMeta{
  295. Name: "foobar",
  296. },
  297. Spec: defaultStoreSpec(),
  298. }
  299. }
  300. func defaultStoreSpec() esv1beta1.SecretStoreSpec {
  301. return esv1beta1.SecretStoreSpec{
  302. Provider: &esv1beta1.SecretStoreProvider{
  303. GCPSM: &esv1beta1.GCPSMProvider{
  304. Auth: esv1beta1.GCPSMAuth{
  305. WorkloadIdentity: &esv1beta1.GCPWorkloadIdentity{
  306. ServiceAccountRef: esmeta.ServiceAccountSelector{
  307. Name: "example",
  308. },
  309. ClusterLocation: "example",
  310. ClusterName: "foobar",
  311. },
  312. },
  313. ProjectID: "1234",
  314. },
  315. },
  316. }
  317. }
  318. func defaultExternalStoreSpec() esv1beta1.SecretStoreSpec {
  319. return esv1beta1.SecretStoreSpec{
  320. Provider: &esv1beta1.SecretStoreProvider{
  321. GCPSM: &esv1beta1.GCPSMProvider{
  322. Auth: esv1beta1.GCPSMAuth{
  323. WorkloadIdentity: &esv1beta1.GCPWorkloadIdentity{
  324. ServiceAccountRef: esmeta.ServiceAccountSelector{
  325. Name: "example",
  326. },
  327. ClusterLocation: "example",
  328. ClusterName: "foobar",
  329. ClusterProjectID: "5678",
  330. },
  331. },
  332. ProjectID: "1234",
  333. },
  334. },
  335. }
  336. }
  337. type storeMutator func(spc esv1beta1.GenericStore)
  338. func composeStore(store esv1beta1.GenericStore, mutators ...storeMutator) esv1beta1.GenericStore {
  339. for _, m := range mutators {
  340. m(store)
  341. }
  342. return store
  343. }
  344. func withSANamespace(namespace string) storeMutator {
  345. return func(store esv1beta1.GenericStore) {
  346. spc := store.GetSpec()
  347. spc.Provider.GCPSM.Auth.WorkloadIdentity.ServiceAccountRef.Namespace = &namespace
  348. }
  349. }
  350. // fake IDBindToken Generator.
  351. type fakeIDBindTokenGen struct {
  352. generateFunc func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
  353. }
  354. func (g *fakeIDBindTokenGen) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
  355. return g.generateFunc(ctx, client, k8sToken, idPool, idProvider)
  356. }
  357. // fake IAM Client.
  358. type fakeIAMClient struct {
  359. generateAccessTokenFunc func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
  360. }
  361. func (f *fakeIAMClient) GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
  362. return f.generateAccessTokenFunc(ctx, req, opts...)
  363. }
  364. func (f *fakeIAMClient) Close() error {
  365. return nil
  366. }
  367. // fake SA Token Generator.
  368. type fakeSATokenGen struct {
  369. GenerateFunc func(context.Context, string, string, string) (*authv1.TokenRequest, error)
  370. }
  371. func (f *fakeSATokenGen) Generate(ctx context.Context, idPool, namespace, name string) (*authv1.TokenRequest, error) {
  372. return f.GenerateFunc(ctx, idPool, namespace, name)
  373. }
  374. // fake k8s client for creating tokens.
  375. type fakeK8sV1 struct {
  376. k8sv1.CoreV1Interface
  377. }
  378. func (m *fakeK8sV1) ServiceAccounts(namespace string) k8sv1.ServiceAccountInterface {
  379. return &fakeK8sV1SA{v1mock: m}
  380. }
  381. // Mock the K8s service account client.
  382. type fakeK8sV1SA struct {
  383. k8sv1.ServiceAccountInterface
  384. v1mock *fakeK8sV1
  385. }
  386. func (ma *fakeK8sV1SA) CreateToken(
  387. ctx context.Context,
  388. serviceAccountName string,
  389. tokenRequest *authv1.TokenRequest,
  390. opts metav1.CreateOptions,
  391. ) (*authv1.TokenRequest, error) {
  392. tokenRequest.Status.Token = defaultSAToken
  393. return tokenRequest, nil
  394. }