| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package secretmanager
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "testing"
- "cloud.google.com/go/iam/credentials/apiv1/credentialspb"
- "github.com/googleapis/gax-go/v2"
- "github.com/stretchr/testify/assert"
- "golang.org/x/oauth2"
- authv1 "k8s.io/api/authentication/v1"
- v1 "k8s.io/api/core/v1"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
- "sigs.k8s.io/controller-runtime/pkg/client"
- clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
- )
- type workloadIdentityTest struct {
- name string
- expTS bool
- expToken *oauth2.Token
- expErr string
- genAccessToken func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
- genIDBindToken func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
- genSAToken func(c context.Context, s1 []string, s2, s3 string) (*authv1.TokenRequest, error)
- instMetadata map[string]string
- store esv1.GenericStore
- kubeObjects []client.Object
- }
- func TestWorkloadIdentity(t *testing.T) {
- clusterSANamespace := "foobar"
- tbl := []*workloadIdentityTest{
- composeTestcase(
- defaultTestCase("should skip when no workload identity is configured: TokenSource and error must be nil"),
- withStore(&esv1.SecretStore{
- Spec: esv1.SecretStoreSpec{
- Provider: &esv1.SecretStoreProvider{
- GCPSM: &esv1.GCPSMProvider{},
- },
- },
- }),
- ),
- composeTestcase(
- defaultTestCase("return access token from GenerateAccessTokenRequest with SecretStore"),
- withStore(defaultStore()),
- expTokenSource(),
- expectToken(defaultGenAccessToken),
- ),
- composeTestcase(
- defaultTestCase("return idBindToken when no annotation is set with SecretStore"),
- expTokenSource(),
- expectToken(defaultIDBindToken),
- withStore(defaultStore()),
- withK8sResources([]client.Object{
- &v1.ServiceAccount{
- ObjectMeta: metav1.ObjectMeta{
- Name: "example",
- Namespace: "default",
- Annotations: map[string]string{},
- },
- },
- }),
- ),
- composeTestcase(
- defaultTestCase("ClusterSecretStore: referent auth / service account without namespace"),
- expTokenSource(),
- withStore(
- composeStore(defaultClusterStore()),
- ),
- withK8sResources([]client.Object{
- &v1.ServiceAccount{
- ObjectMeta: metav1.ObjectMeta{
- Name: "example",
- Namespace: "default",
- Annotations: map[string]string{},
- },
- },
- }),
- ),
- composeTestcase(
- defaultTestCase("ClusterSecretStore: invalid service account"),
- expErr("foobar"),
- withStore(
- composeStore(defaultClusterStore()),
- ),
- withK8sResources([]client.Object{
- &v1.ServiceAccount{
- ObjectMeta: metav1.ObjectMeta{
- Name: "does not exist",
- Namespace: "default",
- Annotations: map[string]string{},
- },
- },
- }),
- ),
- composeTestcase(
- defaultTestCase("return access token from GenerateAccessTokenRequest with ClusterSecretStore"),
- expTokenSource(),
- expectToken(defaultGenAccessToken),
- withStore(
- composeStore(defaultClusterStore(), withSANamespace(clusterSANamespace)),
- ),
- withK8sResources([]client.Object{
- &v1.ServiceAccount{
- ObjectMeta: metav1.ObjectMeta{
- Name: "example",
- Namespace: clusterSANamespace,
- Annotations: map[string]string{
- gcpSAAnnotation: "example",
- },
- },
- },
- }),
- ),
- composeTestcase(
- defaultTestCase("lookup cluster id from instance metadata"),
- expTokenSource(),
- expectToken(defaultGenAccessToken),
- withStore(
- composeStore(defaultStore(), withClusterID("", "", "")),
- ),
- withInstMetadata(map[string]string{
- "project-id": "1234",
- "cluster-location": "example",
- "cluster-name": "foobar",
- }),
- ),
- }
- for _, row := range tbl {
- t.Run(row.name, func(t *testing.T) {
- fakeIam := &fakeIAMClient{generateAccessTokenFunc: row.genAccessToken}
- fakeMeta := &fakeMetadataClient{metadata: row.instMetadata}
- fakeIDBGen := &fakeIDBindTokenGen{generateFunc: row.genIDBindToken}
- fakeSATG := &fakeSATokenGen{GenerateFunc: row.genSAToken}
- w := &workloadIdentity{
- iamClient: fakeIam,
- metadataClient: fakeMeta,
- idBindTokenGenerator: fakeIDBGen,
- saTokenGenerator: fakeSATG,
- }
- cb := clientfake.NewClientBuilder()
- cb.WithObjects(row.kubeObjects...)
- client := cb.Build()
- isCluster := row.store.GetTypeMeta().Kind == esv1.ClusterSecretStoreKind
- ts, err := w.TokenSource(context.Background(), row.store.GetSpec().Provider.GCPSM.Auth, isCluster, client, "default")
- // assert err
- if row.expErr == "" {
- assert.NoError(t, err)
- } else {
- assert.Error(t, err, row.expErr)
- }
- // assert ts
- if row.expTS {
- assert.NotNil(t, ts)
- if row.expToken != nil {
- tk, err := ts.Token()
- assert.NoError(t, err)
- assert.EqualValues(t, tk, row.expToken)
- }
- } else {
- assert.Nil(t, ts)
- }
- })
- }
- }
- func TestClusterProjectID(t *testing.T) {
- clusterID, err := clusterProjectID(t.Context(), defaultStore().GetSpec())
- assert.Nil(t, err)
- assert.Equal(t, clusterID, "1234")
- externalClusterID, err := clusterProjectID(t.Context(), defaultExternalStore().GetSpec())
- assert.Nil(t, err)
- assert.Equal(t, externalClusterID, "5678")
- }
- func TestSATokenGen(t *testing.T) {
- corev1 := &fakeK8sV1{}
- g := &k8sSATokenGenerator{
- corev1: corev1,
- }
- token, err := g.Generate(context.Background(), []string{"my-fake-audience"}, "bar", "default")
- assert.Nil(t, err)
- assert.Equal(t, token.Status.Token, defaultSAToken)
- assert.Equal(t, token.Spec.Audiences[0], "my-fake-audience")
- }
- func TestIDBTokenGen(t *testing.T) {
- srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- payload := make(map[string]string)
- rb, err := io.ReadAll(r.Body)
- assert.Nil(t, err)
- err = json.Unmarshal(rb, &payload)
- assert.Nil(t, err)
- assert.Equal(t, payload["audience"], "identitynamespace:some-idpool:some-id-provider")
- bt, err := json.Marshal(&oauth2.Token{
- AccessToken: "12345",
- })
- assert.Nil(t, err)
- rw.WriteHeader(http.StatusOK)
- rw.Write(bt)
- }))
- defer srv.Close()
- gen := &gcpIDBindTokenGenerator{
- targetURL: srv.URL,
- }
- token, err := gen.Generate(context.Background(), http.DefaultClient, "some-token", "some-idpool", "some-id-provider")
- assert.Nil(t, err)
- assert.Equal(t, token.AccessToken, "12345")
- }
- type testCaseMutator func(tc *workloadIdentityTest)
- func composeTestcase(tc *workloadIdentityTest, mutators ...testCaseMutator) *workloadIdentityTest {
- for _, m := range mutators {
- m(tc)
- }
- return tc
- }
- func withStore(store esv1.GenericStore) testCaseMutator {
- return func(tc *workloadIdentityTest) {
- tc.store = store
- }
- }
- func expTokenSource() testCaseMutator {
- return func(tc *workloadIdentityTest) {
- tc.expTS = true
- }
- }
- func expectToken(token string) testCaseMutator {
- return func(tc *workloadIdentityTest) {
- tc.expToken = &oauth2.Token{
- AccessToken: token,
- }
- }
- }
- func expErr(err string) testCaseMutator {
- return func(tc *workloadIdentityTest) {
- tc.expErr = err
- }
- }
- func withK8sResources(objs []client.Object) testCaseMutator {
- return func(tc *workloadIdentityTest) {
- tc.kubeObjects = objs
- }
- }
- func withInstMetadata(metadata map[string]string) testCaseMutator {
- return func(tc *workloadIdentityTest) {
- tc.instMetadata = metadata
- }
- }
- var (
- defaultGenAccessToken = "default-gen-access-token"
- defaultIDBindToken = "default-id-bind-token"
- defaultSAToken = "default-k8s-sa-token"
- )
- func defaultTestCase(name string) *workloadIdentityTest {
- return &workloadIdentityTest{
- name: name,
- genAccessToken: func(c context.Context, gatr *credentialspb.GenerateAccessTokenRequest, co ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
- return &credentialspb.GenerateAccessTokenResponse{
- AccessToken: defaultGenAccessToken,
- }, nil
- },
- genIDBindToken: func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
- return &oauth2.Token{
- AccessToken: defaultIDBindToken,
- }, nil
- },
- genSAToken: func(c context.Context, s1 []string, s2, s3 string) (*authv1.TokenRequest, error) {
- return &authv1.TokenRequest{
- Status: authv1.TokenRequestStatus{
- Token: defaultSAToken,
- },
- }, nil
- },
- instMetadata: map[string]string{
- "project-id": "1234",
- },
- kubeObjects: []client.Object{
- &v1.ServiceAccount{
- ObjectMeta: metav1.ObjectMeta{
- Name: "example",
- Namespace: "default",
- Annotations: map[string]string{
- gcpSAAnnotation: "example",
- },
- },
- },
- },
- }
- }
- func defaultStore() *esv1.SecretStore {
- return &esv1.SecretStore{
- ObjectMeta: metav1.ObjectMeta{
- Name: "foobar",
- Namespace: "default",
- },
- Spec: defaultStoreSpec(),
- }
- }
- func defaultExternalStore() *esv1.SecretStore {
- return &esv1.SecretStore{
- ObjectMeta: metav1.ObjectMeta{
- Name: "foobar",
- Namespace: "default",
- },
- Spec: defaultExternalStoreSpec(),
- }
- }
- func defaultClusterStore() *esv1.ClusterSecretStore {
- return &esv1.ClusterSecretStore{
- TypeMeta: metav1.TypeMeta{
- Kind: esv1.ClusterSecretStoreKind,
- },
- ObjectMeta: metav1.ObjectMeta{
- Name: "foobar",
- },
- Spec: defaultStoreSpec(),
- }
- }
- func defaultStoreSpec() esv1.SecretStoreSpec {
- return esv1.SecretStoreSpec{
- Provider: &esv1.SecretStoreProvider{
- GCPSM: &esv1.GCPSMProvider{
- Auth: esv1.GCPSMAuth{
- WorkloadIdentity: &esv1.GCPWorkloadIdentity{
- ServiceAccountRef: esmeta.ServiceAccountSelector{
- Name: "example",
- },
- ClusterLocation: "example",
- ClusterName: "foobar",
- },
- },
- ProjectID: "1234",
- },
- },
- }
- }
- func defaultExternalStoreSpec() esv1.SecretStoreSpec {
- return esv1.SecretStoreSpec{
- Provider: &esv1.SecretStoreProvider{
- GCPSM: &esv1.GCPSMProvider{
- Auth: esv1.GCPSMAuth{
- WorkloadIdentity: &esv1.GCPWorkloadIdentity{
- ServiceAccountRef: esmeta.ServiceAccountSelector{
- Name: "example",
- },
- ClusterLocation: "example",
- ClusterName: "foobar",
- ClusterProjectID: "5678",
- },
- },
- ProjectID: "1234",
- },
- },
- }
- }
- type storeMutator func(spc esv1.GenericStore)
- func composeStore(store esv1.GenericStore, mutators ...storeMutator) esv1.GenericStore {
- for _, m := range mutators {
- m(store)
- }
- return store
- }
- func withClusterID(project, location, name string) storeMutator {
- return func(store esv1.GenericStore) {
- spc := store.GetSpec()
- spc.Provider.GCPSM.Auth.WorkloadIdentity.ClusterProjectID = project
- spc.Provider.GCPSM.Auth.WorkloadIdentity.ClusterLocation = location
- spc.Provider.GCPSM.Auth.WorkloadIdentity.ClusterName = name
- }
- }
- func withSANamespace(namespace string) storeMutator {
- return func(store esv1.GenericStore) {
- spc := store.GetSpec()
- spc.Provider.GCPSM.Auth.WorkloadIdentity.ServiceAccountRef.Namespace = &namespace
- }
- }
- // fake IDBindToken Generator.
- type fakeIDBindTokenGen struct {
- generateFunc func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
- }
- func (g *fakeIDBindTokenGen) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
- return g.generateFunc(ctx, client, k8sToken, idPool, idProvider)
- }
- // fake IAM Client.
- type fakeIAMClient struct {
- generateAccessTokenFunc func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
- }
- func (f *fakeIAMClient) GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
- return f.generateAccessTokenFunc(ctx, req, opts...)
- }
- func (f *fakeIAMClient) Close() error {
- return nil
- }
- // fake Metadata Client.
- type fakeMetadataClient struct {
- metadata map[string]string
- }
- func (f *fakeMetadataClient) InstanceAttributeValueWithContext(ctx context.Context, attr string) (string, error) {
- if val, ok := f.metadata[attr]; ok {
- return val, nil
- }
- return "", fmt.Errorf("attr %s not found", attr)
- }
- func (f *fakeMetadataClient) ProjectIDWithContext(ctx context.Context) (string, error) {
- if val, ok := f.metadata["project-id"]; ok {
- return val, nil
- }
- return "", errors.New("attr project-id not found")
- }
- // fake SA Token Generator.
- type fakeSATokenGen struct {
- GenerateFunc func(context.Context, []string, string, string) (*authv1.TokenRequest, error)
- }
- func (f *fakeSATokenGen) Generate(ctx context.Context, idPool []string, namespace, name string) (*authv1.TokenRequest, error) {
- return f.GenerateFunc(ctx, idPool, namespace, name)
- }
- // fake k8s client for creating tokens.
- type fakeK8sV1 struct {
- k8sv1.CoreV1Interface
- }
- func (m *fakeK8sV1) ServiceAccounts(_ string) k8sv1.ServiceAccountInterface {
- return &fakeK8sV1SA{v1mock: m}
- }
- // Mock the K8s service account client.
- type fakeK8sV1SA struct {
- k8sv1.ServiceAccountInterface
- v1mock *fakeK8sV1
- }
- func (ma *fakeK8sV1SA) CreateToken(
- _ context.Context,
- _ string,
- tokenRequest *authv1.TokenRequest,
- _ metav1.CreateOptions,
- ) (*authv1.TokenRequest, error) {
- tokenRequest.Status.Token = defaultSAToken
- return tokenRequest, nil
- }
|