From ef1700da948e20d817ebd6bdabc8b3b848e9d87d Mon Sep 17 00:00:00 2001 From: Andrii Holovko Date: Tue, 13 Sep 2022 17:18:34 +0300 Subject: [PATCH] feat: OIDC client credential authorization Closes #751 Signed-off-by: Andrii Holovko --- cmd/vc-rest/startcmd/start.go | 9 +- pkg/restapi/v1/issuer/controller_test.go | 2 +- pkg/restapi/v1/mw/api_key_auth.go | 35 +++ pkg/restapi/v1/mw/api_key_auth_test.go | 63 ++++++ pkg/restapi/v1/util/auth.go | 16 +- pkg/restapi/v1/verifier/controller.go | 113 +++++++--- pkg/restapi/v1/verifier/controller_test.go | 204 ++++++++++++++++-- .../testdata/create_profile_data.json | 2 +- scripts/generate_test_keys.sh | 1 + ....feature => issuer_profile_v1_api.feature} | 8 +- ...eature => verifier_profile_v1_api.feature} | 3 + test/bdd/fixtures/.env | 5 + test/bdd/fixtures/docker-compose.yml | 65 ++++++ .../fixtures/hydra-config/hydra_configure.sh | 28 +++ .../fixtures/mysql-config/mysql_config.sql | 14 ++ .../fixtures/oathkeeper-config/config.yaml | 39 ++++ .../rules/resource-server.json | 41 ++++ test/bdd/go.mod | 4 +- test/bdd/go.sum | 7 +- test/bdd/pkg/bddutil/util.go | 30 +++ test/bdd/pkg/v1/issuer/issuer_steps.go | 72 ++++--- test/bdd/pkg/v1/verifier/verifer_steps.go | 91 ++++---- 22 files changed, 720 insertions(+), 132 deletions(-) create mode 100644 pkg/restapi/v1/mw/api_key_auth.go create mode 100644 pkg/restapi/v1/mw/api_key_auth_test.go rename test/bdd/features/{issuer_v1_api.feature => issuer_profile_v1_api.feature} (82%) rename test/bdd/features/{verifier_profile_api.feature => verifier_profile_v1_api.feature} (93%) create mode 100755 test/bdd/fixtures/hydra-config/hydra_configure.sh create mode 100644 test/bdd/fixtures/mysql-config/mysql_config.sql create mode 100644 test/bdd/fixtures/oathkeeper-config/config.yaml create mode 100644 test/bdd/fixtures/oathkeeper-config/rules/resource-server.json diff --git a/cmd/vc-rest/startcmd/start.go b/cmd/vc-rest/startcmd/start.go index 040b29050..b8076a7a3 100644 --- a/cmd/vc-rest/startcmd/start.go +++ b/cmd/vc-rest/startcmd/start.go @@ -39,6 +39,7 @@ import ( verifierops "github.com/trustbloc/vcs/pkg/restapi/v0.1/verifier/operation" "github.com/trustbloc/vcs/pkg/restapi/v1/healthcheck" issuerv1 "github.com/trustbloc/vcs/pkg/restapi/v1/issuer" + "github.com/trustbloc/vcs/pkg/restapi/v1/mw" verifierv1 "github.com/trustbloc/vcs/pkg/restapi/v1/verifier" "github.com/trustbloc/vcs/pkg/storage/mongodb" "github.com/trustbloc/vcs/pkg/storage/mongodb/issuerstore" @@ -130,6 +131,10 @@ func buildEchoHandler(conf *Configuration) (*echo.Echo, error) { e.Use(echomw.Logger()) e.Use(echomw.Recover()) + if conf.StartupParameters.token != "" { + e.Use(mw.APIKeyAuth(conf.StartupParameters.token)) + } + swagger, err := spec.GetSwagger() if err != nil { return nil, fmt.Errorf("failed to get openapi spec: %w", err) @@ -161,11 +166,11 @@ func buildEchoHandler(conf *Configuration) (*echo.Echo, error) { issuerProfileStore := issuerstore.NewProfileStore(mongodbClient) issuerProfileSvc := issuersvc.NewProfileService(&issuersvc.ServiceConfig{ ProfileStore: issuerProfileStore, - DIDCreator: did.NewCreator(&did.CreatorConfig{ + DIDCreator: did.NewCreator(&did.CreatorConfig{ VDR: conf.VDR, DIDAnchorOrigin: conf.StartupParameters.didAnchorOrigin, }), - KMSRegistry: kmsRegistry, + KMSRegistry: kmsRegistry, }) issuerv1.RegisterHandlers(e, issuerv1.NewController(issuerProfileSvc, kmsRegistry)) diff --git a/pkg/restapi/v1/issuer/controller_test.go b/pkg/restapi/v1/issuer/controller_test.go index a5fc86470..14f977801 100644 --- a/pkg/restapi/v1/issuer/controller_test.go +++ b/pkg/restapi/v1/issuer/controller_test.go @@ -76,7 +76,7 @@ func createContext(orgID string) echo.Context { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) if orgID != "" { - req.Header.Set("Authorization", "Bearer "+orgID) + req.Header.Set("X-User", orgID) } rec := httptest.NewRecorder() diff --git a/pkg/restapi/v1/mw/api_key_auth.go b/pkg/restapi/v1/mw/api_key_auth.go new file mode 100644 index 000000000..fce5b403d --- /dev/null +++ b/pkg/restapi/v1/mw/api_key_auth.go @@ -0,0 +1,35 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package mw + +import ( + "crypto/subtle" + "net/http" + + "github.com/labstack/echo/v4" +) + +const ( + header = "X-API-Key" +) + +// APIKeyAuth returns a middleware that authenticates requests using the API key from X-API-Key header. +func APIKeyAuth(apiKey string) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + apiKeyHeader := c.Request().Header.Get(header) + if subtle.ConstantTimeCompare([]byte(apiKeyHeader), []byte(apiKey)) != 1 { + return &echo.HTTPError{ + Code: http.StatusUnauthorized, + Message: "Unauthorized", + } + } + + return next(c) + } + } +} diff --git a/pkg/restapi/v1/mw/api_key_auth_test.go b/pkg/restapi/v1/mw/api_key_auth_test.go new file mode 100644 index 000000000..7bb85d30c --- /dev/null +++ b/pkg/restapi/v1/mw/api_key_auth_test.go @@ -0,0 +1,63 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package mw_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/require" + + "github.com/trustbloc/vcs/pkg/restapi/v1/mw" +) + +func TestApiKeyAuth(t *testing.T) { + t.Run("Success", func(t *testing.T) { + handlerCalled := false + handler := func(c echo.Context) error { + handlerCalled = true + return c.String(http.StatusOK, "test") + } + + middlewareChain := mw.APIKeyAuth("test-api-key")(handler) + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("X-API-Key", "test-api-key") + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := middlewareChain(c) + + require.NoError(t, err) + require.True(t, handlerCalled) + }) + + t.Run("401 Unauthorized", func(t *testing.T) { + handlerCalled := false + handler := func(c echo.Context) error { + handlerCalled = true + return c.String(http.StatusOK, "test") + } + + middlewareChain := mw.APIKeyAuth("test-api-key")(handler) + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("X-API-Key", "invalid-api-key") + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := middlewareChain(c) + + require.Error(t, err) + require.Contains(t, err.Error(), "Unauthorized") + require.False(t, handlerCalled) + }) +} diff --git a/pkg/restapi/v1/util/auth.go b/pkg/restapi/v1/util/auth.go index f743483b2..dea679ed2 100644 --- a/pkg/restapi/v1/util/auth.go +++ b/pkg/restapi/v1/util/auth.go @@ -7,22 +7,22 @@ SPDX-License-Identifier: Apache-2.0 package util import ( - "fmt" - "strings" + "errors" "github.com/labstack/echo/v4" "github.com/trustbloc/vcs/pkg/restapi/resterr" ) +const ( + userHeader = "X-User" +) + func GetOrgIDFromOIDC(ctx echo.Context) (string, error) { - // TODO: resolve orgID from auth token - authHeader := ctx.Request().Header.Get("Authorization") - if authHeader == "" || !strings.Contains(authHeader, "Bearer") { - return "", resterr.NewUnauthorizedError(fmt.Errorf("missing authorization")) + orgID := ctx.Request().Header.Get(userHeader) + if orgID == "" { + return "", resterr.NewUnauthorizedError(errors.New("missing authorization")) } - orgID := authHeader[len("Bearer "):] // for now assume that token is just plain orgID - return orgID, nil } diff --git a/pkg/restapi/v1/verifier/controller.go b/pkg/restapi/v1/verifier/controller.go index 29179eaae..f8ba1d94a 100644 --- a/pkg/restapi/v1/verifier/controller.go +++ b/pkg/restapi/v1/verifier/controller.go @@ -14,13 +14,18 @@ import ( "errors" "fmt" "net/http" - "strings" "github.com/labstack/echo/v4" + "github.com/trustbloc/vcs/pkg/restapi/resterr" + "github.com/trustbloc/vcs/pkg/restapi/v1/util" "github.com/trustbloc/vcs/pkg/verifier" ) +const ( + verifierProfileSvcComponent = "verifier.ProfileService" +) + var _ ServerInterface = (*Controller)(nil) // make sure Controller implements ServerInterface type profileService interface { @@ -48,17 +53,14 @@ func NewController(profileSvc profileService) *Controller { // GetVerifierProfiles gets all verifier profiles for organization. // GET /verifier/profiles. func (c *Controller) GetVerifierProfiles(ctx echo.Context) error { - // TODO: resolve orgID from auth token - authHeader := ctx.Request().Header.Get("Authorization") - if authHeader == "" || !strings.Contains(authHeader, "Bearer") { - return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization") + orgID, err := util.GetOrgIDFromOIDC(ctx) + if err != nil { + return err } - orgID := authHeader[len("Bearer "):] // for now assume that token is just plain orgID - profiles, err := c.profileSvc.GetAllProfiles(orgID) if err != nil { - return fmt.Errorf("failed to get verifier profiles: %w", err) + return fmt.Errorf("get all profiles: %w", err) } var verifierProfiles []*VerifierProfile @@ -73,15 +75,25 @@ func (c *Controller) GetVerifierProfiles(ctx echo.Context) error { // PostVerifierProfiles creates a new verifier profile. // POST /verifier/profiles. func (c *Controller) PostVerifierProfiles(ctx echo.Context) error { + orgID, err := util.GetOrgIDFromOIDC(ctx) + if err != nil { + return err + } + var body CreateVerifierProfileData - if err := ctx.Bind(&body); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err) + if err = ctx.Bind(&body); err != nil { + return resterr.NewValidationError(resterr.InvalidValue, "requestBody", err) + } + + if body.OrganizationID != orgID { + return resterr.NewValidationError(resterr.InvalidValue, "organizationID", + fmt.Errorf("org id mismatch (want %q, got %q)", orgID, body.OrganizationID)) } createdProfile, err := c.profileSvc.Create(mapCreateVerifierProfileData(&body)) if err != nil { - return fmt.Errorf("failed to create verifier profile: %w", err) + return fmt.Errorf("create profile: %w", err) } return ctx.JSON(http.StatusOK, mapProfile(createdProfile)) @@ -89,9 +101,14 @@ func (c *Controller) PostVerifierProfiles(ctx echo.Context) error { // DeleteVerifierProfilesProfileID deletes profile from VCS storage. // DELETE /verifier/profiles/{profileID}. -func (c *Controller) DeleteVerifierProfilesProfileID(_ echo.Context, profileID string) error { - if err := c.profileSvc.Delete(profileID); err != nil { - return fmt.Errorf("failed to delete verifier profile: %w", err) +func (c *Controller) DeleteVerifierProfilesProfileID(ctx echo.Context, profileID string) error { + profile, err := c.accessProfile(ctx, profileID) + if err != nil { + return err + } + + if err = c.profileSvc.Delete(profile.ID); err != nil { + return fmt.Errorf("delete profile: %w", err) } return nil @@ -100,13 +117,9 @@ func (c *Controller) DeleteVerifierProfilesProfileID(_ echo.Context, profileID s // GetVerifierProfilesProfileID gets profile by ID. // GET /verifier/profiles/{profileID}. func (c *Controller) GetVerifierProfilesProfileID(ctx echo.Context, profileID string) error { - profile, err := c.profileSvc.GetProfile(profileID) + profile, err := c.accessProfile(ctx, profileID) if err != nil { - if errors.Is(err, verifier.ErrProfileNotFound) { - return echo.NewHTTPError(http.StatusNotFound, err) - } - - return fmt.Errorf("failed to get verifier profile: %w", err) + return err } return ctx.JSON(http.StatusOK, mapProfile(profile)) @@ -115,18 +128,23 @@ func (c *Controller) GetVerifierProfilesProfileID(ctx echo.Context, profileID st // PutVerifierProfilesProfileID updates profile. // PUT /verifier/profiles/{profileID}. func (c *Controller) PutVerifierProfilesProfileID(ctx echo.Context, profileID string) error { + profile, err := c.accessProfile(ctx, profileID) + if err != nil { + return err + } + var body UpdateVerifierProfileData - if err := ctx.Bind(&body); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err) + if err = ctx.Bind(&body); err != nil { + return resterr.NewValidationError(resterr.InvalidValue, "requestBody", err) } profileUpdate := mapUpdateVerifierProfileData(&body) - profileUpdate.ID = profileID + profileUpdate.ID = profile.ID updatedProfile, err := c.profileSvc.Update(profileUpdate) if err != nil { - return fmt.Errorf("failed to update verifier profile: %w", err) + return fmt.Errorf("update profile: %w", err) } return ctx.JSON(http.StatusOK, mapProfile(updatedProfile)) @@ -134,9 +152,14 @@ func (c *Controller) PutVerifierProfilesProfileID(ctx echo.Context, profileID st // PostVerifierProfilesProfileIDActivate activates profile. // POST /verifier/profiles/{profileID}/activate. -func (c *Controller) PostVerifierProfilesProfileIDActivate(_ echo.Context, profileID string) error { - if err := c.profileSvc.ActivateProfile(profileID); err != nil { - return fmt.Errorf("failed to activate verifier profile: %w", err) +func (c *Controller) PostVerifierProfilesProfileIDActivate(ctx echo.Context, profileID string) error { + profile, err := c.accessProfile(ctx, profileID) + if err != nil { + return err + } + + if err = c.profileSvc.ActivateProfile(profile.ID); err != nil { + return fmt.Errorf("activate profile: %w", err) } return nil @@ -144,14 +167,44 @@ func (c *Controller) PostVerifierProfilesProfileIDActivate(_ echo.Context, profi // PostVerifierProfilesProfileIDDeactivate deactivates profile. // POST /verifier/profiles/{profileID}/deactivate. -func (c *Controller) PostVerifierProfilesProfileIDDeactivate(_ echo.Context, profileID string) error { - if err := c.profileSvc.DeactivateProfile(profileID); err != nil { - return fmt.Errorf("failed to deactivate verifier profile: %w", err) +func (c *Controller) PostVerifierProfilesProfileIDDeactivate(ctx echo.Context, profileID string) error { + profile, err := c.accessProfile(ctx, profileID) + if err != nil { + return err + } + + if err = c.profileSvc.DeactivateProfile(profile.ID); err != nil { + return fmt.Errorf("deactivate profile: %w", err) } return nil } +func (c *Controller) accessProfile(ctx echo.Context, profileID string) (*verifier.Profile, error) { + orgID, err := util.GetOrgIDFromOIDC(ctx) + if err != nil { + return nil, err + } + + profile, err := c.profileSvc.GetProfile(profileID) + if err != nil { + if errors.Is(err, verifier.ErrProfileNotFound) { + return nil, resterr.NewValidationError(resterr.DoesntExist, "profile", + fmt.Errorf("no profile with id %s", profileID)) + } + + return nil, resterr.NewSystemError(verifierProfileSvcComponent, "GetProfile", err) + } + + // block access to profiles of other organizations + if profile.OrganizationID != orgID { + return nil, resterr.NewValidationError(resterr.DoesntExist, "profile", + fmt.Errorf("no profile with id %s", profileID)) + } + + return profile, nil +} + func mapCreateVerifierProfileData(data *CreateVerifierProfileData) *verifier.Profile { profile := &verifier.Profile{ Name: data.Name, diff --git a/pkg/restapi/v1/verifier/controller_test.go b/pkg/restapi/v1/verifier/controller_test.go index 29b9207c1..cdda542f6 100644 --- a/pkg/restapi/v1/verifier/controller_test.go +++ b/pkg/restapi/v1/verifier/controller_test.go @@ -22,6 +22,10 @@ import ( verifiersvc "github.com/trustbloc/vcs/pkg/verifier" ) +const ( + userHeader = "X-User" +) + var ( //go:embed testdata/create_profile_data.json createProfileData []byte @@ -50,11 +54,11 @@ var ( } testProfile = &verifiersvc.Profile{ - ID: "id", + ID: "profileID", Name: "test profile", URL: "https://test-verifier.com", Active: true, - OrganizationID: "orgID", + OrganizationID: "org1", Checks: verificationChecks, OIDCConfig: map[string]interface{}{"config": "value"}, } @@ -84,7 +88,7 @@ func TestController_GetVerifierProfiles(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - req.Header.Set("Authorization", "Bearer org1") + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -96,6 +100,25 @@ func TestController_GetVerifierProfiles(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) }) + t.Run("missing authorization", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().GetAllProfiles("org1").Times(0) + + e := echo.New() + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + controller := verifier.NewController(mockProfileSvc) + + err := controller.GetVerifierProfiles(c) + require.Error(t, err) + require.Contains(t, err.Error(), "missing authorization") + }) + t.Run("error from profile service", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().GetAllProfiles("org1").Times(1).Return(nil, errors.New("get all profiles error")) @@ -104,7 +127,7 @@ func TestController_GetVerifierProfiles(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - req.Header.Set("Authorization", "Bearer org1") + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -113,7 +136,7 @@ func TestController_GetVerifierProfiles(t *testing.T) { err := controller.GetVerifierProfiles(c) require.Error(t, err) - require.Contains(t, err.Error(), "failed to get verifier profiles") + require.Contains(t, err.Error(), "get all profiles") }) } @@ -126,6 +149,7 @@ func TestController_PostVerifierProfiles(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(createProfileData)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -137,6 +161,45 @@ func TestController_PostVerifierProfiles(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) }) + t.Run("missing authorization", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().Create(gomock.Any()).Times(0) + + e := echo.New() + + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(createProfileData)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + controller := verifier.NewController(mockProfileSvc) + + err := controller.PostVerifierProfiles(c) + require.Error(t, err) + require.Contains(t, err.Error(), "missing authorization") + }) + + t.Run("invalid org id", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().Create(gomock.Any()).Times(0) + + e := echo.New() + + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(createProfileData)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "invalid") + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + controller := verifier.NewController(mockProfileSvc) + + err := controller.PostVerifierProfiles(c) + require.Error(t, err) + require.Contains(t, err.Error(), "org id mismatch") + }) + t.Run("error from profile service", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().Create(gomock.Any()).Times(1).Return(nil, errors.New("create profile error")) @@ -145,6 +208,7 @@ func TestController_PostVerifierProfiles(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(createProfileData)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -153,7 +217,7 @@ func TestController_PostVerifierProfiles(t *testing.T) { err := controller.PostVerifierProfiles(c) require.Error(t, err) - require.Contains(t, err.Error(), "failed to create verifier profile") + require.Contains(t, err.Error(), "create profile") }) } @@ -161,11 +225,13 @@ func TestController_DeleteVerifierProfilesProfileID(t *testing.T) { t.Run("200 OK", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().Delete("profileID").Times(1).Return(nil) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) e := echo.New() req := httptest.NewRequest(http.MethodDelete, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -177,14 +243,56 @@ func TestController_DeleteVerifierProfilesProfileID(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) }) + t.Run("missing authorization", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().Delete(gomock.Any()).Times(0) + + e := echo.New() + + req := httptest.NewRequest(http.MethodDelete, "/", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + controller := verifier.NewController(mockProfileSvc) + + err := controller.DeleteVerifierProfilesProfileID(c, "profileID") + require.Error(t, err) + require.Contains(t, err.Error(), "missing authorization") + }) + + t.Run("block access to profiles of other organizations", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().Delete(gomock.Any()).Times(0) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) + + e := echo.New() + + req := httptest.NewRequest(http.MethodDelete, "/", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org2") + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + controller := verifier.NewController(mockProfileSvc) + + err := controller.DeleteVerifierProfilesProfileID(c, "profileID") + require.Error(t, err) + require.Contains(t, err.Error(), "no profile with id profileID") + }) + t.Run("error from profile service", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().Delete("profileID").Times(1).Return(errors.New("delete profile error")) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) e := echo.New() req := httptest.NewRequest(http.MethodDelete, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -193,7 +301,7 @@ func TestController_DeleteVerifierProfilesProfileID(t *testing.T) { err := controller.DeleteVerifierProfilesProfileID(c, "profileID") require.Error(t, err) - require.Contains(t, err.Error(), "failed to delete verifier profile") + require.Contains(t, err.Error(), "delete profile") }) } @@ -206,6 +314,7 @@ func TestController_GetVerifierProfilesProfileID(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -225,6 +334,7 @@ func TestController_GetVerifierProfilesProfileID(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -233,7 +343,7 @@ func TestController_GetVerifierProfilesProfileID(t *testing.T) { err := controller.GetVerifierProfilesProfileID(c, "profileID") require.Error(t, err) - require.Contains(t, err.Error(), verifiersvc.ErrProfileNotFound.Error()) + require.Contains(t, err.Error(), "no profile with id profileID") }) t.Run("error from profile service", func(t *testing.T) { @@ -244,6 +354,7 @@ func TestController_GetVerifierProfilesProfileID(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -252,7 +363,7 @@ func TestController_GetVerifierProfilesProfileID(t *testing.T) { err := controller.GetVerifierProfilesProfileID(c, "profileID") require.Error(t, err) - require.Contains(t, err.Error(), "failed to get verifier profile") + require.Contains(t, err.Error(), "get profile") }) } @@ -260,11 +371,13 @@ func TestController_PutVerifierProfilesProfileID(t *testing.T) { t.Run("200 OK", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().Update(gomock.Any()).Times(1).Return(testProfile, nil) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) e := echo.New() req := httptest.NewRequest(http.MethodPut, "/", bytes.NewReader(updateProfileData)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -276,14 +389,35 @@ func TestController_PutVerifierProfilesProfileID(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) }) + t.Run("missing authorization", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().Update(gomock.Any()).Times(0) + + e := echo.New() + + req := httptest.NewRequest(http.MethodPut, "/", bytes.NewReader(updateProfileData)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + controller := verifier.NewController(mockProfileSvc) + + err := controller.PutVerifierProfilesProfileID(c, "profileID") + require.Error(t, err) + require.Contains(t, err.Error(), "missing authorization") + }) + t.Run("error from profile service", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().Update(gomock.Any()).Times(1).Return(nil, errors.New("update profile error")) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) e := echo.New() req := httptest.NewRequest(http.MethodPut, "/", bytes.NewReader(updateProfileData)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -292,7 +426,7 @@ func TestController_PutVerifierProfilesProfileID(t *testing.T) { err := controller.PutVerifierProfilesProfileID(c, "profileID") require.Error(t, err) - require.Contains(t, err.Error(), "failed to update verifier profile") + require.Contains(t, err.Error(), "update profile") }) } @@ -300,11 +434,13 @@ func TestController_PostVerifierProfilesProfileIDActivate(t *testing.T) { t.Run("200 OK", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().ActivateProfile("profileID").Times(1).Return(nil) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -316,14 +452,35 @@ func TestController_PostVerifierProfilesProfileIDActivate(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) }) + t.Run("missing authorization", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().ActivateProfile(gomock.Any()).Times(0) + + e := echo.New() + + req := httptest.NewRequest(http.MethodPost, "/", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + controller := verifier.NewController(mockProfileSvc) + + err := controller.PostVerifierProfilesProfileIDActivate(c, "profileID") + require.Error(t, err) + require.Contains(t, err.Error(), "missing authorization") + }) + t.Run("error from profile service", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().ActivateProfile("profileID").Times(1).Return(errors.New("activate profile error")) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -332,7 +489,7 @@ func TestController_PostVerifierProfilesProfileIDActivate(t *testing.T) { err := controller.PostVerifierProfilesProfileIDActivate(c, "profileID") require.Error(t, err) - require.Contains(t, err.Error(), "failed to activate verifier profile") + require.Contains(t, err.Error(), "activate profile") }) } @@ -340,11 +497,13 @@ func TestController_PostVerifierProfilesProfileIDDeactivate(t *testing.T) { t.Run("200 OK", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().DeactivateProfile("profileID").Times(1).Return(nil) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -356,14 +515,35 @@ func TestController_PostVerifierProfilesProfileIDDeactivate(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) }) + t.Run("missing authorization", func(t *testing.T) { + mockProfileSvc := NewMockProfileService(gomock.NewController(t)) + mockProfileSvc.EXPECT().DeactivateProfile(gomock.Any()).Times(0) + + e := echo.New() + + req := httptest.NewRequest(http.MethodPost, "/", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + controller := verifier.NewController(mockProfileSvc) + + err := controller.PostVerifierProfilesProfileIDDeactivate(c, "profileID") + require.Error(t, err) + require.Contains(t, err.Error(), "missing authorization") + }) + t.Run("error from profile service", func(t *testing.T) { mockProfileSvc := NewMockProfileService(gomock.NewController(t)) mockProfileSvc.EXPECT().DeactivateProfile("profileID").Times(1).Return(errors.New("deactivate profile error")) + mockProfileSvc.EXPECT().GetProfile("profileID").Times(1).Return(testProfile, nil) e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Set(userHeader, "org1") rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -372,6 +552,6 @@ func TestController_PostVerifierProfilesProfileIDDeactivate(t *testing.T) { err := controller.PostVerifierProfilesProfileIDDeactivate(c, "profileID") require.Error(t, err) - require.Contains(t, err.Error(), "failed to deactivate verifier profile") + require.Contains(t, err.Error(), "deactivate profile") }) } diff --git a/pkg/restapi/v1/verifier/testdata/create_profile_data.json b/pkg/restapi/v1/verifier/testdata/create_profile_data.json index c4caaad1d..adbcf7064 100644 --- a/pkg/restapi/v1/verifier/testdata/create_profile_data.json +++ b/pkg/restapi/v1/verifier/testdata/create_profile_data.json @@ -1,6 +1,6 @@ { "name": "test profile", - "organizationID": "orgID", + "organizationID": "org1", "url": "https://test-verifier.com", "checks": { "credential": { diff --git a/scripts/generate_test_keys.sh b/scripts/generate_test_keys.sh index c2a9ebfd5..8c91feaaa 100755 --- a/scripts/generate_test_keys.sh +++ b/scripts/generate_test_keys.sh @@ -22,6 +22,7 @@ subjectAltName = @alt_names DNS.1 = localhost DNS.2 = testnet.orb.local DNS.4 = file-server.trustbloc.local +DNS.5 = oidc-provider.example.com " >> "$tmp" #create CA diff --git a/test/bdd/features/issuer_v1_api.feature b/test/bdd/features/issuer_profile_v1_api.feature similarity index 82% rename from test/bdd/features/issuer_v1_api.feature rename to test/bdd/features/issuer_profile_v1_api.feature index 438f85fc7..55d3628d9 100644 --- a/test/bdd/features/issuer_v1_api.feature +++ b/test/bdd/features/issuer_profile_v1_api.feature @@ -5,15 +5,17 @@ # @all -@issuer_rest +@issuer_profile_rest Feature: Issuer VC REST API + Background: + Given "Charlie" has been authorized with client id "National Bank" and secret "bank-secret" to use vcs + @issuerProfileRecreationV1 Scenario: Delete and recreate issuer profile Given "Charlie" sends request to create an issuer profile with the organization "National Bank" And "Charlie" deletes the issuer profile Then "Charlie" can recreate the issuer profile with the organization "National Bank" - @issuerProfileUpdateV1 Scenario: Create and update issuer profile Given "Charlie" sends request to create an issuer profile with the organization "National Bank" @@ -23,4 +25,4 @@ Feature: Issuer VC REST API Scenario: Create and update issuer profile Given "Charlie" sends request to create an issuer profile with the organization "National Bank" And "Charlie" deactivates the issuer profile - And "Charlie" activates the issuer profile \ No newline at end of file + And "Charlie" activates the issuer profile diff --git a/test/bdd/features/verifier_profile_api.feature b/test/bdd/features/verifier_profile_v1_api.feature similarity index 93% rename from test/bdd/features/verifier_profile_api.feature rename to test/bdd/features/verifier_profile_v1_api.feature index b3608b132..92e271c79 100644 --- a/test/bdd/features/verifier_profile_api.feature +++ b/test/bdd/features/verifier_profile_v1_api.feature @@ -7,6 +7,9 @@ @all @verifier_profile_rest Feature: Verifier Profile Management REST API + Background: + Given organization "org1" has been authorized using client id "org1" and secret "org1-secret" + Scenario: Create a new verifier profile When organization "org1" creates a verifier profile with data from "verifier_profile_create.json" Then verifier profile is created diff --git a/test/bdd/fixtures/.env b/test/bdd/fixtures/.env index e30370b36..892544af8 100644 --- a/test/bdd/fixtures/.env +++ b/test/bdd/fixtures/.env @@ -44,3 +44,8 @@ MONGODB_PORT=27017 # sidetree SIDETREE_MOCK_IMAGE=ghcr.io/trustbloc-cicd/sidetree-mock SIDETREE_MOCK_IMAGE_TAG=0.7.0-snapshot-1a17931 + +# OAuth authorization +HYDRA_IMAGE_TAG=v1.10.7-alpine +OATHKEEPER_IMAGE_TAG=v0.38.19-alpine +MYSQL_IMAGE_TAG=8.0.30 diff --git a/test/bdd/fixtures/docker-compose.yml b/test/bdd/fixtures/docker-compose.yml index e1de8f191..c2f25aeb4 100644 --- a/test/bdd/fixtures/docker-compose.yml +++ b/test/bdd/fixtures/docker-compose.yml @@ -249,6 +249,71 @@ services: networks: - bdd_net + oathkeeper.trustbloc.local: + container_name: oathkeeper.trustbloc.local + image: oryd/oathkeeper:${OATHKEEPER_IMAGE_TAG} + ports: + - "4455:4455" + command: /bin/sh -c "cp /etc/tls/ec-cacert.pem /usr/local/share/ca-certificates/;update-ca-certificates;oathkeeper serve proxy --config /oathkeeper/config.yaml" + user: root + entrypoint: "" + environment: + - LOG_LEVEL=debug + - PORT=4455 + - ISSUER_URL=https://oathkeeper-proxy.trustbloc.local + - SERVE_PROXY_TLS_KEY_PATH=/etc/tls/ec-key.pem + - SERVE_PROXY_TLS_CERT_PATH=/etc/tls/ec-pubCert.pem + - LOG_LEAK_SENSITIVE_VALUES=true + volumes: + - ./oathkeeper-config:/oathkeeper + - ./keys/tls:/etc/tls + networks: + - bdd_net + + oidc-provider.example.com: + container_name: oidc-provider.example.com + image: oryd/hydra:${HYDRA_IMAGE_TAG} + ports: + - "4444:4444" + - "4445:4445" + command: /bin/sh -c "sleep 20 && hydra migrate sql --read-from-env --yes; (sleep 10; tmp/hydra_configure.sh)& hydra serve all" + entrypoint: "" + environment: + - DSN=mysql://thirdpartyoidc:thirdpartyoidc-secret-pw@tcp(mysql:3306)/thirdpartyoidc?max_conns=20&max_idle_conns=4 + - URLS_SELF_ISSUER=https://oidc-provider.example.com:4444/ + - SECRETS_SYSTEM=testSecretsSystem + - OIDC_SUBJECT_TYPES_SUPPORTED=public + - OIDC_SUBJECT_TYPE_PAIRWISE_SALT=testSecretsSystem + - SERVE_TLS_KEY_PATH=/etc/tls/ec-key.pem + - SERVE_TLS_CERT_PATH=/etc/tls/ec-pubCert.pem + - SERVE_PUBLIC_PORT=4444 + - SERVE_ADMIN_PORT=4445 + - LOG_LEAK_SENSITIVE_VALUES=true + restart: unless-stopped + volumes: + - ./keys/tls:/etc/tls + - ./hydra-config/hydra_configure.sh:/tmp/hydra_configure.sh + depends_on: + - mysql + networks: + - bdd_net + + mysql: + container_name: mysql + image: mysql:${MYSQL_IMAGE_TAG} + ports: + - "3306:3306" + restart: always + command: --default-authentication-plugin=mysql_native_password + environment: + MYSQL_ROOT_PASSWORD: secret + logging: + driver: "none" + volumes: + - ./mysql-config:/docker-entrypoint-initdb.d + networks: + - bdd_net + networks: bdd_net: driver: bridge diff --git a/test/bdd/fixtures/hydra-config/hydra_configure.sh b/test/bdd/fixtures/hydra-config/hydra_configure.sh new file mode 100755 index 000000000..ed71bd0f5 --- /dev/null +++ b/test/bdd/fixtures/hydra-config/hydra_configure.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# +# Copyright SecureKey Technologies Inc. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +echo "Creating OAuth clients..." + +hydra clients create \ + --endpoint https://oidc-provider.example.com:4445 \ + --id org1 \ + --secret org1-secret \ + --grant-types client_credentials \ + --response-types token,code \ + --scope org_admin \ + --skip-tls-verify + +hydra clients create \ + --endpoint https://oidc-provider.example.com:4445 \ + --id "National Bank" \ + --secret bank-secret \ + --grant-types client_credentials \ + --response-types token,code \ + --scope org_admin \ + --skip-tls-verify + +echo "Finished creating OAuth clients" diff --git a/test/bdd/fixtures/mysql-config/mysql_config.sql b/test/bdd/fixtures/mysql-config/mysql_config.sql new file mode 100644 index 000000000..ac2668ed1 --- /dev/null +++ b/test/bdd/fixtures/mysql-config/mysql_config.sql @@ -0,0 +1,14 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +\! echo "Configuring MySQL users..."; + +/* +oidc provider (hydra) +*/ +CREATE USER 'thirdpartyoidc'@'%' IDENTIFIED BY 'thirdpartyoidc-secret-pw'; +CREATE DATABASE thirdpartyoidc; +GRANT ALL PRIVILEGES ON thirdpartyoidc.* TO 'thirdpartyoidc'@'%'; diff --git a/test/bdd/fixtures/oathkeeper-config/config.yaml b/test/bdd/fixtures/oathkeeper-config/config.yaml new file mode 100644 index 000000000..af0cb9061 --- /dev/null +++ b/test/bdd/fixtures/oathkeeper-config/config.yaml @@ -0,0 +1,39 @@ +# +# Copyright SecureKey Technologies Inc. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +serve: + proxy: + port: 4455 + api: + port: 4458 + +access_rules: + repositories: + - file:///oathkeeper/rules/resource-server.json + matching_strategy: regexp + +authenticators: + oauth2_introspection: + enabled: true + config: + introspection_url: https://oidc-provider.example.com:4445/oauth2/introspect + scope_strategy: exact + noop: + enabled: true + +authorizers: + allow: + enabled: true + +mutators: + header: + enabled: true + config: + headers: + X-User: "{{ print .Subject }}" + X-API-Key: "rw_token" + noop: + enabled: true diff --git a/test/bdd/fixtures/oathkeeper-config/rules/resource-server.json b/test/bdd/fixtures/oathkeeper-config/rules/resource-server.json new file mode 100644 index 000000000..9b974dd1c --- /dev/null +++ b/test/bdd/fixtures/oathkeeper-config/rules/resource-server.json @@ -0,0 +1,41 @@ +[ + { + "id": "profile-management", + "upstream": { + "url": "http://vc-rest-echo.trustbloc.local:8075" + }, + "match": { + "url": "https://localhost:4455//profiles<.*>", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE" + ] + }, + "authenticators": [ + { + "handler": "oauth2_introspection", + "config": { + "required_scope": [ + "org_admin" + ] + } + } + ], + "mutators": [ + { + "handler": "header", + "config": { + "headers": { + "X-User": "{{ print .Subject }}", + "X-API-Key": "rw_token" + } + } + } + ], + "authorizer": { + "handler": "allow" + } + } +] diff --git a/test/bdd/go.mod b/test/bdd/go.mod index f8fe5dd27..c6bb3408d 100644 --- a/test/bdd/go.mod +++ b/test/bdd/go.mod @@ -19,6 +19,7 @@ require ( github.com/tidwall/gjson v1.9.3 github.com/trustbloc/edge-core v0.1.9-0.20220718150010-aa7941986372 github.com/trustbloc/vcs v0.0.0-00010101000000-000000000000 + golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 ) require ( @@ -94,10 +95,11 @@ require ( github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect - golang.org/x/net v0.0.0-20220513224357-95641704303c // indirect + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect google.golang.org/grpc v1.44.0 // indirect google.golang.org/protobuf v1.28.0 // indirect diff --git a/test/bdd/go.sum b/test/bdd/go.sum index a250afcec..e25f7bf36 100644 --- a/test/bdd/go.sum +++ b/test/bdd/go.sum @@ -1022,8 +1022,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0= -golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1040,6 +1040,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1248,6 +1250,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/test/bdd/pkg/bddutil/util.go b/test/bdd/pkg/bddutil/util.go index 813cc7578..a09612b51 100644 --- a/test/bdd/pkg/bddutil/util.go +++ b/test/bdd/pkg/bddutil/util.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0 package bddutil import ( + "context" "crypto/ed25519" "crypto/tls" _ "embed" //nolint:gci // required for go:embed @@ -28,6 +29,8 @@ import ( vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" ldstore "github.com/hyperledger/aries-framework-go/pkg/store/ld" "github.com/trustbloc/edge-core/pkg/log" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" ) var logger = log.New("bddutil") @@ -344,3 +347,30 @@ func DocumentLoader() (*ld.DocumentLoader, error) { return loader, nil } + +const oidcProviderURL = "https://localhost:4444" + +func IssueAccessToken(ctx context.Context, clientID, secret string, scopes []string) (string, error) { + conf := clientcredentials.Config{ + TokenURL: oidcProviderURL + "/oauth2/token", + ClientID: clientID, + ClientSecret: secret, + Scopes: scopes, + AuthStyle: oauth2.AuthStyleInHeader, + } + + ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + }) + + token, err := conf.Token(ctx) + if err != nil { + return "", fmt.Errorf("failed to get token: %w", err) + } + + return token.AccessToken, nil +} diff --git a/test/bdd/pkg/v1/issuer/issuer_steps.go b/test/bdd/pkg/v1/issuer/issuer_steps.go index 71d10b54e..25ea9ad71 100644 --- a/test/bdd/pkg/v1/issuer/issuer_steps.go +++ b/test/bdd/pkg/v1/issuer/issuer_steps.go @@ -8,20 +8,20 @@ package issuer import ( "bytes" + "context" + "crypto/tls" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/cucumber/godog" - "github.com/trustbloc/edge-core/pkg/log" - "github.com/trustbloc/vcs/test/bdd/pkg/bddutil" - "github.com/trustbloc/vcs/test/bdd/pkg/context" + bddcontext "github.com/trustbloc/vcs/test/bdd/pkg/context" ) const ( - issuerURL = "http://localhost:8075" + issuerURL = "https://localhost:4455" issuerProfileURL = issuerURL + "/issuer/profiles" issuerProfileURLFormat = issuerProfileURL + "/%s" ) @@ -30,34 +30,48 @@ func getProfileIDKey(user string) string { return user + "-profileID" } -func getProfileAuthToken(user string) string { - // temporary we use org id as token - return user + "-userOrg" +func getProfileAuthTokenKey(user string) string { + return user + "-accessToken" } -var logger = log.New("bdd-test") - // Steps is steps for VC BDD tests type Steps struct { - bddContext *context.BDDContext + bddContext *bddcontext.BDDContext + tlsConfig *tls.Config } // NewSteps returns new agent from client SDK -func NewSteps(ctx *context.BDDContext) *Steps { - return &Steps{bddContext: ctx} +func NewSteps(ctx *bddcontext.BDDContext) *Steps { + return &Steps{ + bddContext: ctx, + tlsConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } } // RegisterSteps registers agent steps func (e *Steps) RegisterSteps(s *godog.ScenarioContext) { + s.Step(`^"([^"]*)" has been authorized with client id "([^"]*)" and secret "([^"]*)" to use vcs$`, e.authorizeUser) s.Step(`^"([^"]*)" sends request to create an issuer profile with the organization "([^"]*)"$`, e.createIssuerProfile) s.Step(`^"([^"]*)" deactivates the issuer profile$`, e.deactivateIssuerProfile) s.Step(`^"([^"]*)" activates the issuer profile$`, e.activateIssuerProfile) s.Step(`^"([^"]*)" deletes the issuer profile$`, e.deleteIssuerProfile) s.Step(`^"([^"]*)" updates the issuer profile name to "([^"]*)"$`, e.updateIssuerProfileName) - s.Step(`^"([^"]*)" can recreate the issuer profile with the organization "([^"]*)"$`, e.createIssuerProfile) } +func (e *Steps) authorizeUser(user, clientID, secret string) error { + accessToken, err := bddutil.IssueAccessToken(context.Background(), clientID, secret, []string{"org_admin"}) + if err != nil { + return err + } + + e.bddContext.Args[getProfileAuthTokenKey(user)] = accessToken + + return nil +} + func (e *Steps) createIssuerProfile(user, organizationName string) error { //nolint: funlen profileRequest := createIssuerProfileData{ Name: "Test", @@ -74,23 +88,21 @@ func (e *Steps) createIssuerProfile(user, organizationName string) error { //nol }, } - e.bddContext.Args[getProfileAuthToken(user)] = organizationName - requestBytes, err := json.Marshal(profileRequest) if err != nil { return err } - resp, err := bddutil.HTTPDo(http.MethodPost, issuerProfileURL, "application/json", - e.bddContext.Args[getProfileAuthToken(user)], //nolint: bodyclose - bytes.NewBuffer(requestBytes)) + resp, err := bddutil.HTTPSDo(http.MethodPost, issuerProfileURL, "application/json", + e.bddContext.Args[getProfileAuthTokenKey(user)], //nolint: bodyclose + bytes.NewBuffer(requestBytes), e.tlsConfig) if err != nil { return err } defer bddutil.CloseResponseBody(resp.Body) - respBytes, err := ioutil.ReadAll(resp.Body) + respBytes, err := io.ReadAll(resp.Body) if err != nil { return err } @@ -113,7 +125,7 @@ func (e *Steps) createIssuerProfile(user, organizationName string) error { //nol func (e *Steps) updateIssuerProfileName(user, profileName string) error { id := e.bddContext.Args[getProfileIDKey(user)] - token := e.bddContext.Args[getProfileAuthToken(user)] + token := e.bddContext.Args[getProfileAuthTokenKey(user)] profileRequest := updateIssuerProfileData{ Name: profileName, @@ -124,15 +136,15 @@ func (e *Steps) updateIssuerProfileName(user, profileName string) error { return err } - resp, err := bddutil.HTTPDo(http.MethodDelete, fmt.Sprintf(issuerProfileURLFormat, //nolint: bodyclose - id), "", token, bytes.NewBuffer(requestBytes)) + resp, err := bddutil.HTTPSDo(http.MethodDelete, fmt.Sprintf(issuerProfileURLFormat, //nolint: bodyclose + id), "", token, bytes.NewBuffer(requestBytes), e.tlsConfig) if err != nil { return err } defer bddutil.CloseResponseBody(resp.Body) - respBytes, err := ioutil.ReadAll(resp.Body) + respBytes, err := io.ReadAll(resp.Body) if err != nil { return err } @@ -149,26 +161,26 @@ func (e *Steps) deleteIssuerProfile(user string) error { } func (e *Steps) activateIssuerProfile(user string) error { - return e.doSimpleProfileIDRequest(user, http.MethodPost, issuerProfileURLFormat + "/activate") + return e.doSimpleProfileIDRequest(user, http.MethodPost, issuerProfileURLFormat+"/activate") } func (e *Steps) deactivateIssuerProfile(user string) error { - return e.doSimpleProfileIDRequest(user, http.MethodPost, issuerProfileURLFormat + "/deactivate") + return e.doSimpleProfileIDRequest(user, http.MethodPost, issuerProfileURLFormat+"/deactivate") } func (e *Steps) doSimpleProfileIDRequest(user, httpMethod, urlFormat string) error { id := e.bddContext.Args[getProfileIDKey(user)] - token := e.bddContext.Args[getProfileAuthToken(user)] + token := e.bddContext.Args[getProfileAuthTokenKey(user)] - resp, err := bddutil.HTTPDo(httpMethod, fmt.Sprintf(urlFormat, //nolint: bodyclose - id), "", token, nil) + resp, err := bddutil.HTTPSDo(httpMethod, fmt.Sprintf(urlFormat, //nolint: bodyclose + id), "", token, nil, e.tlsConfig) if err != nil { return err } defer bddutil.CloseResponseBody(resp.Body) - respBytes, err := ioutil.ReadAll(resp.Body) + respBytes, err := io.ReadAll(resp.Body) if err != nil { return err } diff --git a/test/bdd/pkg/v1/verifier/verifer_steps.go b/test/bdd/pkg/v1/verifier/verifer_steps.go index 34e57373e..ceeaef635 100644 --- a/test/bdd/pkg/v1/verifier/verifer_steps.go +++ b/test/bdd/pkg/v1/verifier/verifer_steps.go @@ -9,27 +9,27 @@ package verifier import ( "bytes" "context" + "crypto/tls" _ "embed" "encoding/json" "errors" "fmt" "io" "net/http" - "net/http/httputil" - "os" "text/template" "github.com/cucumber/godog" "github.com/google/go-cmp/cmp" "github.com/hyperledger/aries-framework-go/pkg/common/log" + "github.com/trustbloc/vcs/test/bdd/pkg/bddutil" bddcontext "github.com/trustbloc/vcs/test/bdd/pkg/context" ) const ( contentType = "Content-Type" applicationJSON = "application/json" - host = "http://localhost:8075" + host = "https://localhost:4455" ) var ( @@ -53,19 +53,23 @@ type Steps struct { responseBody []byte profileID string testdata map[string][]byte + accessTokens map[string]string } // NewSteps returns new Steps context. func NewSteps(ctx *bddcontext.BDDContext) *Steps { - httpClient := http.DefaultClient - - if os.Getenv("HTTP_CLIENT_TRACE_ON") == "true" { - httpClient.Transport = &DumpTransport{r: http.DefaultTransport} + httpClient := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, } return &Steps{ - bddContext: ctx, - httpClient: httpClient, + bddContext: ctx, + httpClient: &httpClient, + accessTokens: make(map[string]string), testdata: map[string][]byte{ "verifier_profile_create.json": verifierProfileCreateJSON, "verifier_profile_created.json": verifierProfileCreatedJSON, @@ -77,6 +81,7 @@ func NewSteps(ctx *bddcontext.BDDContext) *Steps { // RegisterSteps registers scenario steps. func (s *Steps) RegisterSteps(sc *godog.ScenarioContext) { + sc.Step(`^organization "([^"]*)" has been authorized using client id "([^"]*)" and secret "([^"]*)"$`, s.authorize) sc.Step(`^organization "([^"]*)" creates a verifier profile with data from "([^"]*)"$`, s.createProfile) sc.Step(`^organization "([^"]*)" has a verifier profile with data from "([^"]*)"$`, s.createProfile) sc.Step(`^organization "([^"]*)" gets a verifier profile by ID$`, s.getProfileByID) @@ -93,11 +98,23 @@ func (s *Steps) RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^verifier profile matches "([^"]*)"$`, s.checkProfileMatches) } +func (s *Steps) authorize(ctx context.Context, org, clientID, secret string) error { + accessToken, err := bddutil.IssueAccessToken(ctx, clientID, secret, []string{"org_admin"}) + if err != nil { + return fmt.Errorf("failed to get access token: %w", err) + } + + s.accessTokens[org] = accessToken + + return nil +} + func (s *Steps) createProfile(ctx context.Context, orgID, content string) error { var profile verifierProfile if err := s.httpDo(ctx, http.MethodPost, host+"/verifier/profiles", withBody(bytes.NewReader(s.testdata[content])), + withBearerToken(s.accessTokens[orgID]), withParsedResponse(&profile), ); err != nil { return fmt.Errorf("create verifier profile: %w", err) @@ -112,6 +129,7 @@ func (s *Steps) getProfileByID(ctx context.Context, orgID string) error { var profile verifierProfile if err := s.httpDo(ctx, http.MethodGet, host+"/verifier/profiles/"+s.profileID, + withBearerToken(s.accessTokens[orgID]), withParsedResponse(&profile), ); err != nil { return fmt.Errorf("get verifier profile: %w", err) @@ -127,6 +145,7 @@ func (s *Steps) updateProfile(ctx context.Context, orgID, content string) error if err := s.httpDo(ctx, http.MethodPut, host+"/verifier/profiles/"+s.profileID, withBody(bytes.NewReader(s.testdata[content])), + withBearerToken(s.accessTokens[orgID]), withParsedResponse(&profile), ); err != nil { return fmt.Errorf("update verifier profile: %w", err) @@ -138,7 +157,8 @@ func (s *Steps) updateProfile(ctx context.Context, orgID, content string) error } func (s *Steps) deleteProfile(ctx context.Context, orgID string) error { - if err := s.httpDo(ctx, http.MethodDelete, host+"/verifier/profiles/"+s.profileID); err != nil { + if err := s.httpDo(ctx, http.MethodDelete, host+"/verifier/profiles/"+s.profileID, + withBearerToken(s.accessTokens[orgID])); err != nil { return fmt.Errorf("delete verifier profile: %w", err) } @@ -146,7 +166,8 @@ func (s *Steps) deleteProfile(ctx context.Context, orgID string) error { } func (s *Steps) activateProfile(ctx context.Context, orgID string) error { - if err := s.httpDo(ctx, http.MethodPost, host+"/verifier/profiles/"+s.profileID+"/activate"); err != nil { + if err := s.httpDo(ctx, http.MethodPost, host+"/verifier/profiles/"+s.profileID+"/activate", + withBearerToken(s.accessTokens[orgID])); err != nil { return fmt.Errorf("activate verifier profile: %w", err) } @@ -154,7 +175,8 @@ func (s *Steps) activateProfile(ctx context.Context, orgID string) error { } func (s *Steps) deactivateProfile(ctx context.Context, orgID string) error { - if err := s.httpDo(ctx, http.MethodPost, host+"/verifier/profiles/"+s.profileID+"/deactivate"); err != nil { + if err := s.httpDo(ctx, http.MethodPost, host+"/verifier/profiles/"+s.profileID+"/deactivate", + withBearerToken(s.accessTokens[orgID])); err != nil { return fmt.Errorf("deactivate verifier profile: %w", err) } @@ -178,7 +200,8 @@ func (s *Steps) checkProfileDeleted(ctx context.Context) error { return err } - if err := s.httpDo(ctx, http.MethodGet, host+"/verifier/profiles/"+s.profileID); err != nil { + if err := s.httpDo(ctx, http.MethodGet, host+"/verifier/profiles/"+s.profileID, + withBearerToken(s.accessTokens["org1"])); err != nil { if err.Error() != "404 Not Found" { return err } @@ -195,6 +218,7 @@ func (s *Steps) checkProfileActivated(ctx context.Context) error { var profile verifierProfile if err := s.httpDo(ctx, http.MethodGet, host+"/verifier/profiles/"+s.profileID, + withBearerToken(s.accessTokens["org1"]), withParsedResponse(&profile), ); err != nil { return err @@ -215,6 +239,7 @@ func (s *Steps) checkProfileDeactivated(ctx context.Context) error { var profile verifierProfile if err := s.httpDo(ctx, http.MethodGet, host+"/verifier/profiles/"+s.profileID, + withBearerToken(s.accessTokens["org1"]), withParsedResponse(&profile), ); err != nil { return err @@ -267,6 +292,7 @@ func (s *Steps) checkProfileMatches(content string) error { type options struct { body io.Reader + bearerToken string parsedResponse interface{} } @@ -278,6 +304,12 @@ func withBody(body io.Reader) opt { } } +func withBearerToken(token string) opt { + return func(o *options) { + o.bearerToken = token + } +} + func withParsedResponse(v interface{}) opt { return func(o *options) { o.parsedResponse = v @@ -300,6 +332,10 @@ func (s *Steps) httpDo(ctx context.Context, method, url string, opts ...opt) err req.Header.Add(contentType, applicationJSON) + if op.bearerToken != "" { + req.Header.Add("Authorization", "Bearer "+op.bearerToken) + } + resp, err := s.httpClient.Do(req) if err != nil { return fmt.Errorf("http do: %w", err) @@ -353,32 +389,3 @@ func parseError(status string, body []byte) error { func (s *Steps) ProfileID() string { return s.profileID } - -// DumpTransport is http.RoundTripper that dumps requests and responses. -type DumpTransport struct { - r http.RoundTripper -} - -// RoundTrip implements the RoundTripper interface. -func (d *DumpTransport) RoundTrip(req *http.Request) (*http.Response, error) { - reqDump, err := httputil.DumpRequest(req, true) - if err != nil { - return nil, fmt.Errorf("failed to dump request: %w", err) - } - - fmt.Printf("\n****REQUEST****\n%s\n\n", string(reqDump)) - - resp, err := d.r.RoundTrip(req) - if err != nil { - return nil, err - } - - respDump, err := httputil.DumpResponse(resp, true) - if err != nil { - return nil, fmt.Errorf("failed to dump response: %w", err) - } - - fmt.Printf("****RESPONSE****\n%s****************\n", string(respDump)) - - return resp, nil -}