diff --git a/application/service/services.go b/application/service/services.go index a54e3979..3eb898f7 100644 --- a/application/service/services.go +++ b/application/service/services.go @@ -62,6 +62,8 @@ type ClusterService interface { Clusters(ctx context.Context, options ...rest.HTTPClientOption) ([]cluster.Cluster, error) ClusterByURL(ctx context.Context, url string, options ...rest.HTTPClientOption) (*cluster.Cluster, error) Status(ctx context.Context, options ...rest.HTTPClientOption) (bool, error) + UnlinkIdentityFromCluster(ctx context.Context, identityID uuid.UUID, clusterURL string, options ...rest.HTTPClientOption) error + LinkIdentityToCluster(ctx context.Context, identityID uuid.UUID, clusterURL string, options ...rest.HTTPClientOption) error Stop() } diff --git a/cluster/cluster.go b/cluster/cluster.go index e8fa25c6..1ee56dcd 100644 --- a/cluster/cluster.go +++ b/cluster/cluster.go @@ -1,5 +1,7 @@ package cluster +import "context" + // Cluster represents an OpenShift cluster configuration type Cluster struct { Name string `mapstructure:"name"` @@ -16,3 +18,11 @@ type Cluster struct { AuthClientDefaultScope string `mapstructure:"auth-client-default-scope"` CapacityExhausted bool `mapstructure:"capacity-exhausted"` // Optional in oso-clusters.conf ('false' by default) } + +type ClusterCache interface { + RLock() + RUnlock() + Clusters() map[string]Cluster + Start(ctx context.Context) error + Stop() +} diff --git a/cluster/factory/cluster_cache_factory.go b/cluster/factory/cluster_cache_factory.go index 25fd17de..f9d785dd 100644 --- a/cluster/factory/cluster_cache_factory.go +++ b/cluster/factory/cluster_cache_factory.go @@ -9,6 +9,7 @@ import ( servicecontext "github.com/fabric8-services/fabric8-auth/application/service/context" "github.com/fabric8-services/fabric8-auth/authorization/token/manager" "github.com/fabric8-services/fabric8-auth/cluster" + clusterservice "github.com/fabric8-services/fabric8-auth/cluster/service" "github.com/fabric8-services/fabric8-auth/rest" ) @@ -34,5 +35,5 @@ type clusterCacheFactoryImpl struct { // NewClusterCache creates a new cluster cache func (f *clusterCacheFactoryImpl) NewClusterCache(ctx context.Context, options ...rest.HTTPClientOption) cluster.ClusterCache { - return cluster.NewCache(f.config, options...) + return clusterservice.NewCache(f.config, options...) } diff --git a/cluster/cache.go b/cluster/service/cache.go similarity index 70% rename from cluster/cache.go rename to cluster/service/cache.go index fe409a47..c9514a0c 100644 --- a/cluster/cache.go +++ b/cluster/service/cache.go @@ -1,9 +1,8 @@ -package cluster +package service import ( "context" "net/http" - "net/url" "sync" "time" @@ -11,9 +10,9 @@ import ( "github.com/fabric8-services/fabric8-auth/goasupport" "github.com/fabric8-services/fabric8-auth/log" "github.com/fabric8-services/fabric8-auth/rest" - "github.com/fabric8-services/fabric8-cluster-client/cluster" + clusterclient "github.com/fabric8-services/fabric8-cluster-client/cluster" - goaclient "github.com/goadesign/goa/client" + "github.com/fabric8-services/fabric8-auth/cluster" "github.com/pkg/errors" ) @@ -23,14 +22,6 @@ type clusterConfig interface { GetClusterCacheRefreshInterval() time.Duration } -type ClusterCache interface { - RLock() - RUnlock() - Clusters() map[string]Cluster - Start(ctx context.Context) error - Stop() -} - type cache struct { sync.RWMutex @@ -38,10 +29,10 @@ type cache struct { options []rest.HTTPClientOption refresher *time.Ticker stopCh chan bool - clusters map[string]Cluster + clusters map[string]cluster.Cluster } -func NewCache(config clusterConfig, options ...rest.HTTPClientOption) ClusterCache { +func NewCache(config clusterConfig, options ...rest.HTTPClientOption) cluster.ClusterCache { return &cache{ config: config, refresher: time.NewTicker(config.GetClusterCacheRefreshInterval()), @@ -81,7 +72,7 @@ func (c *cache) Stop() { } } -func (c *cache) Clusters() map[string]Cluster { +func (c *cache) Clusters() map[string]cluster.Cluster { return c.clusters } @@ -99,13 +90,14 @@ func (c *cache) refreshCache(ctx context.Context) error { } // fetchClusters fetches a new list of clusters from Cluster Management Service -func (c *cache) fetchClusters(ctx context.Context) (map[string]Cluster, error) { - cln, err := c.createClientWithServiceAccountSigner(ctx) +func (c *cache) fetchClusters(ctx context.Context) (map[string]cluster.Cluster, error) { + signer := newJWTSASigner(ctx, c.config, c.options...) + cln, err := signer.createSignedClient() if err != nil { return nil, err } - res, err := cln.ShowAuthClientClusters(goasupport.ForwardContextRequestID(ctx), cluster.ShowAuthClientClustersPath()) + res, err := cln.ShowAuthClientClusters(goasupport.ForwardContextRequestID(ctx), clusterclient.ShowAuthClientClustersPath()) if err != nil { return nil, err } @@ -125,10 +117,10 @@ func (c *cache) fetchClusters(ctx context.Context) (map[string]Cluster, error) { return nil, err } - clusterMap := map[string]Cluster{} + clusterMap := map[string]cluster.Cluster{} if clusters.Data != nil { for _, d := range clusters.Data { - cls := Cluster{ + cls := cluster.Cluster{ Name: d.Name, APIURL: d.APIURL, AppDNS: d.AppDNS, @@ -148,38 +140,3 @@ func (c *cache) fetchClusters(ctx context.Context) (map[string]Cluster, error) { } return clusterMap, nil } - -// createClientWithSASigner creates a client with a JWT signer which uses the Auth Service Account token -func (c *cache) createClientWithServiceAccountSigner(ctx context.Context) (*cluster.Client, error) { - cln, err := c.createClient(ctx) - if err != nil { - return nil, err - } - m, err := manager.DefaultManager(c.config) - if err != nil { - return nil, err - } - signer := m.AuthServiceAccountSigner() - cln.SetJWTSigner(signer) - return cln, nil -} - -func (c *cache) createClient(ctx context.Context) (*cluster.Client, error) { - u, err := url.Parse(c.config.GetClusterServiceURL()) - if err != nil { - return nil, err - } - - httpClient := http.DefaultClient - - if c.options != nil { - for _, opt := range c.options { - opt(httpClient) - } - } - cln := cluster.New(goaclient.HTTPClientDoer(httpClient)) - - cln.Host = u.Host - cln.Scheme = u.Scheme - return cln, nil -} diff --git a/cluster/service/cluster.go b/cluster/service/cluster.go index 38b1b460..fcf63d5e 100644 --- a/cluster/service/cluster.go +++ b/cluster/service/cluster.go @@ -13,7 +13,15 @@ import ( "github.com/fabric8-services/fabric8-auth/application/service/base" servicecontext "github.com/fabric8-services/fabric8-auth/application/service/context" "github.com/fabric8-services/fabric8-auth/cluster" + "github.com/fabric8-services/fabric8-auth/goasupport" + "github.com/fabric8-services/fabric8-auth/log" "github.com/fabric8-services/fabric8-auth/rest" + clusterclient "github.com/fabric8-services/fabric8-cluster-client/cluster" + goaclient "github.com/goadesign/goa/client" + "github.com/pkg/errors" + "github.com/satori/go.uuid" + "net/http" + "net/url" ) type clusterServiceConfig interface { @@ -77,6 +85,64 @@ func (s *clusterService) Stop() { } } +// LinkIdentityToCluster links Identity To Cluster using Cluster URL +func (s *clusterService) LinkIdentityToCluster(ctx context.Context, identityID uuid.UUID, clusterURL string, options ...rest.HTTPClientOption) error { + signer := newJWTSASigner(ctx, s.config, options...) + remoteClusterService, err := signer.createSignedClient() + if err != nil { + return errors.Wrapf(err, "failed to create JWT signer for cluster service") + } + identityToClusterData := &clusterclient.LinkIdentityToClusterData{ + ClusterURL: clusterURL, + IdentityID: identityID.String(), + } + res, err := remoteClusterService.LinkIdentityToClusterClusters(goasupport.ForwardContextRequestID(ctx), clusterclient.LinkIdentityToClusterClustersPath(), identityToClusterData) + if err != nil { + return errors.Wrapf(err, "failed to link identity %s to cluster having url %s", identityID, clusterURL) + } + defer rest.CloseResponse(res) + bodyString := rest.ReadBody(res.Body) // To prevent FDs leaks + if res.StatusCode != http.StatusNoContent { + log.Error(ctx, map[string]interface{}{ + "identity_id": identityID, + "cluster_url": clusterURL, + "response_status": res.Status, + "response_body": bodyString, + }, "unable to link identity to cluster in cluster management service") + return errors.Errorf("failed to link identity to cluster in cluster management service. Response status: %s. Response body: %s", res.Status, bodyString) + } + return nil +} + +// UnlinkIdentityFromCluster removes linked Identity from Cluster using Cluster URL +func (s *clusterService) UnlinkIdentityFromCluster(ctx context.Context, identityID uuid.UUID, clusterURL string, options ...rest.HTTPClientOption) error { + signer := newJWTSASigner(ctx, s.config, options...) + remoteClusterService, err := signer.createSignedClient() + if err != nil { + return err + } + identityToClusterData := &clusterclient.UnLinkIdentityToClusterdata{ + ClusterURL: clusterURL, + IdentityID: identityID.String(), + } + res, err := remoteClusterService.RemoveIdentityToClusterLinkClusters(goasupport.ForwardContextRequestID(ctx), clusterclient.RemoveIdentityToClusterLinkClustersPath(), identityToClusterData) + if err != nil { + return errors.Wrapf(err, "failed to unlink identity %s from cluster having url %s", identityID, clusterURL) + } + defer rest.CloseResponse(res) + bodyString := rest.ReadBody(res.Body) // To prevent FDs leaks + if res.StatusCode != http.StatusNoContent { + log.Error(ctx, map[string]interface{}{ + "identity_id": identityID, + "cluster_url": clusterURL, + "response_status": res.Status, + "response_body": bodyString, + }, "unable to remove identity cluster relationship in cluster management service") + return errors.Errorf("failed to unlink identity to cluster in cluster management service. Response status: %s. Response body: %s", res.Status, bodyString) + } + return nil +} + // Start initializes the default Cluster cache if it's not initialized already // Cache initialization loads the list of clusters from the cluster management service and starts regular cache refresher func Start(ctx context.Context, factory service.ClusterCacheFactory, options ...rest.HTTPClientOption) (bool, error) { @@ -93,10 +159,10 @@ func Start(ctx context.Context, factory service.ClusterCacheFactory, options ... } else { clusterCache = nil } - return (clusterCache != nil && started == uint32(1)), err + return clusterCache != nil && started == uint32(1), err } } - return (clusterCache != nil && started == uint32(1)), nil + return clusterCache != nil && started == uint32(1), nil } // Clusters converts the given cluster map to an array slice @@ -109,11 +175,60 @@ func Clusters(clusters map[string]cluster.Cluster) []cluster.Cluster { } func ClusterByURL(clusters map[string]cluster.Cluster, url string) *cluster.Cluster { - for apiURL, cluster := range clusters { + for apiURL, c := range clusters { if strings.HasPrefix(rest.AddTrailingSlashToURL(url), apiURL) { - return &cluster + return &c } } return nil } + +type saSigner interface { + createSignedClient() (*clusterclient.Client, error) +} + +type jwtSASigner struct { + ctx context.Context + config clusterConfig + options []rest.HTTPClientOption +} + +func newJWTSASigner(ctx context.Context, config clusterConfig, options ...rest.HTTPClientOption) saSigner { + return &jwtSASigner{ctx, config, options} +} + +// CreateSignedClient creates a client with a JWT signer which uses the Auth Service Account token +func (c jwtSASigner) createSignedClient() (*clusterclient.Client, error) { + cln, err := c.createClient(c.ctx) + if err != nil { + return nil, err + } + m, err := manager.DefaultManager(c.config) + if err != nil { + return nil, err + } + signer := m.AuthServiceAccountSigner() + cln.SetJWTSigner(signer) + return cln, nil +} + +func (c jwtSASigner) createClient(ctx context.Context) (*clusterclient.Client, error) { + u, err := url.Parse(c.config.GetClusterServiceURL()) + if err != nil { + return nil, err + } + + httpClient := http.DefaultClient + + if c.options != nil { + for _, opt := range c.options { + opt(httpClient) + } + } + cln := clusterclient.New(goaclient.HTTPClientDoer(httpClient)) + + cln.Host = u.Host + cln.Scheme = u.Scheme + return cln, nil +} diff --git a/cluster/service/cluster_blackbox_test.go b/cluster/service/cluster_blackbox_test.go index e4a1174a..929ac8e8 100644 --- a/cluster/service/cluster_blackbox_test.go +++ b/cluster/service/cluster_blackbox_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/fabric8-services/fabric8-auth/authorization/token/manager" - "github.com/fabric8-services/fabric8-auth/cluster" "github.com/fabric8-services/fabric8-auth/cluster/factory" clusterservice "github.com/fabric8-services/fabric8-auth/cluster/service" "github.com/fabric8-services/fabric8-auth/gormtestsupport" @@ -16,6 +15,8 @@ import ( "github.com/dnaeon/go-vcr/cassette" vcrec "github.com/dnaeon/go-vcr/recorder" + "github.com/fabric8-services/fabric8-auth/cluster" + "github.com/satori/go.uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -48,37 +49,154 @@ func (s *ClusterServiceTestSuite) TestClustersFail() { ctx, _, reqID := tokentestsupport.ContextWithTokenAndRequestID(s.T()) s.T().Run("clusters() fails if can't get clusters", func(t *testing.T) { - r, err := recorder.New("../../test/data/cluster/cluster_get_error", recorder.WithMatcher(clusterRequestMatcher(t, reqID, s.saToken))) + r, err := recorder.New("../../test/data/cluster/cluster_get_error", recorder.WithDefaultMatcher(reqID, s.saToken)) require.NoError(t, err) defer func() { require.NoError(t, stopRecorder(r)) }() _, err = s.Application.ClusterService().Clusters(ctx, rest.WithRoundTripper(r.Transport)) - require.EqualError(t, err, "unable to get clusters from Cluster Management Service. Response status: 500 Internal Server Error. Response body: oopsy woopsy", err.Error()) + require.EqualError(t, err, "unable to get clusters from Cluster Management Service. Response status: 500 Internal Server Error. Response body: oopsy woopsy") }) s.T().Run("clusters() fails if can't get clusters", func(t *testing.T) { - r, err := recorder.New("../../test/data/cluster/cluster_get_error", recorder.WithMatcher(clusterRequestMatcher(t, reqID, s.saToken))) + r, err := recorder.New("../../test/data/cluster/cluster_get_error", recorder.WithDefaultMatcher(reqID, s.saToken)) require.NoError(t, err) defer func() { require.NoError(t, stopRecorder(r)) }() _, err = s.Application.ClusterService().ClusterByURL(ctx, "https://api.starter-us-east-2.openshift.com/") - assert.EqualError(t, err, "unable to get clusters from Cluster Management Service. Response status: 500 Internal Server Error. Response body: oopsy woopsy", err.Error()) + assert.EqualError(t, err, "unable to get clusters from Cluster Management Service. Response status: 500 Internal Server Error. Response body: oopsy woopsy") + }) +} + +func (s *ClusterServiceTestSuite) TestRemoveIdentityToClusterLinkOK() { + ctx, _, reqID := tokentestsupport.ContextWithTokenAndRequestID(s.T()) + identityID := createIdentityIDFromString(s.T()) + + s.T().Run("200", func(t *testing.T) { + clusterURL := "https://cluster.ok/" + r, err := recorder.New("../../test/data/cluster/unlink_identity_to_cluster", recorder.WithUnLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().UnlinkIdentityFromCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + assert.NoError(t, err) + }) +} + +func (s *ClusterServiceTestSuite) TestRemoveIdentityToClusterLinkFail() { + ctx, _, reqID := tokentestsupport.ContextWithTokenAndRequestID(s.T()) + identityID := createIdentityIDFromString(s.T()) + + s.T().Run("500", func(t *testing.T) { + clusterURL := "https://cluster.error/" + r, err := recorder.New("../../test/data/cluster/unlink_identity_to_cluster", recorder.WithUnLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().UnlinkIdentityFromCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + require.EqualError(t, err, "failed to unlink identity to cluster in cluster management service. Response status: 500 Internal Server Error. Response body: oopsy woopsy") + }) + + s.T().Run("400", func(t *testing.T) { + clusterURL := "https://cluster.bad/" + r, err := recorder.New("../../test/data/cluster/unlink_identity_to_cluster", recorder.WithUnLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().UnlinkIdentityFromCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + require.EqualError(t, err, "failed to unlink identity to cluster in cluster management service. Response status: 400 Bad Request. Response body: invalid identity-id") + }) + + s.T().Run("401", func(t *testing.T) { + clusterURL := "https://cluster.unauthorized/" + r, err := recorder.New("../../test/data/cluster/unlink_identity_to_cluster", recorder.WithUnLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().UnlinkIdentityFromCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + require.EqualError(t, err, "failed to unlink identity to cluster in cluster management service. Response status: 401 Unauthorized. Response body: unauthorized") + }) + + s.T().Run("404", func(t *testing.T) { + clusterURL := "https://cluster.notfound/" + r, err := recorder.New("../../test/data/cluster/unlink_identity_to_cluster", recorder.WithUnLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().UnlinkIdentityFromCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + require.EqualError(t, err, "failed to unlink identity to cluster in cluster management service. Response status: 404 Not Found. Response body: not found") }) } +func (s *ClusterServiceTestSuite) TestAddIdentityToClusterLinkOK() { + ctx, _, reqID := tokentestsupport.ContextWithTokenAndRequestID(s.T()) + identityID := createIdentityIDFromString(s.T()) + + s.T().Run("200", func(t *testing.T) { + clusterURL := "https://cluster.ok/" + r, err := recorder.New("../../test/data/cluster/link_identity_to_cluster", recorder.WithLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().LinkIdentityToCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + assert.NoError(t, err) + }) +} + +func (s *ClusterServiceTestSuite) TestAddIdentityToClusterLinkFail() { + ctx, _, reqID := tokentestsupport.ContextWithTokenAndRequestID(s.T()) + identityID := createIdentityIDFromString(s.T()) + + s.T().Run("500", func(t *testing.T) { + clusterURL := "https://cluster.error/" + r, err := recorder.New("../../test/data/cluster/link_identity_to_cluster", recorder.WithLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().LinkIdentityToCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + require.EqualError(t, err, "failed to link identity to cluster in cluster management service. Response status: 500 Internal Server Error. Response body: oopsy woopsy") + }) + + s.T().Run("400", func(t *testing.T) { + clusterURL := "https://cluster.bad/" + r, err := recorder.New("../../test/data/cluster/link_identity_to_cluster", recorder.WithLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().LinkIdentityToCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + require.EqualError(t, err, "failed to link identity to cluster in cluster management service. Response status: 400 Bad Request. Response body: invalid identity-id") + }) + + s.T().Run("401", func(t *testing.T) { + clusterURL := "https://cluster.unauthorized/" + r, err := recorder.New("../../test/data/cluster/link_identity_to_cluster", recorder.WithLinkIdentityToClusterRequestPayloadMatcher(clusterURL, reqID, s.saToken)) + require.NoError(t, err) + defer func() { require.NoError(t, stopRecorder(r)) }() + + err = s.Application.ClusterService().LinkIdentityToCluster(ctx, identityID, clusterURL, rest.WithRoundTripper(r.Transport)) + require.EqualError(t, err, "failed to link identity to cluster in cluster management service. Response status: 401 Unauthorized. Response body: unauthorized") + }) +} + +func createIdentityIDFromString(t *testing.T) uuid.UUID { + identityID, err := uuid.FromString("715eab6a-9818-4642-8002-1d0cf8939007") + require.NoError(t, err) + + return identityID +} + type dummyFactory struct { config factory.ClusterCacheFactoryConfiguration option rest.HTTPClientOption } func (f *dummyFactory) NewClusterCache(ctx context.Context, options ...rest.HTTPClientOption) cluster.ClusterCache { - return cluster.NewCache(f.config, f.option) + return clusterservice.NewCache(f.config, f.option) } func (s *ClusterServiceTestSuite) TestStart() { ctx, _, reqID := tokentestsupport.ContextWithTokenAndRequestID(s.T()) s.T().Run("start fails if can't get clusters", func(t *testing.T) { - r, err := recorder.New("../../test/data/cluster/cluster_get_error", recorder.WithMatcher(clusterRequestMatcher(t, reqID, s.saToken))) + r, err := recorder.New("../../test/data/cluster/cluster_get_error", recorder.WithDefaultMatcher(reqID, s.saToken)) require.NoError(t, err) defer func() { require.NoError(t, stopRecorder(r)) }() @@ -94,7 +212,7 @@ func (s *ClusterServiceTestSuite) TestStart() { }) s.T().Run("start OK", func(t *testing.T) { - r, err := recorder.New("../../test/data/cluster/cluster_get_ok", recorder.WithMatcher(clusterRequestMatcher(t, reqID, s.saToken))) + r, err := recorder.New("../../test/data/cluster/cluster_get_ok", recorder.WithDefaultMatcher(reqID, s.saToken)) require.NoError(t, err) defer func() { require.NoError(t, stopRecorder(r)) }() @@ -155,19 +273,3 @@ func stopRecorder(r *vcrec.Recorder) error { }) return nil } - -func clusterRequestMatcher(t *testing.T, reqID, token string) cassette.Matcher { - return func(httpRequest *http.Request, cassetteRequest cassette.Request) bool { - authorization := httpRequest.Header.Get("Authorization") - assert.Equal(t, "Bearer "+token, authorization) - - rID := httpRequest.Header.Get("X-Request-Id") - assert.Equal(t, reqID, rID) - - assert.Equal(t, cassetteRequest.Method, httpRequest.Method) - require.NotNil(t, cassetteRequest.Method, httpRequest.URL) - assert.Equal(t, cassetteRequest.URL, httpRequest.URL.String()) - - return true - } -} diff --git a/controller/users.go b/controller/users.go index 112b5d79..ca40918a 100644 --- a/controller/users.go +++ b/controller/users.go @@ -155,6 +155,19 @@ func (c *UsersController) Create(ctx *app.CreateUsersContext) error { return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(ctx, err)) } + // we create a identity cluster relationship in cluster management service. + clusterURL := ctx.Payload.Data.Attributes.Cluster + err = c.app.ClusterService().LinkIdentityToCluster(ctx.Context, identityID, clusterURL) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + "identity_id": identityID, + "cluster_url": clusterURL, + }, "failed to link identity to cluster in cluster service") + // Not a blocker. Log the error and proceed. + // TODO roll back user creation here (hard delete user&identity from DB) if failed and return error See https://github.com/fabric8-services/fabric8-auth/issues/747 + } + // finally, if all works, we create a user in WIT too. err = c.app.WITService().CreateUser(ctx.Context, identity, identityID.String()) if err != nil { @@ -169,7 +182,7 @@ func (c *UsersController) Create(ctx *app.CreateUsersContext) error { } func (c *UsersController) checkPreviewUser(email string) (bool, error) { - // Any +preview*@redhat.com email matches + // Any +preview*@redhat.com email matches. Note that this is set only for production. not in prod-preview return regexp.MatchString(c.config.GetIgnoreEmailInProd(), strings.ToLower(email)) } diff --git a/controller/users_blackbox_test.go b/controller/users_blackbox_test.go index c8a7b065..02e5ad4c 100644 --- a/controller/users_blackbox_test.go +++ b/controller/users_blackbox_test.go @@ -38,19 +38,21 @@ func TestUsersController(t *testing.T) { type UsersControllerTestSuite struct { gormtestsupport.DBTestSuite - svc *goa.Service - controller *UsersController - userRepo accountrepo.UserRepository - identityRepo accountrepo.IdentityRepository - tenantService *dummyTenantService - witService *testservice.WITServiceMock + svc *goa.Service + controller *UsersController + userRepo accountrepo.UserRepository + identityRepo accountrepo.IdentityRepository + tenantService *dummyTenantService + witService *testservice.WITServiceMock + clusterServiceMock *testservice.ClusterServiceMock } func (s *UsersControllerTestSuite) SetupSuite() { s.DBTestSuite.SetupSuite() s.svc = goa.New("test") s.witService = testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") - s.Application = gormapplication.NewGormDB(s.DB, s.Configuration, s.Wrappers, factory.WithWITService(s.witService)) + s.clusterServiceMock = testsupport.NewClusterServiceMock(s.T()) + s.Application = gormapplication.NewGormDB(s.DB, s.Configuration, s.Wrappers, factory.WithWITService(s.witService), factory.WithClusterService(s.clusterServiceMock)) s.controller = NewUsersController(s.svc, s.Application, s.Configuration) s.userRepo = s.Application.Users() s.identityRepo = s.Application.Identities() @@ -1414,6 +1416,7 @@ func (s *UsersControllerTestSuite) TestCreateUserAsServiceAccountWithAllFieldsOK approved := false *s.witService = *testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") + *s.clusterServiceMock = *testsupport.NewClusterServiceMock(s.T()) secureService, secureController := s.SecuredServiceAccountController(testsupport.TestOnlineRegistrationAppIdentity) @@ -1424,6 +1427,7 @@ func (s *UsersControllerTestSuite) TestCreateUserAsServiceAccountWithAllFieldsOK _, appUser := test.CreateUsersOK(s.T(), secureService.Context, secureService, secureController, createUserPayload) assertCreatedUser(s.T(), appUser.Data, user, identity) require.Equal(s.T(), uint64(1), s.witService.CreateUserCounter) + require.Equal(s.T(), uint64(1), s.clusterServiceMock.LinkIdentityToClusterCounter) } func (s *UsersControllerTestSuite) TestCreateUserAsServiceAccountForExistingUserInDbFails() { @@ -1513,6 +1517,8 @@ func (s *UsersControllerTestSuite) checkCreateUserAsServiceAccountOK(email strin } *s.witService = *testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") + *s.clusterServiceMock = *testsupport.NewClusterServiceMock(s.T()) + secureService, secureController := s.SecuredServiceAccountController(testsupport.TestOnlineRegistrationAppIdentity) createUserPayload := newCreateUsersPayload(&email, nil, nil, nil, nil, nil, &identity.Username, nil, user.ID.String(), &user.Cluster, nil, nil, nil) @@ -1521,6 +1527,7 @@ func (s *UsersControllerTestSuite) checkCreateUserAsServiceAccountOK(email strin _, appUser := test.CreateUsersOK(s.T(), secureService.Context, secureService, secureController, createUserPayload) assertCreatedUser(s.T(), appUser.Data, user, identity) require.Equal(s.T(), uint64(1), s.witService.CreateUserCounter) + require.Equal(s.T(), uint64(1), s.clusterServiceMock.LinkIdentityToClusterCounter) } func (s *UsersControllerTestSuite) TestCreateUserAsServiceAccountWithMissingRequiredFieldsFails() { @@ -1549,6 +1556,7 @@ func (s *UsersControllerTestSuite) TestCreateUserAsServiceAccountUnauthorized() user := testsupport.TestUser identity := testsupport.TestIdentity *s.witService = *testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") + *s.clusterServiceMock = *testsupport.NewClusterServiceMock(s.T()) secureService, secureController := s.SecuredServiceAccountController(testsupport.TestIdentity) @@ -1556,6 +1564,7 @@ func (s *UsersControllerTestSuite) TestCreateUserAsServiceAccountUnauthorized() createUserPayload := newCreateUsersPayload(&user.Email, &user.FullName, &user.Bio, &user.ImageURL, &user.URL, &user.Company, &identity.Username, nil, user.ID.String(), &user.Cluster, &identity.RegistrationCompleted, nil, user.ContextInformation) test.CreateUsersUnauthorized(s.T(), secureService.Context, secureService, secureController, createUserPayload) require.Equal(s.T(), uint64(0), s.witService.CreateUserCounter) + require.Equal(s.T(), uint64(0), s.clusterServiceMock.LinkIdentityToClusterCounter) } func (s *UsersControllerTestSuite) TestCreateUserAsNonServiceAccountUnauthorized() { @@ -1563,9 +1572,7 @@ func (s *UsersControllerTestSuite) TestCreateUserAsNonServiceAccountUnauthorized user := testsupport.TestUser identity := testsupport.TestIdentity *s.witService = *testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") - - //*s.witService = *testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") - *s.witService = *testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") + *s.clusterServiceMock = *testsupport.NewClusterServiceMock(s.T()) secureService, secureController := s.SecuredController(testsupport.TestIdentity) @@ -1573,6 +1580,7 @@ func (s *UsersControllerTestSuite) TestCreateUserAsNonServiceAccountUnauthorized createUserPayload := newCreateUsersPayload(&user.Email, &user.FullName, &user.Bio, &user.ImageURL, &user.URL, &user.Company, &identity.Username, nil, user.ID.String(), &user.Cluster, &identity.RegistrationCompleted, nil, user.ContextInformation) test.CreateUsersUnauthorized(s.T(), secureService.Context, secureService, secureController, createUserPayload) require.Equal(s.T(), uint64(0), s.witService.CreateUserCounter) + require.Equal(s.T(), uint64(0), s.clusterServiceMock.LinkIdentityToClusterCounter) } func (s *UsersControllerTestSuite) TestCreateUserAsServiceAccountForPreviewUserIgnored() { @@ -1588,6 +1596,7 @@ func (s *UsersControllerTestSuite) TestCreateUserAsServiceAccountForPreviewUserI func (s *UsersControllerTestSuite) checkCreateUserAsServiceAccountForPreviewUserIgnored(email string) { *s.witService = *testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") + *s.clusterServiceMock = *testsupport.NewClusterServiceMock(s.T()) secureService, secureController := s.SecuredServiceAccountController(testsupport.TestOnlineRegistrationAppIdentity) username := "someuser" @@ -1599,6 +1608,7 @@ func (s *UsersControllerTestSuite) checkCreateUserAsServiceAccountForPreviewUser require.NotNil(s.T(), appUser) assertCreatedUser(s.T(), appUser.Data, accountrepo.User{Cluster: cluster, Email: email}, accountrepo.Identity{Username: username}) require.Equal(s.T(), uint64(0), s.witService.CreateUserCounter) + require.Equal(s.T(), uint64(0), s.clusterServiceMock.LinkIdentityToClusterCounter) } func (s *UsersControllerTestSuite) TestCreateUserUnauthorized() { @@ -1606,11 +1616,13 @@ func (s *UsersControllerTestSuite) TestCreateUserUnauthorized() { user := testsupport.TestUser identity := testsupport.TestIdentity *s.witService = *testsupport.NewWITMock(s.T(), uuid.NewV4().String(), "test-space") + *s.clusterServiceMock = *testsupport.NewClusterServiceMock(s.T()) // then createUserPayload := newCreateUsersPayload(&user.Email, &user.FullName, &user.Bio, &user.ImageURL, &user.URL, &user.Company, &identity.Username, nil, user.ID.String(), &user.Cluster, &identity.RegistrationCompleted, nil, user.ContextInformation) test.CreateUsersUnauthorized(s.T(), context.Background(), nil, s.controller, createUserPayload) require.Equal(s.T(), uint64(0), s.witService.CreateUserCounter) + require.Equal(s.T(), uint64(0), s.clusterServiceMock.LinkIdentityToClusterCounter) } func newCreateUsersPayload(email, fullName, bio, imageURL, profileURL, company, username, rhdUsername *string, rhdUserID string, cluster *string, registrationCompleted, approved *bool, contextInformation map[string]interface{}) *app.CreateUsersPayload { diff --git a/test/clusterservice.go b/test/clusterservice.go index c82b56ab..3e42b7fe 100644 --- a/test/clusterservice.go +++ b/test/clusterservice.go @@ -33,6 +33,12 @@ func NewClusterServiceMock(t minimock.Tester) *testservice.ClusterServiceMock { clusterServiceMock.StatusFunc = func(ctx context.Context, options ...rest.HTTPClientOption) (bool, error) { return false, nil } + clusterServiceMock.LinkIdentityToClusterFunc = func(p context.Context, identityID uuid.UUID, clusterURL string, options ...rest.HTTPClientOption) (r error) { + return nil + } + clusterServiceMock.UnlinkIdentityFromClusterFunc = func(p context.Context, identityID uuid.UUID, clusterURL string, options ...rest.HTTPClientOption) (r error) { + return nil + } return clusterServiceMock } diff --git a/test/data/cluster/link_identity_to_cluster.yaml b/test/data/cluster/link_identity_to_cluster.yaml new file mode 100644 index 00000000..d1076268 --- /dev/null +++ b/test/data/cluster/link_identity_to_cluster.yaml @@ -0,0 +1,62 @@ +--- +version: 1 +interactions: +- request: + body: | + { + "cluster-url": "https://cluster.ok/", + "identity-id": "715eab6a-9818-4642-8002-1d0cf8939007" + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: POST + response: + status: 204 No Content + code: 204 +- request: + body: | + { + "cluster-url": "https://cluster.error/", + "identity-id": "715eab6a-9818-4642-8002-1d0cf8939007" + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: POST + response: + body: "oopsy woopsy" + status: 500 Internal Server Error + code: 500 +- request: + body: | + { + "cluster-url": "https://cluster.bad/", + "identity-id": "Quis consequuntur ipsam qui." + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: POST + response: + status: 400 Bad Request + code: 400 + body: "invalid identity-id" +- request: + body: | + { + "cluster-url": "https://cluster.unauthorized/", + "identity-id": "715eab6a-9818-4642-8002-1d0cf8939007" + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: POST + response: + body: "unauthorized" + status: 401 Unauthorized + code: 401 diff --git a/test/data/cluster/unlink_identity_to_cluster.yaml b/test/data/cluster/unlink_identity_to_cluster.yaml new file mode 100644 index 00000000..9fa64ec5 --- /dev/null +++ b/test/data/cluster/unlink_identity_to_cluster.yaml @@ -0,0 +1,77 @@ +--- +version: 1 +interactions: +- request: + body: | + { + "cluster-url": "https://cluster.ok/", + "identity-id": "715eab6a-9818-4642-8002-1d0cf8939007" + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: DELETE + response: + status: 204 No Content + code: 204 +- request: + body: | + { + "cluster-url": "https://cluster.error/", + "identity-id": "715eab6a-9818-4642-8002-1d0cf8939007" + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: DELETE + response: + body: "oopsy woopsy" + status: 500 Internal Server Error + code: 500 +- request: + body: | + { + "cluster-url": "https://cluster.bad/", + "identity-id": "Quis consequuntur ipsam qui." + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: DELETE + response: + status: 400 Bad Request + code: 400 + body: "invalid identity-id" +- request: + body: | + { + "cluster-url": "https://cluster.unauthorized/", + "identity-id": "715eab6a-9818-4642-8002-1d0cf8939007" + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: DELETE + response: + body: "unauthorized" + status: 401 Unauthorized + code: 401 +- request: + body: | + { + "cluster-url": "https://cluster.notfound/", + "identity-id": "715eab6a-9818-4642-8002-1d0cf8939007" + } + headers: + Content-Type: + - application/json + url: http://f8cluster/api/clusters/identities + method: DELETE + response: + body: "not found" + status: 404 Not Found + code: 404 diff --git a/test/recorder/cluster.go b/test/recorder/cluster.go new file mode 100644 index 00000000..22a91f74 --- /dev/null +++ b/test/recorder/cluster.go @@ -0,0 +1,96 @@ +package recorder + +import ( + "encoding/json" + "github.com/dnaeon/go-vcr/cassette" + "github.com/dnaeon/go-vcr/recorder" + "github.com/fabric8-services/fabric8-auth/log" + "github.com/fabric8-services/fabric8-cluster-client/cluster" + "net/http" + "strings" +) + +type RequestParamMatcher interface { + MatchClusterURL(r cassette.Request, url string) bool + RequestMatcher(clusterURL string) cassette.Matcher +} + +type LinkIdentityToClusterMatcher struct { + DefaultRequestMatcher +} + +func (m LinkIdentityToClusterMatcher) MatchClusterURL(r cassette.Request, url string) bool { + payload := cluster.LinkIdentityToClusterData{} + if err := json.NewDecoder(strings.NewReader(r.Body)).Decode(&payload); err != nil { + log.Error(nil, map[string]interface{}{"error": err.Error()}, "Cassette request payload doesn't match with LinkIdentityToClusterData payload") + return false + } + + if payload.ClusterURL != "" { + return payload.ClusterURL == url + } + return false +} + +func (m LinkIdentityToClusterMatcher) RequestMatcher(clusterURL string) cassette.Matcher { + return func(httpRequest *http.Request, cassetteRequest cassette.Request) bool { + if ok := m.RequestMethodAndURLMatch(httpRequest, cassetteRequest); !ok { + return ok + } + + if m.MatchClusterURL(cassetteRequest, clusterURL) { + return m.HeaderMatch(httpRequest, cassetteRequest) + } + + return false + } +} + +type UnLinkIdentityToClusterMatcher struct { + DefaultRequestMatcher +} + +func (m UnLinkIdentityToClusterMatcher) MatchClusterURL(r cassette.Request, url string) bool { + payload := cluster.UnLinkIdentityToClusterdata{} + if err := json.NewDecoder(strings.NewReader(r.Body)).Decode(&payload); err != nil { + log.Error(nil, map[string]interface{}{"error": err.Error()}, "Cassette request payload doesn't match with UnLinkIdentityToClusterdata payload") + return false + } + + if payload.ClusterURL != "" { + return payload.ClusterURL == url + } + return false +} + +func (m UnLinkIdentityToClusterMatcher) RequestMatcher(clusterURL string) cassette.Matcher { + return func(httpRequest *http.Request, cassetteRequest cassette.Request) bool { + if ok := m.RequestMethodAndURLMatch(httpRequest, cassetteRequest); !ok { + return ok + } + + if m.MatchClusterURL(cassetteRequest, clusterURL) { + return m.HeaderMatch(httpRequest, cassetteRequest) + } + + return false + } +} + +// WithLinkIdentityToClusterRequestPayloadMatcher an option to specify the RequestPayload matcher for the recorder +func WithLinkIdentityToClusterRequestPayloadMatcher(clusterURL, requestID, saToken string) Option { + linkIdentityToClusterMatcher := &LinkIdentityToClusterMatcher{DefaultRequestMatcher{requestID: requestID, saToken: saToken}} + + return func(r *recorder.Recorder) { + r.SetMatcher(linkIdentityToClusterMatcher.RequestMatcher(clusterURL)) + } +} + +// WithUnLinkIdentityToClusterRequestPayloadMatcher an option to specify the RequestPayload matcher for the recorder +func WithUnLinkIdentityToClusterRequestPayloadMatcher(clusterURL, requestID, saToken string) Option { + unLinkIdentityToClusterMatcher := &UnLinkIdentityToClusterMatcher{DefaultRequestMatcher{requestID: requestID, saToken: saToken}} + + return func(r *recorder.Recorder) { + r.SetMatcher(unLinkIdentityToClusterMatcher.RequestMatcher(clusterURL)) + } +} diff --git a/test/recorder/notification.go b/test/recorder/notification.go new file mode 100644 index 00000000..67124f4f --- /dev/null +++ b/test/recorder/notification.go @@ -0,0 +1,64 @@ +package recorder + +import ( + "encoding/json" + "github.com/dnaeon/go-vcr/cassette" + "github.com/dnaeon/go-vcr/recorder" + "github.com/fabric8-services/fabric8-auth/log" + "github.com/fabric8-services/fabric8-auth/notification/client" + goauuid "github.com/goadesign/goa/uuid" + "github.com/satori/go.uuid" + "net/http" + "strings" +) + +// JWTMatcher a cassette matcher that verifies the request method/URL and the subject of the token in the "Authorization" header. +func NotifyRequestPayloadMatcher(messageID *uuid.UUID) cassette.Matcher { + return func(httpRequest *http.Request, cassetteRequest cassette.Request) bool { + // check the request URI and method + if httpRequest.Method != cassetteRequest.Method || + (httpRequest.URL != nil && httpRequest.URL.String() != cassetteRequest.URL) { + log.Debug(nil, map[string]interface{}{ + "httpRequest_method": httpRequest.Method, + "cassetteRequest_method": cassetteRequest.Method, + "httpRequest_url": httpRequest.URL, + "cassetteRequest_url": cassetteRequest.URL, + }, "Cassette method/url doesn't match with the current request") + return false + } + + payload := client.SendNotifyPayload{} + if err := json.NewDecoder(strings.NewReader(cassetteRequest.Body)).Decode(&payload); err != nil { + log.Error(nil, map[string]interface{}{"error": err.Error()}, "Cassette request payload doesn't match with notification payload") + return false + } + + if messageUUID, e := goauuid.FromString(messageID.String()); e == nil && payload.Data != nil { + return *payload.Data.ID == messageUUID + + } + + return false + } +} + +func NotifyRequestHeaderPayloadMatcher(messageID *uuid.UUID, requestID, saToken string) cassette.Matcher { + return func(httpRequest *http.Request, cassetteRequest cassette.Request) bool { + + if NotifyRequestPayloadMatcher(messageID)(httpRequest, cassetteRequest) { + authorization := httpRequest.Header.Get("Authorization") + reqID := httpRequest.Header.Get("X-Request-Id") + + return "Bearer "+saToken == authorization && reqID == requestID + } + + return false + } +} + +// WithNotifyRequestPayloadMatcher an option to specify the RequestPayload matcher for the recorder +func WithNotifyRequestPayloadMatcher(messageID *uuid.UUID) Option { + return func(r *recorder.Recorder) { + r.SetMatcher(NotifyRequestPayloadMatcher(messageID)) + } +} diff --git a/test/recorder/recorder.go b/test/recorder/recorder.go index eb510808..bddb50dd 100644 --- a/test/recorder/recorder.go +++ b/test/recorder/recorder.go @@ -4,16 +4,11 @@ import ( "fmt" "os" - "encoding/json" "github.com/dnaeon/go-vcr/cassette" "github.com/dnaeon/go-vcr/recorder" "github.com/fabric8-services/fabric8-auth/log" - "github.com/fabric8-services/fabric8-auth/notification/client" - uuid2 "github.com/goadesign/goa/uuid" errs "github.com/pkg/errors" - "github.com/satori/go.uuid" "net/http" - "strings" ) // Option an option to customize the recorder to create @@ -26,13 +21,6 @@ func WithMatcher(matcher cassette.Matcher) Option { } } -// WithNotifyRequestPayloadMatcher an option to specify the RequestPayload matcher for the recorder -func WithNotifyRequestPayloadMatcher(messageID *uuid.UUID) Option { - return func(r *recorder.Recorder) { - r.SetMatcher(NotifyRequestPayloadMatcher(messageID)) - } -} - // New creates a new recorder func New(cassetteName string, options ...Option) (*recorder.Recorder, error) { _, err := os.Stat(fmt.Sprintf("%s.yaml", cassetteName)) @@ -50,46 +38,46 @@ func New(cassetteName string, options ...Option) (*recorder.Recorder, error) { return r, nil } -// JWTMatcher a cassette matcher that verifies the request method/URL and the subject of the token in the "Authorization" header. -func NotifyRequestPayloadMatcher(messageID *uuid.UUID) cassette.Matcher { - return func(httpRequest *http.Request, cassetteRequest cassette.Request) bool { - // check the request URI and method - if httpRequest.Method != cassetteRequest.Method || - (httpRequest.URL != nil && httpRequest.URL.String() != cassetteRequest.URL) { - log.Debug(nil, map[string]interface{}{ - "httpRequest_method": httpRequest.Method, - "cassetteRequest_method": cassetteRequest.Method, - "httpRequest_url": httpRequest.URL, - "cassetteRequest_url": cassetteRequest.URL, - }, "Cassette method/url doesn't match with the current request") - return false - } - - payload := client.SendNotifyPayload{} - if err := json.NewDecoder(strings.NewReader(cassetteRequest.Body)).Decode(&payload); err != nil { - log.Error(nil, map[string]interface{}{"error": err.Error()}, "Cassette request payload doesn't match with notification payload") - return false - } - - if messageUUID, e := uuid2.FromString(messageID.String()); e == nil && payload.Data != nil { - return *payload.Data.ID == messageUUID - - } +// DefaultRequestMatcher to compare the request method, URL requestID, Service Account Token. +type DefaultRequestMatcher struct { + saToken string + requestID string +} +// RequestMethodAndURLMatch matches request Method and URL +func (r DefaultRequestMatcher) RequestMethodAndURLMatch(httpRequest *http.Request, cassetteRequest cassette.Request) bool { + if httpRequest.Method != cassetteRequest.Method || + (httpRequest.URL != nil && httpRequest.URL.String() != cassetteRequest.URL) { + log.Debug(nil, map[string]interface{}{ + "httpRequest_method": httpRequest.Method, + "cassetteRequest_method": cassetteRequest.Method, + "httpRequest_url": httpRequest.URL, + "cassetteRequest_url": cassetteRequest.URL, + }, "Cassette method/url doesn't match with the current request") return false } + return true } -func NotifyRequestHeaderPayloadMatcher(messageID *uuid.UUID, requestID, saToken string) cassette.Matcher { - return func(httpRequest *http.Request, cassetteRequest cassette.Request) bool { +// RequestMethodAndURLMatch matches requestID and Service Account Token +func (r DefaultRequestMatcher) HeaderMatch(httpRequest *http.Request, cassetteRequest cassette.Request) bool { + authorization := httpRequest.Header.Get("Authorization") + reqID := httpRequest.Header.Get("X-Request-Id") + + return "Bearer "+r.saToken == authorization && reqID == r.requestID +} - if NotifyRequestPayloadMatcher(messageID)(httpRequest, cassetteRequest) { - authorization := httpRequest.Header.Get("Authorization") - reqID := httpRequest.Header.Get("X-Request-Id") +// WithDefaultMatcher an option to specify the custom matcher with requestID and Service Account Token for the recorder +func WithDefaultMatcher(requestID, saToken string) Option { + matcher := &DefaultRequestMatcher{saToken, requestID} - return "Bearer "+saToken == authorization && reqID == requestID - } + return func(r *recorder.Recorder) { + r.SetMatcher(func(httpRequest *http.Request, cassetteRequest cassette.Request) bool { + if ok := matcher.RequestMethodAndURLMatch(httpRequest, cassetteRequest); !ok { + return ok + } - return false + return matcher.HeaderMatch(httpRequest, cassetteRequest) + }) } }