From 297d45be0956a874f641ef7c317bbca316da896c Mon Sep 17 00:00:00 2001 From: Ivan Kapelyukhin Date: Tue, 25 Aug 2020 23:58:18 +0200 Subject: [PATCH 01/10] Add a special type and parsing for the new config option --- .../repository/interfaces/config/config.go | 40 ++++++++++++++++++- .../repository/interfaces/structs.go | 2 + 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/jetstream/repository/interfaces/config/config.go b/src/jetstream/repository/interfaces/config/config.go index 872e3470d4..0e4dbe75ad 100644 --- a/src/jetstream/repository/interfaces/config/config.go +++ b/src/jetstream/repository/interfaces/config/config.go @@ -20,6 +20,39 @@ import ( const secretsDir = "/etc/secrets" +// APIKeysConfigValue - special type for configuring whether API keys feature is enabled +type APIKeysConfigValue string + +// APIKeysConfigEnum - defines possible configuration values for Stratos API keys feature +var APIKeysConfigEnum = struct { + Disabled APIKeysConfigValue + AdminOnly APIKeysConfigValue + AllUsers APIKeysConfigValue +}{ + Disabled: "disabled", + AdminOnly: "admin_only", + AllUsers: "all_users", +} + +// verifies that given string is a valid config value (i.e., present in APIKeysConfigEnum) +func parseAPIKeysConfigValue(input string) (APIKeysConfigValue, error) { + t := reflect.TypeOf(APIKeysConfigEnum) + v := reflect.ValueOf(APIKeysConfigEnum) + + var allowedValues []string + + for i := 0; i < t.NumField(); i++ { + allowedValue := string(v.Field(i).Interface().(APIKeysConfigValue)) + if allowedValue == input { + return APIKeysConfigValue(input), nil + } + + allowedValues = append(allowedValues, allowedValue) + } + + return "", fmt.Errorf("Invalid value %q, allowed values: %q", input, allowedValues) +} + var urlType *url.URL // Load the given pointer to struct with values from the environment and the @@ -119,7 +152,12 @@ func SetStructFieldValue(value reflect.Value, field reflect.Value, val string) e b, err = strconv.ParseBool(val) newVal = b case reflect.String: - newVal = val + apiKeysConfigType := reflect.TypeOf((*APIKeysConfigValue)(nil)).Elem() + if typ == apiKeysConfigType { + newVal, err = parseAPIKeysConfigValue(val) + } else { + newVal = val + } default: if typ == reflect.TypeOf(urlType) { newVal, err = url.Parse(val) diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 909c91048b..591b0b7d24 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -4,6 +4,7 @@ import ( "net/http" "net/url" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config" "github.com/gorilla/sessions" "github.com/labstack/echo" ) @@ -369,6 +370,7 @@ type PortalConfig struct { DatabaseProviderName string EnableTechPreview bool `configName:"ENABLE_TECH_PREVIEW"` CanMigrateDatabaseSchema bool + APIKeysEnabled config.APIKeysConfigValue `configName:"API_KEYS_ENABLED"` // CanMigrateDatabaseSchema indicates if we can safely perform migrations // This depends on the deployment mechanism and the database config // e.g. if running in Cloud Foundry with a shared DB, then only the 0-index application instance From 21add38f08861c8e6bff7cbdfb833855cd926f59 Mon Sep 17 00:00:00 2001 From: Ivan Kapelyukhin Date: Thu, 27 Aug 2020 10:37:14 +0200 Subject: [PATCH 02/10] Add APIKeysEnabled to /info, check it in the middleware --- src/jetstream/info.go | 1 + src/jetstream/main.go | 6 ++++++ src/jetstream/middleware.go | 21 +++++++++++++++++++ .../repository/interfaces/structs.go | 7 ++++--- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/jetstream/info.go b/src/jetstream/info.go index 2000460cc5..0c4cef7d57 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -59,6 +59,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { s.Configuration.TechPreview = p.Config.EnableTechPreview s.Configuration.ListMaxSize = p.Config.UIListMaxSize s.Configuration.ListAllowLoadMaxed = p.Config.UIListAllowLoadMaxed + s.Configuration.APIKeysEnabled = string(p.Config.APIKeysEnabled) // Only add diagnostics information if the user is an admin if uaaUser.Admin { diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 1c26e2069d..4deab1d10b 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -257,6 +257,12 @@ func main() { }() log.Info("Session data store initialized.") + // Setting default value for APIKeysEnabled + if portalConfig.APIKeysEnabled == "" { + log.Debug(`APIKeysEnabled not set, setting to "admin_only"`) + portalConfig.APIKeysEnabled = config.APIKeysConfigEnum.AdminOnly + } + // Setup the global interface for the proxy portalProxy := newPortalProxy(portalConfig, databaseConnectionPool, sessionStore, sessionStoreOptions, envLookup) portalProxy.SessionDataStore = sessionDataStore diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index 635afc2260..446677f225 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -16,6 +16,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config" ) const cfSessionCookieName = "JSESSIONID" @@ -323,6 +324,12 @@ func (p *portalProxy) apiKeyMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { log.Debug("apiKeyMiddleware") + // skipping thise middleware if API keys are disabled + if p.Config.APIKeysEnabled == config.APIKeysConfigEnum.Disabled { + log.Debugf("apiKeyMiddleware: API keys are disabled, skipping") + return h(c) + } + apiKeySecret, err := getAPIKeyFromHeader(c) if err != nil { log.Debugf("apiKeyMiddleware: %v", err) @@ -341,6 +348,20 @@ func (p *portalProxy) apiKeyMiddleware(h echo.HandlerFunc) echo.HandlerFunc { return h(c) } + // checking if user is an admin if API keys are enabled for admins only + if p.Config.APIKeysEnabled == config.APIKeysConfigEnum.AdminOnly { + user, err := p.StratosAuthService.GetUser(apiKey.UserGUID) + if err != nil { + log.Errorf("apiKeyMiddleware: %v", err) + return h(c) + } + + if !user.Admin { + log.Debugf("apiKeyMiddleware: user isn't admin, skipping") + return h(c) + } + } + c.Set(APIKeySkipperContextKey, true) c.Set("user_id", apiKey.UserGUID) diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 591b0b7d24..20512c58a3 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -215,9 +215,10 @@ type Info struct { PluginConfig map[string]string `json:"plugin-config,omitempty"` Diagnostics *Diagnostics `json:"diagnostics,omitempty"` Configuration struct { - TechPreview bool `json:"enableTechPreview"` - ListMaxSize int64 `json:"listMaxSize,omitempty"` - ListAllowLoadMaxed bool `json:"listAllowLoadMaxed,omitempty"` + TechPreview bool `json:"enableTechPreview"` + ListMaxSize int64 `json:"listMaxSize,omitempty"` + ListAllowLoadMaxed bool `json:"listAllowLoadMaxed,omitempty"` + APIKeysEnabled string `json:"APIKeysEnabled"` } `json:"config"` } From 6a2b69d6646eb7cbc69b97b5f53cd11b3aea7918 Mon Sep 17 00:00:00 2001 From: Ivan Kapelyukhin Date: Thu, 27 Aug 2020 20:50:50 +0200 Subject: [PATCH 03/10] Add test coverage for apiKeyMiddleware --- src/jetstream/middleware_test.go | 369 ++++++++++++++++++ .../repository/mock_interfaces/mock_auth.go | 107 +++++ 2 files changed, 476 insertions(+) create mode 100644 src/jetstream/middleware_test.go create mode 100644 src/jetstream/repository/mock_interfaces/mock_auth.go diff --git a/src/jetstream/middleware_test.go b/src/jetstream/middleware_test.go new file mode 100644 index 0000000000..c7da9d9e1a --- /dev/null +++ b/src/jetstream/middleware_test.go @@ -0,0 +1,369 @@ +package main + +import ( + "database/sql" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/apikeys" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/mock_interfaces" + "github.com/golang/mock/gomock" + "github.com/labstack/echo" + log "github.com/sirupsen/logrus" + . "github.com/smartystreets/goconvey/convey" + sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" +) + +func makeMockServer(apiKeysRepo apikeys.Repository, mockStratosAuth interfaces.StratosAuth) *portalProxy { + db, _, dberr := sqlmock.New() + if dberr != nil { + log.Panicf("an error '%s' was not expected when opening a stub database connection", dberr) + } + + pp := setupPortalProxy(db) + pp.DatabaseConnectionPool = db + pp.APIKeysRepository = apiKeysRepo + pp.StratosAuthService = mockStratosAuth + + return pp +} + +func makeNewRequest() (echo.Context, *httptest.ResponseRecorder) { + req := setupMockReq("GET", "", map[string]string{}) + rec := httptest.NewRecorder() + e := echo.New() + ctx := e.NewContext(req, rec) + + return ctx, rec +} + +func Test_apiKeyMiddleware(t *testing.T) { + t.Parallel() + + // disabling logging noise + log.SetLevel(log.PanicLevel) + + ctrl := gomock.NewController(t) + mockAPIRepo := apikeys.NewMockRepository(ctrl) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + pp := makeMockServer(mockAPIRepo, mockStratosAuth) + defer ctrl.Finish() + defer pp.DatabaseConnectionPool.Close() + + handlerFunc := func(c echo.Context) error { + return c.String(http.StatusOK, "test") + } + + middleware := pp.apiKeyMiddleware(handlerFunc) + apiKeySecret := "SecretMcSecretface" + + Convey("when API keys are enabled for all users", t, func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.AllUsers + + Convey("when headers are present", func() { + Convey("when schema other than Bearer is used", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "JWT "+apiKeySecret) + + err := middleware(ctx) + + Convey("should not set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldBeNil) + }) + + Convey("should not set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeNil) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + + Convey("when Bearer schema is used", func() { + Convey("when no matching key is found", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "Bearer "+apiKeySecret) + + mockAPIRepo. + EXPECT(). + GetAPIKeyBySecret(gomock.Eq(apiKeySecret)). + Return(nil, sql.ErrNoRows) + + err := middleware(ctx) + + Convey("should not set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldBeNil) + }) + + Convey("should not set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeNil) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + + Convey("when APIKeysRepository returns an error", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "Bearer "+apiKeySecret) + + mockAPIRepo. + EXPECT(). + GetAPIKeyBySecret(gomock.Eq(apiKeySecret)). + Return(nil, errors.New("Something went wrong")) + + err := middleware(ctx) + + Convey("should not set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldBeNil) + }) + + Convey("should not set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeNil) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + + Convey("when a matching key is found", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "Bearer "+apiKeySecret) + + apiKey := &interfaces.APIKey{ + UserGUID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + GUID: "00000000-0000-0000-0000-000000000000", + } + + mockAPIRepo. + EXPECT(). + GetAPIKeyBySecret(gomock.Eq(apiKeySecret)). + Return(apiKey, nil) + + mockAPIRepo. + EXPECT(). + UpdateAPIKeyLastUsed(gomock.Eq(apiKey.GUID)). + Return(nil) + + err := middleware(ctx) + + Convey("should set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldEqual, apiKey.UserGUID) + }) + + Convey("should set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeTrue) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + + Convey("when a matching key is found, but last_used can't be updated", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "Bearer "+apiKeySecret) + + apiKey := &interfaces.APIKey{ + UserGUID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + GUID: "00000000-0000-0000-0000-000000000000", + } + + mockAPIRepo. + EXPECT(). + GetAPIKeyBySecret(gomock.Eq(apiKeySecret)). + Return(apiKey, nil) + + mockAPIRepo. + EXPECT(). + UpdateAPIKeyLastUsed(gomock.Eq(apiKey.GUID)). + Return(errors.New("Something went wrong")) + + err := middleware(ctx) + + Convey("should set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldEqual, apiKey.UserGUID) + }) + + Convey("should set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeTrue) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + + }) + }) + }) + + Convey("when API keys are enabled for all admins only", t, func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.AdminOnly + + Convey("when a matching key belongs a non-admin user", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "Bearer "+apiKeySecret) + + apiKey := &interfaces.APIKey{ + UserGUID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + GUID: "00000000-0000-0000-0000-000000000000", + } + + connectedUser := &interfaces.ConnectedUser{ + GUID: apiKey.UserGUID, + Admin: false, + } + + mockAPIRepo. + EXPECT(). + GetAPIKeyBySecret(gomock.Eq(apiKeySecret)). + Return(apiKey, nil) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(apiKey.UserGUID)). + Return(connectedUser, nil) + + err := middleware(ctx) + + Convey("should not set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldBeNil) + }) + + Convey("should not set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeNil) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + + Convey("when a matching key belongs an admin user", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "Bearer "+apiKeySecret) + + apiKey := &interfaces.APIKey{ + UserGUID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + GUID: "00000000-0000-0000-0000-000000000000", + } + + connectedUser := &interfaces.ConnectedUser{ + GUID: apiKey.UserGUID, + Admin: true, + } + + mockAPIRepo. + EXPECT(). + GetAPIKeyBySecret(gomock.Eq(apiKeySecret)). + Return(apiKey, nil) + + mockAPIRepo. + EXPECT(). + UpdateAPIKeyLastUsed(gomock.Eq(apiKey.GUID)). + Return(nil) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(apiKey.UserGUID)). + Return(connectedUser, nil) + + err := middleware(ctx) + + Convey("should set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldEqual, apiKey.UserGUID) + }) + + Convey("should set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeTrue) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + + Convey("when StratosAuth.GetUser returns an error", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "Bearer "+apiKeySecret) + + apiKey := &interfaces.APIKey{ + UserGUID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + GUID: "00000000-0000-0000-0000-000000000000", + } + + mockAPIRepo. + EXPECT(). + GetAPIKeyBySecret(gomock.Eq(apiKeySecret)). + Return(apiKey, nil) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(apiKey.UserGUID)). + Return(nil, errors.New("Something went wrong")) + + err := middleware(ctx) + + Convey("should not set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldBeNil) + }) + + Convey("should not set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeNil) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + }) + + Convey("when API keys are disabled", t, func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.Disabled + + Convey("when an API key header is supplied", func() { + ctx, rec := makeNewRequest() + ctx.Request().Header.Add("Authentication", "Bearer "+apiKeySecret) + + err := middleware(ctx) + + Convey("should not set user_id in the context", func() { + So(ctx.Get("user_id"), ShouldBeNil) + }) + + Convey("should not set skip flag in the context", func() { + So(ctx.Get(APIKeySkipperContextKey), ShouldBeNil) + }) + + Convey("request should be successful", func() { + So(err, ShouldBeNil) + So(rec.Code, ShouldEqual, 200) + So(rec.Body.String(), ShouldEqual, "test") + }) + }) + }) +} diff --git a/src/jetstream/repository/mock_interfaces/mock_auth.go b/src/jetstream/repository/mock_interfaces/mock_auth.go new file mode 100644 index 0000000000..801847baa9 --- /dev/null +++ b/src/jetstream/repository/mock_interfaces/mock_auth.go @@ -0,0 +1,107 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repository/interfaces/auth.go + +// Package mock_interfaces is a generated GoMock package. +package mock_interfaces + +import ( + interfaces "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + gomock "github.com/golang/mock/gomock" + echo "github.com/labstack/echo" + reflect "reflect" +) + +// MockStratosAuth is a mock of StratosAuth interface +type MockStratosAuth struct { + ctrl *gomock.Controller + recorder *MockStratosAuthMockRecorder +} + +// MockStratosAuthMockRecorder is the mock recorder for MockStratosAuth +type MockStratosAuthMockRecorder struct { + mock *MockStratosAuth +} + +// NewMockStratosAuth creates a new mock instance +func NewMockStratosAuth(ctrl *gomock.Controller) *MockStratosAuth { + mock := &MockStratosAuth{ctrl: ctrl} + mock.recorder = &MockStratosAuthMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockStratosAuth) EXPECT() *MockStratosAuthMockRecorder { + return m.recorder +} + +// Login mocks base method +func (m *MockStratosAuth) Login(c echo.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Login", c) + ret0, _ := ret[0].(error) + return ret0 +} + +// Login indicates an expected call of Login +func (mr *MockStratosAuthMockRecorder) Login(c interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockStratosAuth)(nil).Login), c) +} + +// Logout mocks base method +func (m *MockStratosAuth) Logout(c echo.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Logout", c) + ret0, _ := ret[0].(error) + return ret0 +} + +// Logout indicates an expected call of Logout +func (mr *MockStratosAuthMockRecorder) Logout(c interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockStratosAuth)(nil).Logout), c) +} + +// GetUsername mocks base method +func (m *MockStratosAuth) GetUsername(userGUID string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsername", userGUID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUsername indicates an expected call of GetUsername +func (mr *MockStratosAuthMockRecorder) GetUsername(userGUID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsername", reflect.TypeOf((*MockStratosAuth)(nil).GetUsername), userGUID) +} + +// GetUser mocks base method +func (m *MockStratosAuth) GetUser(userGUID string) (*interfaces.ConnectedUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUser", userGUID) + ret0, _ := ret[0].(*interfaces.ConnectedUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUser indicates an expected call of GetUser +func (mr *MockStratosAuthMockRecorder) GetUser(userGUID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockStratosAuth)(nil).GetUser), userGUID) +} + +// VerifySession mocks base method +func (m *MockStratosAuth) VerifySession(c echo.Context, sessionUser string, sessionExpireTime int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VerifySession", c, sessionUser, sessionExpireTime) + ret0, _ := ret[0].(error) + return ret0 +} + +// VerifySession indicates an expected call of VerifySession +func (mr *MockStratosAuthMockRecorder) VerifySession(c, sessionUser, sessionExpireTime interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifySession", reflect.TypeOf((*MockStratosAuth)(nil).VerifySession), c, sessionUser, sessionExpireTime) +} From 182ee52590a297a762b1182b84c502618056ef0e Mon Sep 17 00:00:00 2001 From: Ivan Kapelyukhin Date: Fri, 28 Aug 2020 01:50:37 +0200 Subject: [PATCH 04/10] Block API keys endpoints if disabled in the config; add tests --- src/jetstream/apikeys.go | 32 +++ src/jetstream/apikeys_test.go | 440 +++++++++++++++++++------------ src/jetstream/middleware_test.go | 9 + 3 files changed, 311 insertions(+), 170 deletions(-) diff --git a/src/jetstream/apikeys.go b/src/jetstream/apikeys.go index 3985f6fda7..c9ab110113 100644 --- a/src/jetstream/apikeys.go +++ b/src/jetstream/apikeys.go @@ -4,10 +4,30 @@ import ( "errors" "net/http" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config" "github.com/labstack/echo" log "github.com/sirupsen/logrus" ) +func (p *portalProxy) checkIfAPIKeysEnabled(userGUID string) error { + if p.Config.APIKeysEnabled == config.APIKeysConfigEnum.Disabled { + log.Info("API keys are disabled") + return errors.New("API keys are disabled") + } else if p.Config.APIKeysEnabled == config.APIKeysConfigEnum.AdminOnly { + user, err := p.StratosAuthService.GetUser(userGUID) + if err != nil { + return err + } + + if !user.Admin { + log.Info("API keys are disabled for non-admin users") + return errors.New("API keys are disabled for non-admin users") + } + } + + return nil +} + func (p *portalProxy) addAPIKey(c echo.Context) error { log.Debug("addAPIKey") @@ -18,6 +38,10 @@ func (p *portalProxy) addAPIKey(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Comment can't be empty") } + if err := p.checkIfAPIKeysEnabled(userGUID); err != nil { + return echo.NewHTTPError(http.StatusForbidden, err.Error()) + } + apiKey, err := p.APIKeysRepository.AddAPIKey(userGUID, comment) if err != nil { log.Errorf("Error adding API key: %v", err) @@ -32,6 +56,10 @@ func (p *portalProxy) listAPIKeys(c echo.Context) error { userGUID := c.Get("user_id").(string) + if err := p.checkIfAPIKeysEnabled(userGUID); err != nil { + return echo.NewHTTPError(http.StatusForbidden, err.Error()) + } + apiKeys, err := p.APIKeysRepository.ListAPIKeys(userGUID) if err != nil { log.Errorf("Error listing API keys: %v", err) @@ -51,6 +79,10 @@ func (p *portalProxy) deleteAPIKey(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "API key guid can't be empty") } + if err := p.checkIfAPIKeysEnabled(userGUID); err != nil { + return echo.NewHTTPError(http.StatusForbidden, err.Error()) + } + if err := p.APIKeysRepository.DeleteAPIKey(userGUID, keyGUID); err != nil { log.Errorf("Error deleting API key: %v", err) return errors.New("Error deleting API key") diff --git a/src/jetstream/apikeys_test.go b/src/jetstream/apikeys_test.go index 6fb072f1cc..3e7e952f4d 100644 --- a/src/jetstream/apikeys_test.go +++ b/src/jetstream/apikeys_test.go @@ -8,6 +8,8 @@ import ( "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/apikeys" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/mock_interfaces" "github.com/golang/mock/gomock" "github.com/labstack/echo" log "github.com/sirupsen/logrus" @@ -17,188 +19,291 @@ import ( func Test_addAPIKey(t *testing.T) { t.Parallel() - Convey("Given a request to add an API key", t, func() { - log.SetLevel(log.WarnLevel) + // disabling logging noise + log.SetLevel(log.PanicLevel) + Convey("Given a request to add an API key", t, func() { userID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - Convey("when comment is not specified", func() { - req := setupMockReq("POST", "", map[string]string{}) + ctrl := gomock.NewController(t) + mockAPIRepo := apikeys.NewMockRepository(ctrl) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + pp := makeMockServer(mockAPIRepo, mockStratosAuth) + defer ctrl.Finish() + defer pp.DatabaseConnectionPool.Close() - _, _, ctx, pp, db, _ := setupHTTPTest(req) - defer db.Close() + Convey("when API keys are disabled", func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.Disabled - ctx.Set("user_id", userID) + Convey("when API key comment is present", func() { + comment := "Test API key" + ctx, _ := makeNewRequestWithParams("POST", map[string]string{"comment": comment}) + ctx.Set("user_id", userID) - err := pp.addAPIKey(ctx) + err := pp.addAPIKey(ctx) - Convey("should return an error", func() { - So(err, ShouldResemble, echo.NewHTTPError(http.StatusBadRequest, "Comment can't be empty")) + Convey("should return an error", func() { + So(err, ShouldResemble, echo.NewHTTPError(http.StatusForbidden, "API keys are disabled")) + }) }) }) - Convey("when a DB error occurs", func() { - comment := "Test API key" + Convey("when API keys are enabled for admin users", func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.AdminOnly - ctrl := gomock.NewController(t) - defer ctrl.Finish() + Convey("when a StratosAuth error occurs", func() { + comment := "Test API key" + ctx, _ := makeNewRequestWithParams("POST", map[string]string{"comment": comment}) + ctx.Set("user_id", userID) - m := apikeys.NewMockRepository(ctrl) - m. - EXPECT(). - AddAPIKey(gomock.Eq(userID), gomock.Eq(comment)). - Return(nil, errors.New("Something went wrong")) + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(userID)). + Return(nil, errors.New("Something went wrong")) - req := setupMockReq("POST", "", map[string]string{ - "comment": comment, + err := pp.addAPIKey(ctx) + + Convey("should return an error", func() { + So(err, ShouldResemble, echo.NewHTTPError(http.StatusForbidden, "Something went wrong")) + }) }) - _, _, ctx, pp, db, _ := setupHTTPTest(req) - defer db.Close() + Convey("when user is not an admin", func() { + Convey("when API key comment is present", func() { + comment := "Test API key" + ctx, _ := makeNewRequestWithParams("POST", map[string]string{"comment": comment}) + ctx.Set("user_id", userID) - pp.APIKeysRepository = m + connectedUser := &interfaces.ConnectedUser{ + GUID: userID, + Admin: false, + } - ctx.Set("user_id", userID) + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(userID)). + Return(connectedUser, nil) - err := pp.addAPIKey(ctx) + err := pp.addAPIKey(ctx) - Convey("should return an error", func() { - So(err, ShouldResemble, errors.New("Error adding API key")) + Convey("should return an error", func() { + So(err, ShouldResemble, echo.NewHTTPError(http.StatusForbidden, "API keys are disabled for non-admin users")) + }) + }) }) - }) - Convey("when API key comment was added successfully", func() { - comment := "Test API key" - retval := interfaces.APIKey{UserGUID: userID, Comment: comment} + Convey("when user is an admin", func() { + Convey("when API key comment is present", func() { + comment := "Test API key" + retval := interfaces.APIKey{UserGUID: userID, Comment: comment} - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctx, rec := makeNewRequestWithParams("POST", map[string]string{"comment": comment}) + ctx.Set("user_id", userID) - m := apikeys.NewMockRepository(ctrl) - m. - EXPECT(). - AddAPIKey(gomock.Eq(userID), gomock.Eq(comment)). - Return(&retval, nil) + connectedUser := &interfaces.ConnectedUser{ + GUID: userID, + Admin: true, + } - req := setupMockReq("POST", "", map[string]string{ - "comment": comment, - }) + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(userID)). + Return(connectedUser, nil) - rec, _, ctx, pp, db, _ := setupHTTPTest(req) - defer db.Close() + mockAPIRepo. + EXPECT(). + AddAPIKey(gomock.Eq(userID), gomock.Eq(comment)). + Return(&retval, nil) - pp.APIKeysRepository = m + err := pp.addAPIKey(ctx) - ctx.Set("user_id", userID) + var data map[string]interface{} + if jsonErr := json.Unmarshal(rec.Body.Bytes(), &data); jsonErr != nil { + panic(jsonErr) + } - err := pp.addAPIKey(ctx) + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) - var data map[string]interface{} - if jsonErr := json.Unmarshal(rec.Body.Bytes(), &data); jsonErr != nil { - panic(jsonErr) - } + Convey("should return HTTP code 200", func() { + So(rec.Code, ShouldEqual, 200) + }) - Convey("there should be no error", func() { - So(err, ShouldBeNil) - }) + Convey("API key user_guid should equal context user", func() { + So(data["user_guid"], ShouldEqual, userID) + }) + + Convey("API key comment should equal request comment", func() { + So(data["comment"], ShouldEqual, comment) + }) - Convey("should return HTTP code 200", func() { - So(rec.Code, ShouldEqual, 200) + Convey("API key last_used should be nil", func() { + So(data["last_used"], ShouldBeNil) + }) + }) }) + }) + + Convey("when API keys are enabled", func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.AllUsers + + Convey("when comment is not specified", func() { + ctx, _ := makeNewRequest() + ctx.Set("user_id", userID) + + err := pp.addAPIKey(ctx) - Convey("API key user_guid should equal context user", func() { - So(data["user_guid"], ShouldEqual, userID) + Convey("should return an error", func() { + So(err, ShouldResemble, echo.NewHTTPError(http.StatusBadRequest, "Comment can't be empty")) + }) }) - Convey("API key comment should equal request comment", func() { - So(data["comment"], ShouldEqual, comment) + Convey("when a DB error occurs", func() { + comment := "Test API key" + + mockAPIRepo. + EXPECT(). + AddAPIKey(gomock.Eq(userID), gomock.Eq(comment)). + Return(nil, errors.New("Something went wrong")) + + ctx, _ := makeNewRequestWithParams("POST", map[string]string{"comment": comment}) + ctx.Set("user_id", userID) + + err := pp.addAPIKey(ctx) + + Convey("should return an error", func() { + So(err, ShouldResemble, errors.New("Error adding API key")) + }) }) - Convey("API key last_used should be nil", func() { - So(data["last_used"], ShouldBeNil) + Convey("when API key comment is present", func() { + comment := "Test API key" + retval := interfaces.APIKey{UserGUID: userID, Comment: comment} + + mockAPIRepo. + EXPECT(). + AddAPIKey(gomock.Eq(userID), gomock.Eq(comment)). + Return(&retval, nil) + + ctx, rec := makeNewRequestWithParams("POST", map[string]string{"comment": comment}) + ctx.Set("user_id", userID) + + err := pp.addAPIKey(ctx) + + var data map[string]interface{} + if jsonErr := json.Unmarshal(rec.Body.Bytes(), &data); jsonErr != nil { + panic(jsonErr) + } + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("should return HTTP code 200", func() { + So(rec.Code, ShouldEqual, 200) + }) + + Convey("API key user_guid should equal context user", func() { + So(data["user_guid"], ShouldEqual, userID) + }) + + Convey("API key comment should equal request comment", func() { + So(data["comment"], ShouldEqual, comment) + }) + + Convey("API key last_used should be nil", func() { + So(data["last_used"], ShouldBeNil) + }) }) }) - }) } func Test_listAPIKeys(t *testing.T) { t.Parallel() - Convey("Given a request to list API keys", t, func() { - log.SetLevel(log.WarnLevel) + // disabling logging noise + log.SetLevel(log.PanicLevel) - userID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - - Convey("when a DB error occurs", func() { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctrl := gomock.NewController(t) + mockAPIRepo := apikeys.NewMockRepository(ctrl) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + pp := makeMockServer(mockAPIRepo, mockStratosAuth) + defer ctrl.Finish() + defer pp.DatabaseConnectionPool.Close() - m := apikeys.NewMockRepository(ctrl) - m. - EXPECT(). - ListAPIKeys(gomock.Eq(userID)). - Return(nil, errors.New("Something went wrong")) - - req := setupMockReq("GET", "", map[string]string{}) + Convey("Given a request to list API keys", t, func() { + userID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - _, _, ctx, pp, db, _ := setupHTTPTest(req) - defer db.Close() - pp.APIKeysRepository = m + Convey("When API keys are disabled", func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.Disabled + ctx, _ := makeNewRequest() ctx.Set("user_id", userID) err := pp.listAPIKeys(ctx) Convey("should return an error", func() { - So(err, ShouldResemble, errors.New("Error listing API keys")) + So(err, ShouldResemble, echo.NewHTTPError(http.StatusForbidden, "API keys are disabled")) }) }) - Convey("when DB no errors occur", func() { - r1 := &interfaces.APIKey{ - GUID: "00000000-0000-0000-0000-000000000000", - Secret: "", - UserGUID: userID, - Comment: "First key", - LastUsed: nil, - } - - r2 := &interfaces.APIKey{ - GUID: "11111111-1111-1111-1111-111111111111", - Secret: "", - UserGUID: userID, - Comment: "Second key", - LastUsed: nil, - } - - retval := []interfaces.APIKey{*r1, *r2} - - ctrl := gomock.NewController(t) - defer ctrl.Finish() + Convey("When API keys are enabled", func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.AllUsers - m := apikeys.NewMockRepository(ctrl) - m. - EXPECT(). - ListAPIKeys(gomock.Eq(userID)). - Return(retval, nil) + Convey("when a DB error occurs", func() { + mockAPIRepo. + EXPECT(). + ListAPIKeys(gomock.Eq(userID)). + Return(nil, errors.New("Something went wrong")) - req := setupMockReq("GET", "", map[string]string{}) + ctx, _ := makeNewRequest() + ctx.Set("user_id", userID) - rec, _, ctx, pp, db, _ := setupHTTPTest(req) - defer db.Close() - pp.APIKeysRepository = m + err := pp.listAPIKeys(ctx) - ctx.Set("user_id", userID) - - err := pp.listAPIKeys(ctx) - - Convey("there should be no error", func() { - So(err, ShouldBeNil) + Convey("should return an error", func() { + So(err, ShouldResemble, errors.New("Error listing API keys")) + }) }) - Convey("return valid JSON", func() { - So(rec.Body.String(), ShouldEqual, jsonMust(retval)+"\n") + Convey("when DB no errors occur", func() { + r1 := &interfaces.APIKey{ + GUID: "00000000-0000-0000-0000-000000000000", + Secret: "", + UserGUID: userID, + Comment: "First key", + LastUsed: nil, + } + + r2 := &interfaces.APIKey{ + GUID: "11111111-1111-1111-1111-111111111111", + Secret: "", + UserGUID: userID, + Comment: "Second key", + LastUsed: nil, + } + + retval := []interfaces.APIKey{*r1, *r2} + + mockAPIRepo. + EXPECT(). + ListAPIKeys(gomock.Eq(userID)). + Return(retval, nil) + + ctx, rec := makeNewRequest() + ctx.Set("user_id", userID) + + err := pp.listAPIKeys(ctx) + + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) + + Convey("return valid JSON", func() { + So(rec.Body.String(), ShouldEqual, jsonMust(retval)+"\n") + }) }) }) }) @@ -207,82 +312,77 @@ func Test_listAPIKeys(t *testing.T) { func Test_deleteAPIKeys(t *testing.T) { t.Parallel() + // disabling logging noise + log.SetLevel(log.PanicLevel) + + ctrl := gomock.NewController(t) + mockAPIRepo := apikeys.NewMockRepository(ctrl) + mockStratosAuth := mock_interfaces.NewMockStratosAuth(ctrl) + pp := makeMockServer(mockAPIRepo, mockStratosAuth) + defer ctrl.Finish() + defer pp.DatabaseConnectionPool.Close() + userID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" keyID := "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" Convey("Given a request to delete an API key", t, func() { - log.SetLevel(log.PanicLevel) - - Convey("when no API key GUID is supplied", func() { - req := setupMockReq("POST", "", map[string]string{ - "guid": "", - }) - - _, _, ctx, pp, db, _ := setupHTTPTest(req) - defer db.Close() + Convey("when API keys are disabled", func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.Disabled + ctx, _ := makeNewRequestWithParams("POST", map[string]string{"guid": keyID}) ctx.Set("user_id", userID) err := pp.deleteAPIKey(ctx) Convey("should return an error", func() { - So(err, ShouldResemble, echo.NewHTTPError(http.StatusBadRequest, "API key guid can't be empty")) + So(err, ShouldResemble, echo.NewHTTPError(http.StatusForbidden, "API keys are disabled")) }) }) - Convey("when an error occured during API key deletion", func() { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - m := apikeys.NewMockRepository(ctrl) - m. - EXPECT(). - DeleteAPIKey(gomock.Eq(userID), gomock.Eq(keyID)). - Return(errors.New("Something went wrong")) - - req := setupMockReq("POST", "", map[string]string{ - "guid": keyID, - }) - - _, _, ctx, pp, db, _ := setupHTTPTest(req) - defer db.Close() + Convey("when API keys are enabled", func() { + pp.Config.APIKeysEnabled = config.APIKeysConfigEnum.AllUsers - pp.APIKeysRepository = m + Convey("when no API key GUID is supplied", func() { + ctx, _ := makeNewRequest() + ctx.Set("user_id", userID) - ctx.Set("user_id", userID) + err := pp.deleteAPIKey(ctx) - err := pp.deleteAPIKey(ctx) - - Convey("should return an error", func() { - So(err, ShouldResemble, errors.New("Error deleting API key")) + Convey("should return an error", func() { + So(err, ShouldResemble, echo.NewHTTPError(http.StatusBadRequest, "API key guid can't be empty")) + }) }) - }) - Convey("when an API key is deleted succesfully", func() { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + Convey("when an error occured during API key deletion", func() { + mockAPIRepo. + EXPECT(). + DeleteAPIKey(gomock.Eq(userID), gomock.Eq(keyID)). + Return(errors.New("Something went wrong")) - m := apikeys.NewMockRepository(ctrl) - m. - EXPECT(). - DeleteAPIKey(gomock.Eq(userID), gomock.Eq(keyID)). - Return(nil) + ctx, _ := makeNewRequestWithParams("POST", map[string]string{"guid": keyID}) + ctx.Set("user_id", userID) - req := setupMockReq("POST", "", map[string]string{ - "guid": keyID, - }) + err := pp.deleteAPIKey(ctx) - _, _, ctx, pp, db, _ := setupHTTPTest(req) - defer db.Close() + Convey("should return an error", func() { + So(err, ShouldResemble, errors.New("Error deleting API key")) + }) + }) - pp.APIKeysRepository = m + Convey("when an API key is deleted succesfully", func() { + mockAPIRepo. + EXPECT(). + DeleteAPIKey(gomock.Eq(userID), gomock.Eq(keyID)). + Return(nil) - ctx.Set("user_id", userID) + ctx, _ := makeNewRequestWithParams("POST", map[string]string{"guid": keyID}) + ctx.Set("user_id", userID) - err := pp.deleteAPIKey(ctx) + err := pp.deleteAPIKey(ctx) - Convey("there should be no error", func() { - So(err, ShouldBeNil) + Convey("there should be no error", func() { + So(err, ShouldBeNil) + }) }) }) }) diff --git a/src/jetstream/middleware_test.go b/src/jetstream/middleware_test.go index c7da9d9e1a..4a64895137 100644 --- a/src/jetstream/middleware_test.go +++ b/src/jetstream/middleware_test.go @@ -41,6 +41,15 @@ func makeNewRequest() (echo.Context, *httptest.ResponseRecorder) { return ctx, rec } +func makeNewRequestWithParams(httpVerb string, formValues map[string]string) (echo.Context, *httptest.ResponseRecorder) { + req := setupMockReq(httpVerb, "", formValues) + rec := httptest.NewRecorder() + e := echo.New() + ctx := e.NewContext(req, rec) + + return ctx, rec +} + func Test_apiKeyMiddleware(t *testing.T) { t.Parallel() From 9b6ecfc3c31abe7b6033ea0864ff35ac8b5b8588 Mon Sep 17 00:00:00 2001 From: Ivan Kapelyukhin Date: Fri, 28 Aug 2020 12:51:33 +0200 Subject: [PATCH 05/10] Fix failing test --- src/jetstream/auth_test.go | 4 ++-- src/jetstream/main.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/jetstream/auth_test.go b/src/jetstream/auth_test.go index b7657fbede..f2c72f7cd0 100644 --- a/src/jetstream/auth_test.go +++ b/src/jetstream/auth_test.go @@ -784,9 +784,9 @@ func TestVerifySession(t *testing.T) { So(contentType, ShouldEqual, "application/json; charset=UTF-8") }) - var expectedScopes = "\"scopes\":[\"openid\",\"scim.read\",\"cloud_controller.admin\",\"uaa.user\",\"cloud_controller.read\",\"password.write\",\"routing.router_groups.read\",\"cloud_controller.write\",\"doppler.firehose\",\"scim.write\"]" + var expectedScopes = `"scopes":["openid","scim.read","cloud_controller.admin","uaa.user","cloud_controller.read","password.write","routing.router_groups.read","cloud_controller.write","doppler.firehose","scim.write"]` - var expectedBody = "{\"version\":{\"proxy_version\":\"dev\",\"database_version\":20161117141922},\"user\":{\"guid\":\"asd-gjfg-bob\",\"name\":\"admin\",\"admin\":false," + expectedScopes + "},\"endpoints\":{\"cf\":{}},\"plugins\":null,\"config\":{\"enableTechPreview\":false}}" + var expectedBody = `{"version":{"proxy_version":"dev","database_version":20161117141922},"user":{"guid":"asd-gjfg-bob","name":"admin","admin":false,` + expectedScopes + `},"endpoints":{"cf":{}},"plugins":null,"config":{"enableTechPreview":false,"APIKeysEnabled":"admin_only"}}` Convey("Should contain expected body", func() { So(res, ShouldNotBeNil) diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 4deab1d10b..fcacdf4cdf 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -257,12 +257,6 @@ func main() { }() log.Info("Session data store initialized.") - // Setting default value for APIKeysEnabled - if portalConfig.APIKeysEnabled == "" { - log.Debug(`APIKeysEnabled not set, setting to "admin_only"`) - portalConfig.APIKeysEnabled = config.APIKeysConfigEnum.AdminOnly - } - // Setup the global interface for the proxy portalProxy := newPortalProxy(portalConfig, databaseConnectionPool, sessionStore, sessionStoreOptions, envLookup) portalProxy.SessionDataStore = sessionDataStore @@ -665,6 +659,12 @@ func newPortalProxy(pc interfaces.PortalConfig, dcp *sql.DB, ss HttpSessionStore log.Infof("Session Cookie name: %s", cookieName) + // Setting default value for APIKeysEnabled + if pc.APIKeysEnabled == "" { + log.Debug(`APIKeysEnabled not set, setting to "admin_only"`) + pc.APIKeysEnabled = config.APIKeysConfigEnum.AdminOnly + } + pp := &portalProxy{ Config: pc, DatabaseConnectionPool: dcp, From a063d9c444b933d734fc231a907dd443dab18f9c Mon Sep 17 00:00:00 2001 From: Ivan Kapelyukhin Date: Fri, 28 Aug 2020 14:55:57 +0200 Subject: [PATCH 06/10] Add API_KEYS_ENABLED to the Helm chart --- deploy/kubernetes/console/README.md | 1 + deploy/kubernetes/console/templates/deployment.yaml | 2 ++ deploy/kubernetes/console/values.yaml | 3 +++ 3 files changed, 6 insertions(+) diff --git a/deploy/kubernetes/console/README.md b/deploy/kubernetes/console/README.md index bbca392c92..3f5243a7ad 100644 --- a/deploy/kubernetes/console/README.md +++ b/deploy/kubernetes/console/README.md @@ -75,6 +75,7 @@ The following table lists the configurable parameters of the Stratos Helm chart |console.templatesConfigMapName|Name of config map that provides the template files for user invitation emails|| |console.userInviteSubject|Email subject of the user invitation message|| |console.techPreview|Enable/disable Tech Preview features|false| +|console.apiKeysEnabled|Enable/disable API key-based access to Stratos API (disable, admin_only, all_users)|admin_only| |console.ui.listMaxSize|Override the default maximum number of entities that a configured list can fetch. When a list meets this amount additional pages are not fetched|| |console.ui.listAllowLoadMaxed|If the maximum list size is met give the user the option to fetch all results|false| |console.localAdminPassword|Use local admin user instead of UAA - set to a password to enable|| diff --git a/deploy/kubernetes/console/templates/deployment.yaml b/deploy/kubernetes/console/templates/deployment.yaml index a6b0b656c7..ceb1259a7e 100644 --- a/deploy/kubernetes/console/templates/deployment.yaml +++ b/deploy/kubernetes/console/templates/deployment.yaml @@ -277,6 +277,8 @@ spec: value: {{ default "" .Values.console.userInviteSubject | quote }} - name: ENABLE_TECH_PREVIEW value: {{ default "false" .Values.console.techPreview | quote }} + - name: API_KEYS_ENABLED + value: {{ default "admin_only" .Values.console.apiKeysEnabled | quote }} {{- if .Values.console.ui }} {{- if .Values.console.ui.listMaxSize }} - name: UI_LIST_MAX_SIZE diff --git a/deploy/kubernetes/console/values.yaml b/deploy/kubernetes/console/values.yaml index 16124f7a9e..2b14718a99 100644 --- a/deploy/kubernetes/console/values.yaml +++ b/deploy/kubernetes/console/values.yaml @@ -66,6 +66,9 @@ console: # Enable/disable Tech Preview techPreview: false + # Enable/disable API key-based access to Stratos API + apiKeysEnabled: admin_only + ui: # Override the default maximum number of entities that a configured list can fetch. When a list meets this amount additional pages are not fetched listMaxSize: From 427a4c8a41bd0c5537b726febaac8521601e0031 Mon Sep 17 00:00:00 2001 From: Ivan Kapelyukhin Date: Tue, 1 Sep 2020 12:40:25 +0200 Subject: [PATCH 07/10] Add JSON schema to the Helm chart --- deploy/kubernetes/console/values.schema.json | 373 +++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 deploy/kubernetes/console/values.schema.json diff --git a/deploy/kubernetes/console/values.schema.json b/deploy/kubernetes/console/values.schema.json new file mode 100644 index 0000000000..34651e77ae --- /dev/null +++ b/deploy/kubernetes/console/values.schema.json @@ -0,0 +1,373 @@ +{ + "$schema": "http://json-schema.org/schema#", + "type": "object", + "properties": { + "autoCleanup": { + "type": "boolean" + }, + "configInit": { + "type": "object", + "properties": { + "nodeSelector": { + "type": "object" + } + } + }, + "console": { + "type": "object", + "properties": { + "apiKeysEnabled": { + "type": "string", + "enum": ["disabled", "admin_only", "all_users"] + }, + "autoRegisterCF": { + "type": ["string", "null"] + }, + "backendLogLevel": { + "type": "string" + }, + "cookieDomain": { + "type": ["string", "null"] + }, + "deploymentAnnotations": { + "type": "object" + }, + "deploymentExtraLabels": { + "type": "object" + }, + "jobAnnotations": { + "type": "object" + }, + "jobExtraLabels": { + "type": "object" + }, + "localAdminPassword": { + "type": ["string", "null"] + }, + "nodeSelector": { + "type": "object" + }, + "podAnnotations": { + "type": "object" + }, + "podExtraLabels": { + "type": "object" + }, + "service": { + "type": "object", + "properties": { + "annotations": { + "type": "object" + }, + "externalIPs": { + "type": "array" + }, + "externalName": { + "type": ["string", "null"] + }, + "extraLabels": { + "type": "object" + }, + "http": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "servicePort": { + "type": "integer" + } + } + }, + "ingress": { + "type": "object", + "properties": { + "annotations": { + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "extraLabels": { + "type": "object" + }, + "host": { + "type": ["string", "null"] + }, + "secretName": { + "type": ["string", "null"] + }, + "tls": { + "type": "object", + "properties": { + "crt": { + "type": ["string", "null"] + }, + "key": { + "type": ["string", "null"] + } + } + } + } + }, + "loadBalancerIP": { + "type": ["string", "null"] + }, + "loadBalancerSourceRanges": { + "type": "array" + }, + "servicePort": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "sessionStoreSecret": { + "type": ["string", "null"] + }, + "sslCiphers": { + "type": ["string", "null"] + }, + "sslProtocols": { + "type": ["string", "null"] + }, + "ssoLogin": { + "type": "boolean" + }, + "ssoOptions": { + "type": ["string", "null"] + }, + "statefulSetAnnotations": { + "type": "object" + }, + "statefulSetExtraLabels": { + "type": "object" + }, + "techPreview": { + "type": "boolean" + }, + "templatesConfigMapName": { + "type": ["string", "null"] + }, + "tlsSecretName": { + "type": ["string", "null"] + }, + "ui": { + "type": "object", + "properties": { + "listAllowLoadMaxed": { + "type": "boolean" + }, + "listMaxSize": { + "type": ["integer", "null"] + } + } + }, + "userInviteSubject": { + "type": ["string", "null"] + } + } + }, + "consoleVersion": { + "type": "string" + }, + "dockerRegistrySecret": { + "type": "string" + }, + "env": { + "type": "object", + "properties": { + "DOMAIN": { + "type": ["string", "null"] + }, + "SMTP_AUTH": { + "type": "string" + }, + "SMTP_FROM_ADDRESS": { + "type": ["string", "null"] + }, + "SMTP_HOST": { + "type": ["string", "null"] + }, + "SMTP_PASSWORD": { + "type": ["string", "null"] + }, + "SMTP_PORT": { + "type": "string" + }, + "SMTP_USER": { + "type": ["string", "null"] + }, + "UAA_HOST": { + "type": ["string", "null"] + }, + "UAA_PORT": { + "type": "integer" + }, + "UAA_ZONE": { + "type": "string" + } + } + }, + "imagePullPolicy": { + "type": "string" + }, + "images": { + "type": "object", + "properties": { + "configInit": { + "type": "string" + }, + "console": { + "type": "string" + }, + "mariadb": { + "type": "string" + }, + "proxy": { + "type": "string" + } + } + }, + "kube": { + "type": "object", + "properties": { + "auth": { + "type": "string" + }, + "external_console_https_port": { + "type": "integer" + }, + "organization": { + "type": "string" + }, + "registry": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "hostname": { + "type": ["string", "null"] + }, + "password": { + "type": ["string", "null"] + }, + "username": { + "type": ["string", "null"] + } + } + }, + "storage_class": { + "type": "object", + "properties": { + "persistent": { + "type": ["string", "null"] + } + } + } + } + }, + "mariadb": { + "type": "object", + "properties": { + "database": { + "type": "string" + }, + "external": { + "type": "boolean" + }, + "host": { + "type": ["string", "null"] + }, + "nodeSelector": { + "type": "object" + }, + "persistence": { + "type": "object", + "properties": { + "accessMode": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "size": { + "type": "string" + }, + "storageClass": { + "type": ["string", "null"] + } + } + }, + "port": { + "type": "null" + }, + "resources": { + "type": "object", + "properties": { + "requests": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + } + } + } + }, + "rootPassword": { + "type": ["string", "null"] + }, + "tls": { + "type": ["string", "null"] + }, + "type": { + "type": ["string", "null"] + }, + "user": { + "type": "string" + }, + "userPassword": { + "type": ["string", "null"] + } + } + }, + "services": { + "type": "object", + "properties": { + "loadbalanced": { + "type": "boolean" + } + } + }, + "uaa": { + "type": "object", + "properties": { + "consoleAdminIdentifier": { + "type": ["string", "null"] + }, + "consoleClient": { + "type": ["string", "null"] + }, + "consoleClientSecret": { + "type": ["string", "null"] + }, + "endpoint": { + "type": ["string", "null"] + }, + "skipSSLValidation": { + "type": "boolean" + } + } + }, + "useLb": { + "type": "boolean" + } + } +} From 93edf016a1a223b83090256bb4ebf33e2cd76444 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 3 Sep 2020 17:27:34 +0100 Subject: [PATCH 08/10] Enable/disable API keys UI given API keys config setting --- deploy/kubernetes/console/README.md | 2 +- src/frontend/packages/core/src/app.routing.ts | 7 +++- .../src/core/apiKey-auth-guard.service.ts | 31 ++++++++++++++++ .../packages/core/src/core/core.module.ts | 2 ++ .../stratos-user-permissions.checker.ts | 35 ++++++++++++++++--- .../page-header/page-header.component.html | 2 +- .../page-header/page-header.component.ts | 6 ++++ .../packages/store/src/types/auth.types.ts | 8 ++++- src/jetstream/config.dev | 3 ++ 9 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 src/frontend/packages/core/src/core/apiKey-auth-guard.service.ts diff --git a/deploy/kubernetes/console/README.md b/deploy/kubernetes/console/README.md index 3f5243a7ad..603a85ec47 100644 --- a/deploy/kubernetes/console/README.md +++ b/deploy/kubernetes/console/README.md @@ -75,7 +75,7 @@ The following table lists the configurable parameters of the Stratos Helm chart |console.templatesConfigMapName|Name of config map that provides the template files for user invitation emails|| |console.userInviteSubject|Email subject of the user invitation message|| |console.techPreview|Enable/disable Tech Preview features|false| -|console.apiKeysEnabled|Enable/disable API key-based access to Stratos API (disable, admin_only, all_users)|admin_only| +|console.apiKeysEnabled|Enable/disable API key-based access to Stratos API (disabled, admin_only, all_users)|admin_only| |console.ui.listMaxSize|Override the default maximum number of entities that a configured list can fetch. When a list meets this amount additional pages are not fetched|| |console.ui.listAllowLoadMaxed|If the maximum list size is met give the user the option to fetch all results|false| |console.localAdminPassword|Use local admin user instead of UAA - set to a password to enable|| diff --git a/src/frontend/packages/core/src/app.routing.ts b/src/frontend/packages/core/src/app.routing.ts index a57cda750d..a54b1c30d5 100644 --- a/src/frontend/packages/core/src/app.routing.ts +++ b/src/frontend/packages/core/src/app.routing.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { APIKeyAuthGuardService } from './core/apiKey-auth-guard.service'; import { AuthGuardService } from './core/auth-guard.service'; import { CoreModule } from './core/core.module'; import { EndpointsService } from './core/endpoints.service'; @@ -94,7 +95,11 @@ const appRoutes: Routes = [ }, { path: 'about', loadChildren: () => import('./features/about/about.module').then(m => m.AboutModule) }, { path: 'user-profile', loadChildren: () => import('./features/user-profile/user-profile.module').then(m => m.UserProfileModule) }, - { path: 'api-keys', loadChildren: () => import('./features/api-keys/api-keys.module').then(m => m.ApiKeysModule) }, + { + path: 'api-keys', + loadChildren: () => import('./features/api-keys/api-keys.module').then(m => m.ApiKeysModule), + canActivate: [APIKeyAuthGuardService] + }, { path: 'events', loadChildren: () => import('./features/event-page/event-page.module').then(m => m.EventPageModule) }, { path: 'errors/:endpointId', diff --git a/src/frontend/packages/core/src/core/apiKey-auth-guard.service.ts b/src/frontend/packages/core/src/core/apiKey-auth-guard.service.ts new file mode 100644 index 0000000000..c6e6e505ce --- /dev/null +++ b/src/frontend/packages/core/src/core/apiKey-auth-guard.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { RouterNav } from '../../../store/src/actions/router.actions'; +import { AppState } from '../../../store/src/app-state'; +import { CurrentUserPermissionsService } from './permissions/current-user-permissions.service'; +import { StratosCurrentUserPermissions } from './permissions/stratos-user-permissions.checker'; + +@Injectable() +export class APIKeyAuthGuardService implements CanActivate { + + constructor( + private store: Store, + private cups: CurrentUserPermissionsService, + ) { } + + canActivate(): Observable { + return this.cups.can(StratosCurrentUserPermissions.API_KEYS).pipe( + map(can => { + if (can) { + return true; + } + this.store.dispatch(new RouterNav({ path: ['/'] })); + return false; + }) + ); + } +} \ No newline at end of file diff --git a/src/frontend/packages/core/src/core/core.module.ts b/src/frontend/packages/core/src/core/core.module.ts index 25b731b63e..792f489d5c 100644 --- a/src/frontend/packages/core/src/core/core.module.ts +++ b/src/frontend/packages/core/src/core/core.module.ts @@ -12,6 +12,7 @@ import { PaginationMonitorFactory } from '../../../store/src/monitors/pagination import { NoContentMessageComponent } from '../shared/components/no-content-message/no-content-message.component'; import { RecentEntitiesComponent } from '../shared/components/recent-entities/recent-entities.component'; import { UserAvatarComponent } from './../shared/components/user-avatar/user-avatar.component'; +import { APIKeyAuthGuardService } from './apiKey-auth-guard.service'; import { AuthGuardService } from './auth-guard.service'; import { ButtonBlurOnClickDirective } from './button-blur-on-click.directive'; import { BytesToHumanSize, MegaBytesToHumanSize } from './byte-formatters.pipe'; @@ -69,6 +70,7 @@ import { WindowRef } from './window-ref/window-ref.service'; ], providers: [ AuthGuardService, + APIKeyAuthGuardService, NotSetupGuardService, PageHeaderService, EventWatcherService, diff --git a/src/frontend/packages/core/src/core/permissions/stratos-user-permissions.checker.ts b/src/frontend/packages/core/src/core/permissions/stratos-user-permissions.checker.ts index db4c382b26..39308b500a 100644 --- a/src/frontend/packages/core/src/core/permissions/stratos-user-permissions.checker.ts +++ b/src/frontend/packages/core/src/core/permissions/stratos-user-permissions.checker.ts @@ -1,12 +1,15 @@ import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import { GeneralEntityAppState } from '../../../../store/src/app-state'; +import { selectSessionData } from '../../../../store/src/reducers/auth.reducer'; import { getCurrentUserStratosHasScope, getCurrentUserStratosRole, PermissionValues, } from '../../../../store/src/selectors/current-user-role.selectors'; +import { APIKeysEnabled } from '../../../../store/src/types/auth.types'; import { IPermissionConfigs, PermissionConfig, PermissionTypes } from './current-user-permissions.config'; import { BaseCurrentUserPermissionsChecker, @@ -19,6 +22,10 @@ import { export enum StratosCurrentUserPermissions { ENDPOINT_REGISTER = 'register.endpoint', PASSWORD_CHANGE = 'change-password', + /** + * Does the user have permission to view/create/delete their own API Keys? + */ + API_KEYS = 'api-keys' } export enum StratosPermissionStrings { @@ -34,7 +41,8 @@ export enum StratosScopeStrings { export enum StratosPermissionTypes { STRATOS = 'internal', - STRATOS_SCOPE = 'internal-scope' + STRATOS_SCOPE = 'internal-scope', + API_KEY = 'api-key' } // For each set permissions are checked by permission types of ENDPOINT, ENDPOINT_SCOPE, STRATOS_SCOPE, FEATURE_FLAG or a random bag. @@ -43,11 +51,12 @@ export enum StratosPermissionTypes { export const stratosPermissionConfigs: IPermissionConfigs = { [StratosCurrentUserPermissions.ENDPOINT_REGISTER]: new PermissionConfig(StratosPermissionTypes.STRATOS, StratosPermissionStrings.STRATOS_ADMIN), [StratosCurrentUserPermissions.PASSWORD_CHANGE]: new PermissionConfig(StratosPermissionTypes.STRATOS_SCOPE, StratosScopeStrings.STRATOS_CHANGE_PASSWORD), + [StratosCurrentUserPermissions.API_KEYS]: new PermissionConfig(StratosPermissionTypes.API_KEY, '') }; export class StratosUserPermissionsChecker extends BaseCurrentUserPermissionsChecker implements ICurrentUserPermissionsChecker { - constructor(private store: Store, ) { - super() + constructor(private store: Store,) { + super(); } getPermissionConfig(action: string) { @@ -75,6 +84,8 @@ export class StratosUserPermissionsChecker extends BaseCurrentUserPermissionsChe return this.getInternalCheck(permissionConfig.permission as StratosPermissionStrings); case (StratosPermissionTypes.STRATOS_SCOPE): return this.getInternalScopesCheck(permissionConfig.permission as StratosScopeStrings); + case (StratosPermissionTypes.API_KEY): + return this.apiKeyCheck(); } } @@ -95,6 +106,20 @@ export class StratosUserPermissionsChecker extends BaseCurrentUserPermissionsChe return this.check(StratosPermissionTypes.STRATOS_SCOPE, permission); } + private apiKeyCheck(): Observable { + return this.store.select(selectSessionData()).pipe( + switchMap(sessionData => { + switch (sessionData.config.APIKeysEnabled) { + case APIKeysEnabled.ADMIN_ONLY: + return this.store.select(getCurrentUserStratosRole(StratosPermissionStrings.STRATOS_ADMIN)); + case APIKeysEnabled.ALL_USERS: + return of(true); + } + return of(false); + }) + ); + } + public getComplexCheck( permissionConfig: PermissionConfig[], ...args: any[] @@ -108,7 +133,7 @@ export class StratosUserPermissionsChecker extends BaseCurrentUserPermissionsChe checks: this.getInternalScopesChecks(configGroup), }; } - }) + }); // Checker must handle all configs return res.every(check => !!check) ? res : null; } diff --git a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.html b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.html index a7573fa455..5f6c34fe7d 100644 --- a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.html +++ b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.html @@ -75,7 +75,7 @@

Recent Activity

-
diff --git a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.ts b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.ts index 29d5ce0e56..2547077ff4 100644 --- a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.ts +++ b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.ts @@ -15,6 +15,8 @@ import { selectIsMobile } from '../../../../../store/src/selectors/dashboard.sel import { InternalEventSeverity } from '../../../../../store/src/types/internal-events.types'; import { StratosStatus } from '../../../../../store/src/types/shared.types'; import { IFavoriteMetadata, UserFavorite } from '../../../../../store/src/types/user-favorites.types'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; +import { StratosCurrentUserPermissions } from '../../../core/permissions/stratos-user-permissions.checker'; import { UserProfileService } from '../../../core/user-profile.service'; import { IPageSideNavTab } from '../../../features/dashboard/page-side-nav/page-side-nav.component'; import { TabNavService } from '../../../tab-nav.service'; @@ -29,6 +31,7 @@ import { BREADCRUMB_URL_PARAM, IHeaderBreadcrumb, IHeaderBreadcrumbLink } from ' styleUrls: ['./page-header.component.scss'] }) export class PageHeaderComponent implements OnDestroy, AfterViewInit { + public canAPIKeys$: Observable; public breadcrumbDefinitions: IHeaderBreadcrumbLink[] = null; private breadcrumbKey: string; public eventSeverity = InternalEventSeverity; @@ -156,6 +159,7 @@ export class PageHeaderComponent implements OnDestroy, AfterViewInit { eventService: GlobalEventService, private favoritesConfigMapper: FavoritesConfigMapper, private userProfileService: UserProfileService, + private cups: CurrentUserPermissionsService, ) { this.events$ = eventService.events$.pipe( startWith([]) @@ -185,6 +189,8 @@ export class PageHeaderComponent implements OnDestroy, AfterViewInit { this.allowGravatar$ = this.store.select(selectDashboardState).pipe( map(dashboardState => dashboardState.gravatarEnabled) ); + + this.canAPIKeys$ = this.cups.can(StratosCurrentUserPermissions.API_KEYS); } ngOnDestroy() { diff --git a/src/frontend/packages/store/src/types/auth.types.ts b/src/frontend/packages/store/src/types/auth.types.ts index cef354d888..aa21a7efb6 100644 --- a/src/frontend/packages/store/src/types/auth.types.ts +++ b/src/frontend/packages/store/src/types/auth.types.ts @@ -22,10 +22,16 @@ export interface SessionEndpoints { export interface SessionEndpoint { [guid: string]: SessionDataEndpoint; } +export enum APIKeysEnabled { + DISABLED = 'disabled', + ADMIN_ONLY = 'admin_only', + ALL_USERS = 'all_users' +} export interface SessionDataConfig { enableTechPreview?: boolean; listMaxSize?: number; listAllowLoadMaxed?: boolean; + APIKeysEnabled?: APIKeysEnabled; } export interface SessionData { endpoints?: SessionEndpoints; @@ -44,7 +50,7 @@ export interface SessionData { ['plugin-config']?: PluginConfig; plugins: { demo: boolean, - [pluginName: string]: boolean + [pluginName: string]: boolean; }; config: SessionDataConfig; } diff --git a/src/jetstream/config.dev b/src/jetstream/config.dev index a98e5c3ec3..d6f6b930b3 100644 --- a/src/jetstream/config.dev +++ b/src/jetstream/config.dev @@ -53,3 +53,6 @@ AUTH_ENDPOINT_TYPE=local LOCAL_USER=admin LOCAL_USER_PASSWORD=admin LOCAL_USER_SCOPE=stratos.admin + +# Enable/disable API key-based access to Stratos API (disabled, admin_only, all_users). Default is admin_only +#API_KEYS_ENABLED=admin_only \ No newline at end of file From 96b76b2df461d2e76f360f808a829f88ad2090e7 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 7 Sep 2020 11:39:43 +0100 Subject: [PATCH 09/10] Changes following review --- .../packages/core/src/core/apiKey-auth-guard.service.ts | 7 +++---- src/frontend/packages/store/src/types/auth.types.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/frontend/packages/core/src/core/apiKey-auth-guard.service.ts b/src/frontend/packages/core/src/core/apiKey-auth-guard.service.ts index c6e6e505ce..70e965da23 100644 --- a/src/frontend/packages/core/src/core/apiKey-auth-guard.service.ts +++ b/src/frontend/packages/core/src/core/apiKey-auth-guard.service.ts @@ -20,11 +20,10 @@ export class APIKeyAuthGuardService implements CanActivate { canActivate(): Observable { return this.cups.can(StratosCurrentUserPermissions.API_KEYS).pipe( map(can => { - if (can) { - return true; + if (!can) { + this.store.dispatch(new RouterNav({ path: ['/'] })); } - this.store.dispatch(new RouterNav({ path: ['/'] })); - return false; + return can; }) ); } diff --git a/src/frontend/packages/store/src/types/auth.types.ts b/src/frontend/packages/store/src/types/auth.types.ts index aa21a7efb6..5787243ea7 100644 --- a/src/frontend/packages/store/src/types/auth.types.ts +++ b/src/frontend/packages/store/src/types/auth.types.ts @@ -50,7 +50,7 @@ export interface SessionData { ['plugin-config']?: PluginConfig; plugins: { demo: boolean, - [pluginName: string]: boolean; + [pluginName: string]: boolean, }; config: SessionDataConfig; } From d49458388152db67dbc60dcd02ff2c3fca2bdae4 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 7 Sep 2020 14:13:57 +0100 Subject: [PATCH 10/10] Fix unit tests --- .../autoscaler-metric-page.component.spec.ts | 4 +++- ...caler-scale-history-page.component.spec.ts | 4 +++- ...it-autoscaler-credential.component.spec.ts | 2 ++ .../edit-autoscaler-policy.component.spec.ts | 2 ++ .../about-page/about-page.component.spec.ts | 6 +++++- .../diagnostics-page.component.spec.ts | 6 +++++- .../eula-page/eula-page.component.spec.ts | 6 +++++- ...reate-endpoint-base-step.component.spec.ts | 21 ++++++++++++------- .../create-endpoint.component.spec.ts | 5 ++++- .../edit-endpoint.component.spec.ts | 4 +++- .../events-page/events-page.component.spec.ts | 6 +++++- .../home/home/home-page.component.spec.ts | 6 +++++- .../metrics/metrics/metrics.component.spec.ts | 7 ++++++- .../no-endpoints-non-admin.component.spec.ts | 6 +++++- .../local-account-wizard.component.spec.ts | 6 +++++- .../console-uaa-wizard.component.spec.ts | 6 +++++- .../profile-info.component.spec.ts | 7 ++++++- .../page-header/page-header.component.spec.ts | 4 +++- 18 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts index 951dceda5c..a6b358f4d1 100644 --- a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts +++ b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts @@ -7,6 +7,7 @@ import { createEmptyStoreModule } from '@stratosui/store/testing'; import { ApplicationService } from '../../../../cloud-foundry/src/features/applications/application.service'; import { ApplicationServiceMock } from '../../../../cloud-foundry/test-framework/application-service-helper'; import { CoreModule } from '../../../../core/src/core/core.module'; +import { CurrentUserPermissionsService } from '../../../../core/src/core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../../core/src/shared/shared.module'; import { TabNavService } from '../../../../core/src/tab-nav.service'; import { CfAutoscalerTestingModule } from '../../cf-autoscaler-testing.module'; @@ -30,7 +31,8 @@ describe('AutoscalerMetricPageComponent', () => { providers: [ DatePipe, { provide: ApplicationService, useClass: ApplicationServiceMock }, - TabNavService + TabNavService, + CurrentUserPermissionsService ] }) .compileComponents(); diff --git a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts index cdadad97b8..477a22f4fa 100644 --- a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts +++ b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts @@ -7,6 +7,7 @@ import { createEmptyStoreModule } from '@stratosui/store/testing'; import { ApplicationService } from '../../../../cloud-foundry/src/features/applications/application.service'; import { ApplicationServiceMock } from '../../../../cloud-foundry/test-framework/application-service-helper'; import { CoreModule } from '../../../../core/src/core/core.module'; +import { CurrentUserPermissionsService } from '../../../../core/src/core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../../core/src/shared/shared.module'; import { TabNavService } from '../../../../core/src/tab-nav.service'; import { CfAutoscalerTestingModule } from '../../cf-autoscaler-testing.module'; @@ -30,7 +31,8 @@ describe('AutoscalerScaleHistoryPageComponent', () => { providers: [ DatePipe, { provide: ApplicationService, useClass: ApplicationServiceMock }, - TabNavService + TabNavService, + CurrentUserPermissionsService ] }) .compileComponents(); diff --git a/src/frontend/packages/cf-autoscaler/src/features/edit-autoscaler-credential/edit-autoscaler-credential.component.spec.ts b/src/frontend/packages/cf-autoscaler/src/features/edit-autoscaler-credential/edit-autoscaler-credential.component.spec.ts index 5c7e1f49fd..847513f52e 100644 --- a/src/frontend/packages/cf-autoscaler/src/features/edit-autoscaler-credential/edit-autoscaler-credential.component.spec.ts +++ b/src/frontend/packages/cf-autoscaler/src/features/edit-autoscaler-credential/edit-autoscaler-credential.component.spec.ts @@ -6,6 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { ApplicationService } from '../../../../cloud-foundry/src/features/applications/application.service'; import { ApplicationServiceMock } from '../../../../cloud-foundry/test-framework/application-service-helper'; import { CoreModule } from '../../../../core/src/core/core.module'; +import { CurrentUserPermissionsService } from '../../../../core/src/core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../../core/src/shared/shared.module'; import { TabNavService } from '../../../../core/src/tab-nav.service'; import { createBasicStoreModule } from '../../../../store/testing/public-api'; @@ -33,6 +34,7 @@ describe('EditAutoscalerCredentialComponent', () => { DatePipe, { provide: ApplicationService, useClass: ApplicationServiceMock }, TabNavService, + CurrentUserPermissionsService ] }) .compileComponents(); diff --git a/src/frontend/packages/cf-autoscaler/src/features/edit-autoscaler-policy/edit-autoscaler-policy.component.spec.ts b/src/frontend/packages/cf-autoscaler/src/features/edit-autoscaler-policy/edit-autoscaler-policy.component.spec.ts index 5f6c6ccb6d..bfab3dab44 100644 --- a/src/frontend/packages/cf-autoscaler/src/features/edit-autoscaler-policy/edit-autoscaler-policy.component.spec.ts +++ b/src/frontend/packages/cf-autoscaler/src/features/edit-autoscaler-policy/edit-autoscaler-policy.component.spec.ts @@ -7,6 +7,7 @@ import { createEmptyStoreModule } from '@stratosui/store/testing'; import { ApplicationService } from '../../../../cloud-foundry/src/features/applications/application.service'; import { ApplicationServiceMock } from '../../../../cloud-foundry/test-framework/application-service-helper'; import { CoreModule } from '../../../../core/src/core/core.module'; +import { CurrentUserPermissionsService } from '../../../../core/src/core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../../core/src/shared/shared.module'; import { TabNavService } from '../../../../core/src/tab-nav.service'; import { CfAutoscalerTestingModule } from '../../cf-autoscaler-testing.module'; @@ -43,6 +44,7 @@ describe('EditAutoscalerPolicyComponent', () => { { provide: ApplicationService, useClass: ApplicationServiceMock }, TabNavService, EditAutoscalerPolicyService, + CurrentUserPermissionsService ] }) .compileComponents(); diff --git a/src/frontend/packages/core/src/features/about/about-page/about-page.component.spec.ts b/src/frontend/packages/core/src/features/about/about-page/about-page.component.spec.ts index ab09df31b4..69f0863602 100644 --- a/src/frontend/packages/core/src/features/about/about-page/about-page.component.spec.ts +++ b/src/frontend/packages/core/src/features/about/about-page/about-page.component.spec.ts @@ -4,6 +4,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; import { AboutPageComponent } from './about-page.component'; @@ -22,7 +23,10 @@ describe('AboutPageComponent', () => { CoreTestingModule, createBasicStoreModule(), ], - providers: [TabNavService] + providers: [ + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/about/diagnostics-page/diagnostics-page.component.spec.ts b/src/frontend/packages/core/src/features/about/diagnostics-page/diagnostics-page.component.spec.ts index 9cbf0ffe41..16786352e6 100644 --- a/src/frontend/packages/core/src/features/about/diagnostics-page/diagnostics-page.component.spec.ts +++ b/src/frontend/packages/core/src/features/about/diagnostics-page/diagnostics-page.component.spec.ts @@ -4,6 +4,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; import { DiagnosticsPageComponent } from './diagnostics-page.component'; @@ -22,7 +23,10 @@ describe('DiagnosticsPageComponent', () => { CoreTestingModule, createBasicStoreModule(), ], - providers: [TabNavService] + providers: [ + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/about/eula-page/eula-page.component.spec.ts b/src/frontend/packages/core/src/features/about/eula-page/eula-page.component.spec.ts index 8ebf8ea27c..cd45da187e 100644 --- a/src/frontend/packages/core/src/features/about/eula-page/eula-page.component.spec.ts +++ b/src/frontend/packages/core/src/features/about/eula-page/eula-page.component.spec.ts @@ -5,6 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; import { EulaPageComponent } from './eula-page.component'; @@ -24,7 +25,10 @@ describe('EulaPageComponent', () => { HttpClientTestingModule, createBasicStoreModule(), ], - providers: [TabNavService] + providers: [ + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.spec.ts index 715e655dc6..eae2dd40c7 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.spec.ts @@ -6,6 +6,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../../shared/shared.module'; import { TabNavService } from '../../../../tab-nav.service'; import { CreateEndpointBaseStepComponent } from './create-endpoint-base-step.component'; @@ -27,15 +28,19 @@ describe('CreateEndpointBaseStepComponent', () => { createBasicStoreModule(), NoopAnimationsModule ], - providers: [{ - provide: ActivatedRoute, - useValue: { - snapshot: { - queryParams: {}, - params: { type: 'metrics' } + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + queryParams: {}, + params: { type: 'metrics' } + } } - } - }, TabNavService], + }, + TabNavService, + CurrentUserPermissionsService + ], }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts index 3df52ba96a..a074af1f7b 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts @@ -7,6 +7,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { SidePanelService } from '../../../shared/services/side-panel.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; @@ -49,7 +50,9 @@ describe('CreateEndpointComponent', () => { } }, TabNavService, - SidePanelService], + SidePanelService, + CurrentUserPermissionsService + ], }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/endpoints/edit-endpoint/edit-endpoint.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/edit-endpoint/edit-endpoint.component.spec.ts index 9e97689e70..6dbdbb0753 100644 --- a/src/frontend/packages/core/src/features/endpoints/edit-endpoint/edit-endpoint.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/edit-endpoint/edit-endpoint.component.spec.ts @@ -6,6 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { createBasicStoreModule } from '../../../../../store/testing/public-api'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; import { EditEndpointStepComponent } from './edit-endpoint-step/edit-endpoint-step.component'; @@ -36,7 +37,8 @@ describe('EditEndpointComponent', () => { } } }, - TabNavService + TabNavService, + CurrentUserPermissionsService ] }) .compileComponents(); diff --git a/src/frontend/packages/core/src/features/event-page/events-page/events-page.component.spec.ts b/src/frontend/packages/core/src/features/event-page/events-page/events-page.component.spec.ts index ed0314a253..c79c2d4d5d 100644 --- a/src/frontend/packages/core/src/features/event-page/events-page/events-page.component.spec.ts +++ b/src/frontend/packages/core/src/features/event-page/events-page/events-page.component.spec.ts @@ -4,6 +4,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; import { EventsPageComponent } from './events-page.component'; @@ -22,7 +23,10 @@ describe('EventsPageComponent', () => { createBasicStoreModule(), RouterTestingModule ], - providers: [TabNavService] + providers: [ + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/home/home/home-page.component.spec.ts b/src/frontend/packages/core/src/features/home/home/home-page.component.spec.ts index be4e9bfb8e..9579ad3f19 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page.component.spec.ts +++ b/src/frontend/packages/core/src/features/home/home/home-page.component.spec.ts @@ -6,6 +6,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; import { HomePageComponent } from './home-page.component'; @@ -26,7 +27,10 @@ describe('HomePageComponent', () => { CoreTestingModule, createBasicStoreModule() ], - providers: [TabNavService] + providers: [ + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.spec.ts b/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.spec.ts index b183a885f1..ae19f047f3 100644 --- a/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.spec.ts +++ b/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.spec.ts @@ -6,6 +6,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; import { MetricsService } from '../services/metrics-service'; @@ -27,7 +28,11 @@ describe('MetricsComponent', () => { createBasicStoreModule(), ], declarations: [MetricsComponent], - providers: [MetricsService, TabNavService] + providers: [ + MetricsService, + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/no-endpoints-non-admin/no-endpoints-non-admin.component.spec.ts b/src/frontend/packages/core/src/features/no-endpoints-non-admin/no-endpoints-non-admin.component.spec.ts index e9cb274f2f..4e8f59f873 100644 --- a/src/frontend/packages/core/src/features/no-endpoints-non-admin/no-endpoints-non-admin.component.spec.ts +++ b/src/frontend/packages/core/src/features/no-endpoints-non-admin/no-endpoints-non-admin.component.spec.ts @@ -4,6 +4,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../test-framework/core-test.modules'; import { CoreModule } from '../../core/core.module'; +import { CurrentUserPermissionsService } from '../../core/permissions/current-user-permissions.service'; import { SharedModule } from '../../shared/shared.module'; import { TabNavService } from '../../tab-nav.service'; import { NoEndpointsNonAdminComponent } from './no-endpoints-non-admin.component'; @@ -22,7 +23,10 @@ describe('NoEndpointsNonAdminComponent', () => { CoreTestingModule, createBasicStoreModule(), ], - providers: [TabNavService] + providers: [ + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/setup/local-account-wizard/local-account-wizard.component.spec.ts b/src/frontend/packages/core/src/features/setup/local-account-wizard/local-account-wizard.component.spec.ts index 9e2b3ba3d8..a15ea5f550 100644 --- a/src/frontend/packages/core/src/features/setup/local-account-wizard/local-account-wizard.component.spec.ts +++ b/src/frontend/packages/core/src/features/setup/local-account-wizard/local-account-wizard.component.spec.ts @@ -6,6 +6,7 @@ import { createEmptyStoreModule } from '@stratosui/store/testing'; import { CoreModule } from '../../../core/core.module'; import { MDAppModule } from '../../../core/md.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { PageHeaderModule } from '../../../shared/components/page-header/page-header.module'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; @@ -30,7 +31,10 @@ describe('LocalAccountWizardComponent', () => { createEmptyStoreModule(), NoopAnimationsModule, ], - providers: [TabNavService] + providers: [ + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/setup/uaa-wizard/console-uaa-wizard.component.spec.ts b/src/frontend/packages/core/src/features/setup/uaa-wizard/console-uaa-wizard.component.spec.ts index 558b44dcff..56353578c4 100644 --- a/src/frontend/packages/core/src/features/setup/uaa-wizard/console-uaa-wizard.component.spec.ts +++ b/src/frontend/packages/core/src/features/setup/uaa-wizard/console-uaa-wizard.component.spec.ts @@ -7,6 +7,7 @@ import { StoreModule } from '@ngrx/store'; import { appReducers } from '../../../../../store/src/reducers.module'; import { CoreModule } from '../../../core/core.module'; import { MDAppModule } from '../../../core/md.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { PageHeaderModule } from '../../../shared/components/page-header/page-header.module'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; @@ -31,7 +32,10 @@ describe('ConsoleUaaWizardComponent', () => { StoreModule.forRoot(appReducers), NoopAnimationsModule, ], - providers: [TabNavService] + providers: [ + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.spec.ts b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.spec.ts index 496f43f9d2..e10f58880b 100644 --- a/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.spec.ts +++ b/src/frontend/packages/core/src/features/user-profile/profile-info/profile-info.component.spec.ts @@ -6,6 +6,7 @@ import { createBasicStoreModule } from '@stratosui/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; import { CoreModule } from '../../../core/core.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { UserProfileService } from '../../../core/user-profile.service'; import { SharedModule } from '../../../shared/shared.module'; import { TabNavService } from '../../../tab-nav.service'; @@ -27,7 +28,11 @@ describe('ProfileInfoComponent', () => { CoreTestingModule, createBasicStoreModule() ], - providers: [UserProfileService, TabNavService] + providers: [ + UserProfileService, + TabNavService, + CurrentUserPermissionsService + ] }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.spec.ts b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.spec.ts index fe79d0308e..dfa81f3643 100644 --- a/src/frontend/packages/core/src/shared/components/page-header/page-header.component.spec.ts +++ b/src/frontend/packages/core/src/shared/components/page-header/page-header.component.spec.ts @@ -7,6 +7,7 @@ import { InternalEventMonitorFactory } from '../../../../../store/src/monitors/i import { appReducers } from '../../../../../store/src/reducers.module'; import { CoreModule } from '../../../core/core.module'; import { MDAppModule } from '../../../core/md.module'; +import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { TabNavService } from '../../../tab-nav.service'; import { SharedModule } from '../../shared.module'; import { PageHeaderComponent } from './page-header.component'; @@ -28,7 +29,8 @@ describe('PageHeaderComponent', () => { } } }, - TabNavService + TabNavService, + CurrentUserPermissionsService ], imports: [ MDAppModule,